17.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:05:14 +02:00
parent 2e65bf056a
commit df627a6bba
328 changed files with 578149 additions and 759311 deletions

View file

@ -29,7 +29,7 @@ except ImportError:
from decorator import decorator
from .exceptions import AccessError, CacheMiss
from .tools import classproperty, frozendict, lazy_property, OrderedSet, Query, StackMap
from .tools import clean_context, frozendict, lazy_property, OrderedSet, Query, SQL, StackMap
from .tools.translate import _
_logger = logging.getLogger(__name__)
@ -498,22 +498,6 @@ class Environment(Mapping):
names to models. It also holds a cache for records, and a data
structure to manage recomputations.
"""
@classproperty
def envs(cls):
raise NotImplementedError(
"Since Odoo 15.0, Environment.envs no longer works; "
"use cr.transaction or env.transaction instead."
)
@classmethod
@contextmanager
def manage(cls):
warnings.warn(
"Since Odoo 15.0, Environment.manage() is useless.",
DeprecationWarning, stacklevel=2,
)
yield
def reset(self):
""" Reset the transaction, see :meth:`Transaction.reset`. """
self.transaction.reset()
@ -596,7 +580,8 @@ class Environment(Mapping):
"""
cr = self.cr if cr is None else cr
uid = self.uid if user is None else int(user)
context = self.context if context is None else context
if context is None:
context = clean_context(self.context) if su and not self.su else self.context
su = (user is None and self.su) if su is None else su
return Environment(cr, uid, context, su, self.uid_origin)
@ -655,7 +640,7 @@ class Environment(Mapping):
.. warning::
No sanity checks applied in sudo mode !
No sanity checks applied in sudo mode!
When in sudo mode, a user can access any company,
even if not in his allowed companies.
@ -723,33 +708,14 @@ class Environment(Mapping):
# because 'env.lang' may be injected in SQL queries
return lang if lang and self['res.lang']._lang_get_id(lang) else None
@property
def ocb(self):
"""Allow to flag OCB environment so we can easily address compatibility issues
when making backports or improvements that aren't present in the current Odoo
version.
:rtype bool
"""
return True
def clear(self):
""" Clear all record caches, and discard all fields to recompute.
This may be useful when recovering from a failed ORM operation.
"""
lazy_property.reset_all(self)
self._cache_key.clear()
self.transaction.clear()
def clear_upon_failure(self):
""" Context manager that rolls back the environments (caches and pending
computations and updates) upon exception.
"""
warnings.warn(
"Since Odoo 15.0, use cr.savepoint() instead of env.clear_upon_failure().",
DeprecationWarning, stacklevel=2,
)
return self.cr.savepoint()
def invalidate_all(self, flush=True):
""" Invalidate the cache of all records.
@ -846,7 +812,8 @@ class Environment(Mapping):
@contextmanager
def norecompute(self):
""" Delay recomputations (deprecated: this is not the default behavior). """
""" Deprecated: It does nothing, recomputation is delayed by default. """
warnings.warn("`norecompute` is useless. Deprecated since 17.0.", DeprecationWarning, 2)
yield
def cache_key(self, field):
@ -864,6 +831,8 @@ class Environment(Mapping):
return get_context('lang') or None
elif key == 'active_test':
return get_context('active_test', field.context.get('active_test', True))
elif key.startswith('bin_size'):
return bool(get_context(key))
else:
val = get_context(key)
if type(val) is list:
@ -890,6 +859,7 @@ class Transaction:
self.registry = registry
# weak set of environments
self.envs = WeakSet()
self.envs.data = OrderedSet() # make the weakset OrderedWeakSet
# cache for all records
self.cache = Cache()
# fields to protect {field: ids}
@ -922,6 +892,7 @@ class Transaction:
for env in self.envs:
env.registry = self.registry
lazy_property.reset_all(env)
env._cache_key.clear()
self.clear()
@ -960,6 +931,10 @@ class Cache(object):
# in `_data`
self._dirty = defaultdict(OrderedSet)
# {field: {record_id: ids}} record ids to be added to the values of
# x2many fields if they are not in cache yet
self._patches = defaultdict(lambda: defaultdict(list))
def __repr__(self):
# for debugging: show the cache content and dirty flags as stars
data = {}
@ -1001,7 +976,7 @@ class Cache(object):
cache_value = field_cache.get(record.id, EMPTY_DICT)
if cache_value is None:
return True
lang = record.env.lang or 'en_US'
lang = field._lang(record.env)
return lang in cache_value
return record.id in field_cache
@ -1022,12 +997,14 @@ class Cache(object):
field_cache = self._get_field_cache(record, field)
cache_value = field_cache[record._ids[0]]
if field.translate and cache_value is not None:
lang = record.env.lang or 'en_US'
lang = field._lang(record.env)
if not (field.compute or field.store and record._origin):
return cache_value.get(lang, cache_value.get('en_US'))
return cache_value[lang]
return cache_value
except KeyError:
if default is NOTHING:
raise CacheMiss(record, field)
raise CacheMiss(record, field) from None
return default
def set(self, record, field, value, dirty=False, check_dirty=True):
@ -1042,26 +1019,33 @@ class Cache(object):
dirty must raise an exception
"""
field_cache = self._set_field_cache(record, field)
record_id = record.id
if field.translate and value is not None:
# only for model translated fields
lang = record.env.lang or 'en_US'
cache_value = field_cache.get(record._ids[0]) or {}
cache_value = field_cache.get(record_id) or {}
cache_value[lang] = value
if not (field.compute or field.store and record._origin):
cache_value.setdefault('en_US', value)
value = cache_value
field_cache[record._ids[0]] = value
field_cache[record_id] = value
if not check_dirty:
return
if dirty:
assert field.column_type and field.store and record.id
self._dirty[field].add(record.id)
assert field.column_type and field.store and record_id
self._dirty[field].add(record_id)
if record.pool.field_depends_context[field]:
# put the values under conventional context key values {'context_key': None},
# in order to ease the retrieval of those values to flush them
context_none = dict.fromkeys(record.pool.field_depends_context[field])
record = record.with_env(record.env(context=context_none))
field_cache = self._set_field_cache(record, field)
field_cache[record._ids[0]] = value
elif record.id in self._dirty.get(field, ()):
field_cache[record_id] = value
elif record_id in self._dirty.get(field, ()):
_logger.error("cache.set() removing flag dirty on %s.%s", record, field.name, stack_info=True)
def update(self, records, field, values, dirty=False, check_dirty=True):
@ -1076,15 +1060,18 @@ class Cache(object):
dirty must raise an exception
"""
if field.translate:
lang = records.env.lang or 'en_US'
# only for model translated fields
lang = (records.env.lang or 'en_US') if dirty else field._lang(records.env)
field_cache = self._get_field_cache(records, field)
cache_values = []
for id_, value in zip(records._ids, values):
for record, value in zip(records, values):
if value is None:
cache_values.append(None)
else:
cache_value = field_cache.get(id_) or {}
cache_value = field_cache.get(record.id) or {}
cache_value[lang] = value
if not (field.compute or field.store and record._origin):
cache_value.setdefault('en_US', value)
cache_values.append(cache_value)
values = cache_values
@ -1120,18 +1107,61 @@ class Cache(object):
"""
field_cache = self._set_field_cache(records, field)
if field.translate:
lang = records.env.lang or 'en_US'
for id_, val in zip(records._ids, values):
if val is None:
field_cache.setdefault(id_, None)
else:
cache_value = field_cache.setdefault(id_, {})
if cache_value is not None:
cache_value.setdefault(lang, val)
if records.env.context.get('prefetch_langs'):
langs = {lang for lang, _ in records.env['res.lang'].get_installed()} | {'en_US'}
_langs = {f'_{l}' for l in langs} if field._lang(records.env).startswith('_') else set()
for id_, val in zip(records._ids, values):
if val is None:
field_cache.setdefault(id_, None)
else:
if _langs: # fallback missing _lang to lang if exists
val.update({f'_{k}': v for k, v in val.items() if k in langs and f'_{k}' not in val})
field_cache[id_] = {
**dict.fromkeys(langs, val['en_US']), # fallback missing lang to en_US
**dict.fromkeys(_langs, val.get('_en_US')), # fallback missing _lang to _en_US
**val
}
else:
lang = field._lang(records.env)
for id_, val in zip(records._ids, values):
if val is None:
field_cache.setdefault(id_, None)
else:
cache_value = field_cache.setdefault(id_, {})
if cache_value is not None:
cache_value.setdefault(lang, val)
else:
for id_, val in zip(records._ids, values):
field_cache.setdefault(id_, val)
def patch(self, records, field, new_id):
""" Apply a patch to an x2many field on new records. The patch consists
in adding new_id to its value in cache. If the value is not in cache
yet, it will be applied once the value is put in cache with method
:meth:`patch_and_set`.
"""
assert not new_id, "Cache.patch can only be called with a new id"
field_cache = self._set_field_cache(records, field)
for id_ in records._ids:
assert not id_, "Cache.patch can only be called with new records"
if id_ in field_cache:
field_cache[id_] = tuple(dict.fromkeys(field_cache[id_] + (new_id,)))
else:
self._patches[field][id_].append(new_id)
def patch_and_set(self, record, field, value):
""" Set the value of ``field`` for ``record``, like :meth:`set`, but
apply pending patches to ``value`` and return the value actually put
in cache.
"""
field_patches = self._patches.get(field)
if field_patches:
ids = field_patches.pop(record.id, ())
if ids:
value = tuple(dict.fromkeys(value + tuple(ids)))
self.set(record, field, value)
return value
def remove(self, record, field):
""" Remove the value of ``field`` for ``record``. """
assert record.id not in self._dirty.get(field, ())
@ -1154,7 +1184,7 @@ class Cache(object):
""" Return the cached values of ``field`` for ``records`` until a value is not found. """
field_cache = self._get_field_cache(records, field)
if field.translate:
lang = records.env.lang or 'en_US'
lang = field._lang(records.env)
def get_value(id_):
cache_value = field_cache[id_]
@ -1174,7 +1204,7 @@ class Cache(object):
""" Return the subset of ``records`` that has not ``value`` for ``field``. """
field_cache = self._get_field_cache(records, field)
if field.translate:
lang = records.env.lang or 'en_US'
lang = field._lang(records.env)
def get_value(id_):
cache_value = field_cache[id_]
@ -1217,7 +1247,7 @@ class Cache(object):
""" Return the ids of ``records`` that have no value for ``field``. """
field_cache = self._get_field_cache(records, field)
if field.translate:
lang = records.env.lang or 'en_US'
lang = field._lang(records.env)
for record_id in records._ids:
cache_value = field_cache.get(record_id, False)
if cache_value is False or not (cache_value is None or lang in cache_value):
@ -1292,6 +1322,7 @@ class Cache(object):
""" Invalidate the cache and its dirty flags. """
self._data.clear()
self._dirty.clear()
self._patches.clear()
def check(self, env):
""" Check the consistency of the cache for the given environment. """
@ -1307,14 +1338,14 @@ class Cache(object):
# select the column for the given ids
query = Query(env.cr, model._table, model._table_query)
qname = model._inherits_join_calc(model._table, field.name, query)
sql_id = SQL.identifier(model._table, 'id')
sql_field = model._field_to_sql(model._table, field.name, query)
if field.type == 'binary' and (
model.env.context.get('bin_size') or model.env.context.get('bin_size_' + field.name)
):
qname = f'pg_size_pretty(length({qname})::bigint)'
query.add_where(f'"{model._table}".id IN %s', [tuple(ids)])
query_str, params = query.select(f'"{model._table}".id', qname)
env.cr.execute(query_str, params)
sql_field = SQL('pg_size_pretty(length(%s)::bigint)', sql_field)
query.add_where(SQL("%s IN %s", sql_id, tuple(ids)))
env.cr.execute(query.select(sql_id, sql_field))
# compare returned values with corresponding values in cache
for id_, value in env.cr.fetchall():