19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -82,6 +82,12 @@ def initialize(cr: Cursor) -> None:
(module_id, d, d in (info['auto_install'] or ()))
)
from odoo.tools import config # noqa: PLC0415
if config.get('skip_auto_install'):
# even if skip_auto_install is enabled we still want to have base
cr.execute("""UPDATE ir_module_module SET state='to install' WHERE name = 'base'""")
return
# Install recursively all auto-installing modules
while True:
# this selects all the auto_install modules whose auto_install_required
@ -168,10 +174,9 @@ def has_unaccent(cr: BaseCursor) -> FunctionStatus:
cr.execute("""
SELECT p.provolatile
FROM pg_proc p
LEFT JOIN pg_catalog.pg_namespace ns ON p.pronamespace = ns.oid
WHERE p.proname = 'unaccent'
AND p.pronamespace = current_schema::regnamespace
AND p.pronargs = 1
AND ns.nspname = 'public'
""")
result = cr.fetchone()
if not result:

View file

@ -17,6 +17,7 @@ import odoo.sql_db
import odoo.tools.sql
import odoo.tools.translate
from odoo import api, tools
from odoo.tools import OrderedSet
from odoo.tools.convert import convert_file, IdRef, ConvertMode as LoadMode
from . import db as modules_db
@ -69,7 +70,7 @@ def load_demo(env: Environment, package: ModuleNode, idref: IdRef, mode: LoadMod
if package.manifest.get('demo') or package.manifest.get('demo_xml'):
_logger.info("Module %s: loading demo", package.name)
with env.cr.savepoint(flush=False):
load_data(env(su=True), idref, mode, kind='demo', package=package)
load_data(env(su=True, context=dict(env.context, install_demo=True)), idref, mode, kind='demo', package=package)
return True
except Exception: # noqa: BLE001
# If we could not install demo data for this module
@ -89,6 +90,7 @@ def force_demo(env: Environment) -> None:
"""
Forces the `demo` flag on all modules, and installs demo data for all installed modules.
"""
assert env.registry.ready
env.cr.execute('UPDATE ir_module_module SET demo=True')
env.cr.execute(
"SELECT name FROM ir_module_module WHERE state IN ('installed', 'to upgrade', 'to remove')"
@ -102,13 +104,20 @@ def force_demo(env: Environment) -> None:
env['ir.module.module'].invalidate_model(['demo'])
# If demo data triggered module state changes (to install/upgrade/remove),
# commit and rebuild registry to process button_install/upgrade calls.
if env['ir.module.module'].search_count([('state', 'in', ('to install', 'to upgrade', 'to remove'))], limit=1):
env.cr.commit()
Registry.new(env.cr.dbname, update_module=True)
env.transaction.reset()
def load_module_graph(
env: Environment,
graph: ModuleGraph,
update_module: bool = False,
report: OdooTestResult | None = None,
models_to_check: set[str] | None = None,
models_to_check: OrderedSet[str] | None = None,
install_demo: bool = True,
) -> None:
""" Load, upgrade and install not loaded module nodes in the ``graph`` for ``env.registry``
@ -121,7 +130,7 @@ def load_module_graph(
:param install_demo: whether to attempt installing demo data for newly installed modules
"""
if models_to_check is None:
models_to_check = set()
models_to_check = OrderedSet()
registry = env.registry
assert isinstance(env.cr, odoo.sql_db.Cursor), "Need for a real Cursor to load modules"
@ -179,8 +188,8 @@ def load_module_graph(
if update_operation:
model_names = registry.descendants(model_names, '_inherit', '_inherits')
models_updated |= set(model_names)
models_to_check -= set(model_names)
models_updated |= model_names
models_to_check -= model_names
registry._setup_models__(env.cr, []) # incremental setup
registry.init_models(env.cr, model_names, {'module': package.name}, update_operation == 'install')
elif update_module and package.state != 'to remove':
@ -190,7 +199,11 @@ def load_module_graph(
# e.g. adding required=True to an existing field, but the schema has not been
# updated by this module because it's not marked as 'to upgrade/to install'.
model_names = registry.descendants(model_names, '_inherit', '_inherits')
models_to_check |= set(model_names) & models_updated
models_to_check |= model_names & models_updated
elif update_module and package.state == 'to remove':
# For all model extented (with _inherit) in the package to uninstall, we need to
# update ir.model / ir.model.fields along side not-null SQL constrains.
models_to_check |= model_names
if update_operation:
# Can't put this line out of the loop: ir.module.module will be
@ -332,6 +345,7 @@ def load_modules(
install_modules: Collection[str] = (),
reinit_modules: Collection[str] = (),
new_db_demo: bool = False,
models_to_check: OrderedSet[str] | None = None,
) -> None:
""" Load the modules for a registry object that has just been created. This
function is part of Registry.new() and should not be used anywhere else.
@ -343,9 +357,10 @@ def load_modules(
:param reinit_modules: A collection of module names to reinitialize.
:param new_db_demo: Whether to install demo data for new database. Defaults to ``False``
"""
initialize_sys_path()
if models_to_check is None:
models_to_check = OrderedSet()
models_to_check: set[str] = set()
initialize_sys_path()
with registry.cursor() as cr:
# prevent endless wait for locks on schema changes (during online
@ -536,11 +551,8 @@ def load_modules(
cr.commit()
_logger.info('Reloading registry once more after uninstalling modules')
registry = Registry.new(
cr.dbname, update_module=update_module
cr.dbname, update_module=update_module, models_to_check=models_to_check,
)
cr.reset()
registry.check_tables_exist(cr)
cr.commit()
return
# STEP 5.5: Verify extended fields on every model
@ -555,7 +567,9 @@ def load_modules(
cr.execute("""SELECT DISTINCT model FROM ir_model_fields WHERE state = 'manual'""")
models_to_check.update(model_name for model_name, in cr.fetchall() if model_name in registry)
if models_to_check:
registry.init_models(cr, list(models_to_check), {'models_to_check': True, 'update_custom_fields': True})
# Doesn't check models that didn't exist anymore, it might happen during uninstallation
models_to_check = [model for model in models_to_check if model in registry]
registry.init_models(cr, models_to_check, {'models_to_check': True, 'update_custom_fields': True})
# STEP 6: verify custom views on every model
if update_module:
@ -583,6 +597,16 @@ def load_modules(
# STEP 10: check that we can trust nullable columns
registry.check_null_constraints(cr)
if update_module:
cr.execute(
"""
INSERT INTO ir_config_parameter(key, value)
SELECT 'base.partially_updated_database', '1'
WHERE EXISTS(SELECT FROM ir_module_module WHERE state IN ('to upgrade', 'to install', 'to remove'))
ON CONFLICT DO NOTHING
"""
)
def reset_modules_state(db_name: str) -> None:
"""
@ -596,8 +620,7 @@ def reset_modules_state(db_name: str) -> None:
# of time
db = odoo.sql_db.db_connect(db_name)
with db.cursor() as cr:
cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='ir_module_module'")
if not cr.fetchall():
if not odoo.tools.sql.table_exists(cr, 'ir_module_module'):
_logger.info('skipping reset_modules_state, ir_module_module table does not exists')
return
cr.execute(

View file

@ -165,7 +165,7 @@ class MigrationManager:
def _get_migration_versions(pkg, stage: str) -> list[str]:
versions = sorted({
ver
ver: None
for lv in self.migrations[pkg.name].values()
for ver, lf in lv.items()
if lf

View file

@ -182,7 +182,7 @@ class Manifest(Mapping[str, typing.Any]):
_, self.name = os.path.split(path)
if not MODULE_NAME_RE.match(self.name):
raise FileNotFoundError(f"Invalid module name: {self.name}")
self._manifest_content = manifest_content
self.__manifest_content = manifest_content
@property
def addons_path(self) -> str:
@ -191,14 +191,14 @@ class Manifest(Mapping[str, typing.Any]):
return parent_path
@functools.cached_property
def manifest_cached(self) -> dict:
def __manifest_cached(self) -> dict:
"""Parsed and validated manifest data from the file."""
return _load_manifest(self.name, self._manifest_content)
return _load_manifest(self.name, self.__manifest_content)
@functools.cached_property
def description(self):
"""The description of the module defaulting to the README file."""
if (desc := self.manifest_cached.get('description')):
if (desc := self.__manifest_cached.get('description')):
return desc
for file_name in README:
try:
@ -211,7 +211,7 @@ class Manifest(Mapping[str, typing.Any]):
@functools.cached_property
def version(self):
try:
return self.manifest_cached['version']
return self.__manifest_cached['version']
except Exception: # noqa: BLE001
return adapt_version('1.0')
@ -222,7 +222,7 @@ class Manifest(Mapping[str, typing.Any]):
@functools.cached_property
def static_path(self) -> str | None:
static_path = opj(self.path, 'static')
manifest = self.manifest_cached
manifest = self.__manifest_cached
if (manifest['installable'] or manifest['assets']) and os.path.isdir(static_path):
return static_path
return None
@ -230,10 +230,13 @@ class Manifest(Mapping[str, typing.Any]):
def __getitem__(self, key: str):
if key in ('description', 'icon', 'addons_path', 'version', 'static_path'):
return getattr(self, key)
return copy.deepcopy(self.manifest_cached[key])
return copy.deepcopy(self.__manifest_cached[key])
def raw_value(self, key):
return copy.deepcopy(self.__manifest_cached.get(key))
def __iter__(self):
manifest = self.manifest_cached
manifest = self.__manifest_cached
yield from manifest
for key in ('description', 'icon', 'addons_path', 'version', 'static_path'):
if key not in manifest:
@ -377,9 +380,15 @@ def get_module_icon(module: str) -> str:
""" Get the path to the module's icon. Invalid module names are accepted. """
manifest = Manifest.for_addon(module, display_warning=False)
if manifest and 'icon' in manifest.__dict__:
# we have a value in the cached property
return manifest.icon
try:
fpath = ''
if manifest:
fpath = manifest.raw_value('icon') or ''
fpath = fpath.lstrip('/')
if not fpath:
fpath = f"{module}/static/description/icon.png"
try:
tools.file_path(fpath)
return "/" + fpath
except FileNotFoundError:

View file

@ -145,7 +145,7 @@ class ModuleNode:
# acceptable in this context since we don't modify it
manifest = Manifest.for_addon(name, display_warning=False)
if manifest is not None:
manifest.manifest_cached # parse the manifest now
manifest.raw_value('') # parse the manifest now
self.manifest: Mapping = manifest or {}
# ir_module_module data # column_name