[IMP] core: better API for flush() and invalidate()

This provides a new API for those operations, in order to make the
distinction between the use cases more explicit.  The former API was
using obscure parameter combinations to correspond to various cases.

In the summary below, `fnames` is an iterable of field names.  If the
parameter is not given, it means "all fields" in the given context.
Note that method recompute() is now mostly private, as it should not be
used in business code.

    # process pending computations and updates
    records.env.flush_all()                # all fields of all models
    records.flush_model(fnames)            # the fields of all records of the model
    records.flush_recordset(fnames)        # the fields of the given records

    # process pending computations, became non-public methods
    records.env._recompute_all()           # all fields of all models
    records._recompute_model(fnames)       # the fields of all records of the model
    records._recompute_recordset(fnames)   # the fields of the given records

    # invalidate the cache of fields
    records.env.invalidate_all()           # all fields of all models
    records.invalidate_model(fnames)       # the fields of all records of the model
    records.invalidate_recordset(fnames)   # the fields of the given records

Part-of: odoo/odoo#87527
This commit is contained in:
Raphael Collet
2022-05-25 18:00:46 +02:00
parent 5ceef05996
commit 32bc28aa66
7 changed files with 242 additions and 134 deletions
+18 -3
View File
@@ -459,7 +459,7 @@ def call_kw(model, name, args, kwargs):
result = _call_kw_model_create(method, model, args, kwargs)
else:
result = _call_kw_multi(method, model, args, kwargs)
model.flush()
model.env.flush_all()
return result
@@ -701,6 +701,21 @@ class Environment(Mapping):
)
return self.cr.savepoint()
def invalidate_all(self):
""" Invalidate the cache of all records. """
self.cache.invalidate()
def _recompute_all(self):
""" Process all pending computations. """
for field in list(self.fields_to_compute()):
self[field.model_name]._recompute_field(field)
def flush_all(self):
""" Flush all pending computations and updates to the database. """
self._recompute_all()
for model_name in list(self.all.towrite):
self[model_name].flush_model()
def is_protected(self, field, record):
""" Return whether `record` is protected against invalidation or
recomputation for `field`.
@@ -834,7 +849,7 @@ class Transaction:
if env.uid is not None:
break
if env_to_flush is not None:
env_to_flush['base'].flush()
env_to_flush.flush_all()
def clear(self):
""" Clear the caches and pending computations and updates in the translations. """
@@ -983,7 +998,7 @@ class Cache(object):
def check(self, env):
""" Check the consistency of the cache for the given environment. """
# flush fields to be recomputed before evaluating the cache
env['res.partner'].recompute()
env._recompute_all()
# make a copy of the cache, and invalidate it
dump = dict(self._data)
+1 -1
View File
@@ -80,7 +80,7 @@ class Populate(Command):
# force the flush to make sure population time still
# considers flushing all values to database
env['base'].flush()
env.flush_all()
if commit:
env.cr.commit()
+2 -2
View File
@@ -1024,7 +1024,7 @@ class Field(MetaField('DummyField', (object,), {})):
@model.pool.post_init
def add_not_null():
# flush values before adding NOT NULL constraint
model.flush([self.name])
model.flush_model([self.name])
model.pool.post_constraint(apply_required, model, self.name)
elif not self.required and has_notnull:
@@ -3472,7 +3472,7 @@ class One2many(_RelationalMulti):
# cache value of self is consistent
inverse_field = records.pool[self.comodel_name]._fields[self.inverse_name]
if inverse_field.compute:
records.env[self.comodel_name].recompute([self.inverse_name])
records.env[self.comodel_name]._recompute_model([self.inverse_name])
return super().__get__(records, owner)
def read(self, records):
+195 -102
View File
@@ -908,7 +908,7 @@ class BaseModel(metaclass=MetaModel):
)
finally:
psycopg2.extensions.set_wait_callback(callback)
self.env['ir.model.data'].invalidate_cache(fnames=fields)
self.env['ir.model.data'].invalidate_model(fields)
return (
(record, to_xid(record.id))
@@ -935,7 +935,7 @@ class BaseModel(metaclass=MetaModel):
sub = rs[idx:idx+1000]
for rec in sub:
yield rec
rs.invalidate_cache(ids=sub.ids)
sub.invalidate_recordset()
if not _is_toplevel_call:
splittor = lambda rs: rs
@@ -1060,7 +1060,7 @@ class BaseModel(metaclass=MetaModel):
:type data: list(list(str))
:returns: {ids: list(int)|False, messages: [Message][, lastrow: int]}
"""
self.flush()
self.env.flush_all()
# determine values of mode, current_module and noupdate
mode = self._context.get('mode', 'init')
@@ -1171,15 +1171,15 @@ class BaseModel(metaclass=MetaModel):
# make 'flush' available to the methods below, in the case where XMLID
# resolution fails, for instance
flush_self = self.with_context(import_flush=flush, import_cache=LRU(1024))
flush_recordset = self.with_context(import_flush=flush, import_cache=LRU(1024))
# TODO: break load's API instead of smuggling via context?
limit = self._context.get('_import_limit')
if limit is None:
limit = float('inf')
extracted = flush_self._extract_records(fields, data, log=messages.append, limit=limit)
extracted = flush_recordset._extract_records(fields, data, log=messages.append, limit=limit)
converted = flush_self._convert_records(extracted, log=messages.append)
converted = flush_recordset._convert_records(extracted, log=messages.append)
info = {'rows': {'to': -1}}
for id, xid, record, info in converted:
@@ -2837,7 +2837,7 @@ class BaseModel(metaclass=MetaModel):
WHERE row.id = comp.id
""".format(table=self._table, parent=self._parent_name)
self.env.cr.execute(query)
self.invalidate_cache(['parent_path'])
self.invalidate_model(['parent_path'])
return True
def _check_removed_columns(self, log=False):
@@ -3419,7 +3419,7 @@ class BaseModel(metaclass=MetaModel):
# if a read() follows a write(), we must flush updates, as read() will
# fetch from database and overwrites the cache (`test_update_with_id`)
self.flush(fields, self)
self.flush_recordset(fields)
field_names = []
inherited_field_names = []
@@ -3761,7 +3761,7 @@ class BaseModel(metaclass=MetaModel):
# mark fields that depend on 'self' to recompute them after 'self' has
# been deleted (like updating a sum of lines after deleting one line)
self.flush()
self.env.flush_all()
self.modified(self._fields, before=True)
with self.env.norecompute():
@@ -3819,14 +3819,14 @@ class BaseModel(metaclass=MetaModel):
# invalidate the *whole* cache, since the orm does not handle all
# changes made in the database, like cascading delete!
self.invalidate_cache()
self.env.invalidate_all()
if ir_model_data_unlink:
ir_model_data_unlink.unlink()
if ir_attachment_unlink:
ir_attachment_unlink.unlink()
# DLE P93: flush after the unlink, for recompute fields depending on
# the modified of the unlink
self.flush()
self.env.flush_all()
# auditing: deletions are infrequent and leave no trace in the database
_unlink.info('User #%s deleted %s records with IDs: %r', self._uid, self._name, self.ids)
@@ -3943,7 +3943,7 @@ class BaseModel(metaclass=MetaModel):
for field in protected
if field.compute and field.name not in vals]
if to_compute:
self.recompute(to_compute, self)
self._recompute_recordset(to_compute)
# protect fields being written against recomputation
with env.protecting(protected, self):
@@ -3992,7 +3992,7 @@ class BaseModel(metaclass=MetaModel):
self.modified(vals)
if self._parent_store and self._parent_name in vals:
self.flush([self._parent_name])
self.flush_model([self._parent_name])
# validate non-inversed fields first
inverse_fields = [f.name for fs in determine_inverses.values() for f in fs]
@@ -4840,7 +4840,7 @@ class BaseModel(metaclass=MetaModel):
models.append(self.env[model_name])
for model_name, field_names in to_flush.items():
self.env[model_name].flush(field_names)
self.env[model_name].flush_model(field_names)
@api.model
def _search(self, domain, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
@@ -5067,7 +5067,7 @@ class BaseModel(metaclass=MetaModel):
# must ignore 'active' flag, ir.rules, etc. => direct SQL query
cr = self._cr
self.flush([parent])
self.flush_model([parent])
query = 'SELECT "%s" FROM "%s" WHERE id = %%s' % (parent, self._table)
for id in self.ids:
current_id = id
@@ -5093,7 +5093,7 @@ class BaseModel(metaclass=MetaModel):
# field must be a many2many on itself
raise ValueError('invalid field_name: %r' % (field_name,))
self.flush([field_name])
self.flush_model([field_name])
cr = self._cr
query = 'SELECT "%s", "%s" FROM "%s" WHERE "%s" IN %%s AND "%s" IS NOT NULL' % \
@@ -5775,63 +5775,92 @@ class BaseModel(metaclass=MetaModel):
:param Model records: if given (together with ``fnames``), limit the
processing to the given records.
"""
warnings.warn(
"Deprecated method flush(), use flush_model(), flush_recordset() or env.flush_all() instead",
DeprecationWarning,
)
if fnames is None:
self.env.flush_all()
elif records is None:
self.flush_model(fnames)
else:
records.flush_recordset(fnames)
def flush_model(self, fnames=None):
""" Process the pending computations and database updates on ``self``'s
model. When the parameter is given, the method guarantees that at least
the given fields are flushed to the database. More fields can be
flushed, though.
:param fnames: optional iterable of field names to flush
"""
self._recompute_model(fnames)
self._flush(fnames)
def flush_recordset(self, fnames=None):
""" Process the pending computations and database updates on the records
``self``. When the parameter is given, the method guarantees that at
least the given fields on records ``self`` are flushed to the database.
More fields and records can be flushed, though.
:param fnames: optional iterable of field names to flush
"""
self._recompute_recordset(fnames)
towrite = self.env.all.towrite.get(self._name)
if fnames is None:
must_flush = towrite and any(id_ in towrite for id_ in self._ids)
else:
must_flush = towrite and any(
fname in vals
for id_ in self._ids
for vals in towrite.get(id_, ())
for fname in fnames
)
if must_flush:
self._flush(fnames)
def _flush(self, fnames=None):
def process(model, id_vals):
# group record ids by vals, to update in batch when possible
updates = defaultdict(list)
for rid, vals in id_vals.items():
updates[frozendict(vals)].append(rid)
for id_, vals in id_vals.items():
updates[frozendict(vals)].append(id_)
for vals, ids in updates.items():
model.browse(ids)._write(vals)
# DLE P76: test_onchange_one2many_with_domain_on_related_field
# ```
# email.important = True
# self.assertIn(email, discussion.important_emails)
# ```
# When a search on a field coming from a related occurs (the domain
# on discussion.important_emails field), make sure the related field
# is flushed
if fnames is None:
# flush everything
self.recompute()
while self.env.all.towrite:
model_name, id_vals = self.env.all.towrite.popitem()
process(self.env[model_name], id_vals)
fields = self._fields.values()
else:
# flush self's model if any of the fields must be flushed
self.recompute(fnames, records=records)
fields = [self._fields[fname] for fname in fnames]
# check whether any of 'records' must be flushed
if records is not None:
fnames = set(fnames)
towrite = self.env.all.towrite.get(self._name)
if not towrite or all(
fnames.isdisjoint(towrite.get(record.id, ()))
for record in records
):
return
model_fields = defaultdict(list)
for field in fields:
model_fields[field.model_name].append(field)
if field.related_field:
model_fields[field.related_field.model_name].append(field.related_field)
# DLE P76: test_onchange_one2many_with_domain_on_related_field
# ```
# email.important = True
# self.assertIn(email, discussion.important_emails)
# ```
# When a search on a field coming from a related occurs (the domain
# on discussion.important_emails field), make sure the related field
# is flushed
model_fields = {}
for fname in fnames:
field = self._fields[fname]
model_fields.setdefault(field.model_name, []).append(field)
if field.related_field:
model_fields.setdefault(field.related_field.model_name, []).append(field.related_field)
for model_name, fields in model_fields.items():
if any(
field.name in vals
for vals in self.env.all.towrite.get(model_name, {}).values()
for field in fields
):
id_vals = self.env.all.towrite.pop(model_name)
process(self.env[model_name], id_vals)
for model_name, fields_ in model_fields.items():
if any(
field.name in vals
for vals in self.env.all.towrite.get(model_name, {}).values()
for field in fields_
):
id_vals = self.env.all.towrite.pop(model_name)
process(self.env[model_name], id_vals)
# missing for one2many fields, flush their inverse
for fname in fnames:
field = self._fields[fname]
if field.type == 'one2many' and field.inverse_name:
self.env[field.comodel_name].flush([field.inverse_name])
# flush the inverse of one2many fields, too
for field in fields:
if field.type == 'one2many' and field.inverse_name:
self.env[field.comodel_name].flush_model([field.inverse_name])
#
# New records - represent records that do not exist in the database yet;
@@ -6099,7 +6128,7 @@ class BaseModel(metaclass=MetaModel):
"""
warnings.warn('refresh() is deprecated method, use invalidate_cache() instead',
DeprecationWarning)
self.invalidate_cache()
self.env.invalidate_all()
@api.model
def invalidate_cache(self, fnames=None, ids=None):
@@ -6109,16 +6138,45 @@ class BaseModel(metaclass=MetaModel):
:param fnames: the list of modified fields, or ``None`` for all fields
:param ids: the list of modified record ids, or ``None`` for all
"""
if fnames is None:
if ids is None:
return self.env.cache.invalidate()
fields = list(self._fields.values())
warnings.warn(
"Deprecated method invalidate_cache(), use invalidate_model(), invalidate_recordset() or env.invalidate_all() instead",
DeprecationWarning,
)
if ids is not None:
self.browse(ids).invalidate_recordset(fnames)
elif fnames is not None:
self.invalidate_model(fnames)
else:
fields = [self._fields[n] for n in fnames]
self.env.invalidate_all()
# invalidate fields and inverse fields, too
spec = [(f, ids) for f in fields] + \
[(invf, None) for f in fields for invf in self.pool.field_inverses[f]]
def invalidate_model(self, fnames=None):
""" Invalidate the cache of all records of ``self``'s model, when the
cached values no longer correspond to the database values. If the
parameter is given, only the given fields are invalidated from cache.
:param fnames: optional iterable of field names to invalidate
"""
self._invalidate_cache(fnames)
def invalidate_recordset(self, fnames=None):
""" Invalidate the cache of the records in ``self``, when the cached
values no longer correspond to the database values. If the parameter
is given, only the given fields on ``self`` are invalidated from cache.
:param fnames: optional iterable of field names to invalidate
"""
self._invalidate_cache(fnames, self._ids)
def _invalidate_cache(self, fnames=None, ids=None):
if fnames is None:
fields = self._fields.values()
else:
fields = [self._fields[fname] for fname in fnames]
spec = []
for field in fields:
spec.append((field, ids))
for invf in self.pool.field_inverses[field]:
spec.append((invf, None))
self.env.cache.invalidate(spec)
def modified(self, fnames, create=False, before=False):
@@ -6260,38 +6318,73 @@ class BaseModel(metaclass=MetaModel):
The fields and records to recompute have been determined by method
:meth:`modified`.
"""
def process(field):
recs = self.env.records_to_compute(field)
if (not recs) or (records is not None and not (records & recs)):
return
if field.store:
# do not force recomputation on new records; those will be
# recomputed by accessing the field on the records
recs = recs.filtered('id')
try:
field.recompute(recs)
except MissingError:
existing = recs.exists()
field.recompute(existing)
# mark the field as computed on missing records, otherwise
# they remain forever in the todo list, and lead to an
# infinite loop...
for f in recs.pool.field_computed[field]:
self.env.remove_to_compute(f, recs - existing)
else:
self.env.cache.invalidate([(field, recs._ids)])
self.env.remove_to_compute(field, recs)
warnings.warn(
"Deprecated method recompute(), use flush_model(), flush_recordset() or env.flush_all() instead",
DeprecationWarning,
stacklevel=2,
)
if fnames is None:
# recompute everything
for field in list(self.env.fields_to_compute()):
process(field)
self.env._recompute()
elif records is None:
self._recompute_model(fnames)
else:
# recompute the given fields on self's model
for fname in fnames:
field = self._fields[fname]
if field.compute:
process(field)
records._recompute_recordset(fnames)
def _recompute_model(self, fnames=None):
""" Process the pending computations of the fields of ``self``'s model.
:param fnames: optional iterable of field names to compute
"""
if fnames is None:
fields = self._fields.values()
else:
fields = [self._fields[fname] for fname in fnames]
for field in fields:
if field.compute:
self._recompute_field(field)
def _recompute_recordset(self, fnames=None):
""" Process the pending computations of the fields of the records in ``self``.
:param fnames: optional iterable of field names to compute
"""
if fnames is None:
fields = self._fields.values()
else:
fields = [self._fields[fname] for fname in fnames]
for field in fields:
if field.compute:
self._recompute_field(field, self._ids)
def _recompute_field(self, field, ids=None):
ids_to_compute = self.env.all.tocompute.get(field, ())
if ids is None:
ids = ids_to_compute
else:
ids = [id_ for id_ in ids if id_ in ids_to_compute]
if not ids:
return
records = self.browse(ids)
if field.store:
# do not force recomputation on new records; those will be
# recomputed by accessing the field on the records
records = records.filtered('id')
try:
field.recompute(records)
except MissingError:
existing = records.exists()
field.recompute(existing)
# mark the field as computed on missing records, otherwise
# they remain forever in the todo list, and lead to an
# infinite loop...
for f in records.pool.field_computed[field]:
self.env.remove_to_compute(f, records - existing)
else:
self.env.cache.invalidate([(field, records._ids)])
self.env.remove_to_compute(field, records)
#
# Generic onchange method
@@ -6388,7 +6481,7 @@ class BaseModel(metaclass=MetaModel):
methods to them, and return all the fields in ``field_onchange``.
"""
# this is for tests using `Form`
self.flush()
self.env.flush_all()
env = self.env
if isinstance(field_name, list):
@@ -6653,7 +6746,7 @@ class BaseModel(metaclass=MetaModel):
snapshot1 = Snapshot(record, nametree)
# determine values that have changed by comparing snapshots
self.invalidate_cache()
self.env.invalidate_all()
result['value'] = snapshot1.diff(snapshot0, force=first_call)
# format warnings
+8 -8
View File
@@ -119,7 +119,7 @@ def force_demo(cr):
load_demo(cr, package, {}, 'init')
env = api.Environment(cr, SUPERUSER_ID, {})
env['ir.module.module'].invalidate_cache(['demo'])
env['ir.module.module'].invalidate_model(['demo'])
env['res.groups']._update_user_groups_view()
@@ -183,7 +183,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
migrations.migrate_module(package, 'pre')
if package.name != 'base':
env = api.Environment(cr, SUPERUSER_ID, {})
env['base'].flush()
env.flush_all()
load_openerp_module(package.name)
@@ -230,7 +230,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
load_data(cr, idref, mode, kind='data', package=package)
demo_loaded = package.dbdemo = load_demo(cr, package, idref, mode)
cr.execute('update ir_module_module set demo=%s where id=%s', (demo_loaded, module_id))
module.invalidate_cache(['demo'])
module.invalidate_model(['demo'])
migrations.migrate_module(package, 'post')
@@ -308,7 +308,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True,
for kind in ('init', 'demo', 'update'):
if hasattr(package, kind):
delattr(package, kind)
module.flush()
module.env.flush_all()
extra_queries = odoo.sql_db.sql_counter - module_extra_query_count - test_queries
extras = []
@@ -454,9 +454,9 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
if modules:
modules.button_upgrade()
env.flush_all()
cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
Module.invalidate_cache(['state'])
Module.flush()
Module.invalidate_model(['state'])
# STEP 3: Load marked modules (skipping base which was done in STEP 1)
# IMPORTANT: this is done in two parts, first loading all installed or
@@ -519,7 +519,7 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
# Cleanup orphan records
env['ir.model.data']._process_end(processed_modules)
env['base'].flush()
env.flush_all()
for kind in ('init', 'demo', 'update'):
tools.config[kind] = {}
@@ -587,7 +587,7 @@ def load_modules(registry, force_demo=False, status=None, update_module=False):
env = api.Environment(cr, SUPERUSER_ID, {})
for model in env.values():
model._register_hook()
env['base'].flush()
env.flush_all()
# STEP 9: save installed/updated modules for post-install tests
registry.updated_modules += processed_modules
+3 -3
View File
@@ -306,7 +306,7 @@ class Registry(Mapping):
if self.ready:
for model in env.values():
model._register_hook()
env['base'].flush()
env.flush_all()
@lazy_property
def field_computed(self):
@@ -445,7 +445,7 @@ class Registry(Mapping):
self.check_indexes(cr, model_names)
self.check_foreign_keys(cr)
env['base'].flush()
env.flush_all()
# make sure all tables are present
self.check_tables_exist(cr)
@@ -561,7 +561,7 @@ class Registry(Mapping):
for name in missing:
_logger.info("Recreate table of model %s.", name)
env[name].init()
env['base'].flush()
env.flush_all()
# check again, and log errors if tables are still missing
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
for table in missing_tables:
+15 -15
View File
@@ -496,14 +496,14 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
return lambda x: x
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
with patch('odoo.sql_db.Cursor.execute', execute):
with patch('odoo.osv.expression.get_unaccent_wrapper', get_unaccent_wrapper):
yield actual_queries
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
self.assertEqual(
@@ -538,12 +538,12 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
login = self.env.user.login
expected = counters.get(login, default)
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
count0 = self.cr.sql_log_count
yield
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
count = self.cr.sql_log_count - count0
if count != expected:
@@ -562,11 +562,11 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
# flush before and after during warmup, in order to reproduce the
# same operations, otherwise the caches might not be ready!
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
yield
if flush:
self.env.user.flush()
self.env.flush_all()
self.env.cr.flush()
def assertRecordValues(self, records, expected_values):
@@ -772,7 +772,7 @@ class TransactionCase(BaseCase):
self.addCleanup(self.env.clear)
# flush everything in setUpClass before introducing a savepoint
self.env['base'].flush()
self.env.flush_all()
self._savepoint_id = next(savepoint_seq)
self.cr.execute('SAVEPOINT test_%d' % self._savepoint_id)
@@ -816,7 +816,7 @@ class SingleTransactionCase(BaseCase):
def setUp(self):
super(SingleTransactionCase, self).setUp()
self.env.user.flush()
self.env.flush_all()
class ChromeBrowserException(Exception):
@@ -1509,7 +1509,7 @@ class HttpCase(TransactionCase):
ICP = cls.env['ir.config_parameter']
ICP.set_param('web.base.url', cls.base_url())
ICP.flush()
ICP.env.flush_all()
# v8 api with correct xmlrpc exception handling.
cls.xmlrpc_url = f'http://{HOST}:{odoo.tools.config["http_port"]:d}/xmlrpc/2/'
cls._logger = logging.getLogger('%s.%s' % (cls.__module__, cls.__name__))
@@ -1737,16 +1737,16 @@ def warmup(func, *args, **kwargs):
effects of the warmup phase are rolled back thanks to a savepoint.
"""
self = args[0]
self.env['base'].flush()
self.env.cache.invalidate()
self.env.flush_all()
self.env.invalidate_all()
# run once to warm up the caches
self.warm = False
self.cr.execute('SAVEPOINT test_warmup')
func(*args, **kwargs)
self.env['base'].flush()
self.env.flush_all()
# run once for real
self.cr.execute('ROLLBACK TO SAVEPOINT test_warmup')
self.env.cache.invalidate()
self.env.invalidate_all()
self.warm = True
func(*args, **kwargs)
@@ -2127,7 +2127,7 @@ class Form(object):
record_to_values(self._view['fields'], r)
)
self._changed.clear()
self._model.flush()
self._model.env.flush_all()
self._model.env.clear() # discard cache and pending recomputations
return r
@@ -2244,7 +2244,7 @@ class Form(object):
record = self._model.browse(self._values.get('id'))
result = record.onchange(self._onchange_values(), fields, spec)
self._model.flush()
self._model.env.flush_all()
self._model.env.clear() # discard cache and pending recomputations
if result.get('warning'):
_logger.getChild('onchange').warning("%(title)s %(message)s" % result.get('warning'))