18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -5,9 +5,6 @@ from . import db
from . import model
from . import server
# deprecated since 15.3
from . import wsgi_server
#.apidoc title: RPC Services
""" Classes of this module implement the network protocols that the

View file

@ -5,6 +5,7 @@ import logging
import odoo.release
import odoo.tools
from odoo.exceptions import AccessDenied
from odoo.modules.registry import Registry
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
@ -22,9 +23,10 @@ def exp_login(db, login, password):
def exp_authenticate(db, login, password, user_agent_env):
if not user_agent_env:
user_agent_env = {}
res_users = odoo.registry(db)['res.users']
res_users = Registry(db)['res.users']
try:
return res_users.authenticate(db, login, password, {**user_agent_env, 'interactive': False})
credential = {'login': login, 'password': password, 'type': 'password'}
return res_users.authenticate(db, credential, {**user_agent_env, 'interactive': False})['uid']
except AccessDenied:
return False

View file

@ -6,36 +6,44 @@ import os
import shutil
import subprocess
import tempfile
import threading
import traceback
from xml.etree import ElementTree as ET
import zipfile
from datetime import datetime
from psycopg2 import sql
from pytz import country_timezones
from functools import wraps
from contextlib import closing
from decorator import decorator
from datetime import datetime
from xml.etree import ElementTree as ET
import psycopg2
from psycopg2.extensions import quote_ident
from decorator import decorator
from pytz import country_timezones
import odoo
from odoo import SUPERUSER_ID
from odoo.exceptions import AccessDenied
import odoo.release
import odoo.sql_db
import odoo.tools
from odoo.sql_db import db_connect
from odoo import SUPERUSER_ID
from odoo.exceptions import AccessDenied
from odoo.release import version_info
from odoo.tools import find_pg_tool, exec_pg_environ
from odoo.sql_db import db_connect
from odoo.tools import SQL
from odoo.tools.misc import exec_pg_environ, find_pg_tool
_logger = logging.getLogger(__name__)
class DatabaseExists(Warning):
pass
def database_identifier(cr, name: str) -> SQL:
"""Quote a database identifier.
Use instead of `SQL.identifier` to accept all kinds of identifiers.
"""
name = quote_ident(name, cr._cnx)
return SQL(name)
def check_db_management_enabled(method):
def if_db_mgt_enabled(method, self, *args, **kwargs):
if not odoo.tools.config['list_db']:
@ -92,7 +100,6 @@ def _initialize_db(id, db_name, demo, lang, user_password, login='admin', countr
values['email'] = emails[0]
env.ref('base.user_admin').write(values)
cr.execute('SELECT login, password FROM res_users ORDER BY login')
cr.commit()
except Exception as e:
_logger.exception('CREATE DATABASE failed:')
@ -136,10 +143,11 @@ def _create_empty_database(name):
cr._cnx.autocommit = True
# 'C' collate is only safe with template0, but provides more useful indexes
collate = sql.SQL("LC_COLLATE 'C'" if chosen_template == 'template0' else "")
cr.execute(
sql.SQL("CREATE DATABASE {} ENCODING 'unicode' {} TEMPLATE {}").format(
sql.Identifier(name), collate, sql.Identifier(chosen_template)
cr.execute(SQL(
"CREATE DATABASE %s ENCODING 'unicode' %s TEMPLATE %s",
database_identifier(cr, name),
SQL("LC_COLLATE 'C'") if chosen_template == 'template0' else SQL(""),
database_identifier(cr, chosen_template),
))
# TODO: add --extension=trigram,unaccent
@ -185,9 +193,10 @@ def exp_duplicate_database(db_original_name, db_name, neutralize_database=False)
# database-altering operations cannot be executed inside a transaction
cr._cnx.autocommit = True
_drop_conn(cr, db_original_name)
cr.execute(sql.SQL("CREATE DATABASE {} ENCODING 'unicode' TEMPLATE {}").format(
sql.Identifier(db_name),
sql.Identifier(db_original_name)
cr.execute(SQL(
"CREATE DATABASE %s ENCODING 'unicode' TEMPLATE %s",
database_identifier(cr, db_name),
database_identifier(cr, db_original_name),
))
registry = odoo.modules.registry.Registry.new(db_name)
@ -234,7 +243,7 @@ def exp_drop(db_name):
_drop_conn(cr, db_name)
try:
cr.execute(sql.SQL('DROP DATABASE {}').format(sql.Identifier(db_name)))
cr.execute(SQL('DROP DATABASE %s', database_identifier(cr, db_name)))
except Exception as e:
_logger.info('DROP DB: %s failed:\n%s', db_name, e)
raise Exception("Couldn't drop database %s: %s" % (db_name, e))
@ -385,7 +394,7 @@ def exp_rename(old_name, new_name):
cr._cnx.autocommit = True
_drop_conn(cr, old_name)
try:
cr.execute(sql.SQL('ALTER DATABASE {} RENAME TO {}').format(sql.Identifier(old_name), sql.Identifier(new_name)))
cr.execute(SQL('ALTER DATABASE %s RENAME TO %s', database_identifier(cr, old_name), database_identifier(cr, new_name)))
_logger.info('RENAME DB: %s -> %s', old_name, new_name)
except Exception as e:
_logger.info('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e)
@ -437,16 +446,15 @@ def list_dbs(force=False):
return res
chosen_template = odoo.tools.config['db_template']
templates_list = tuple(set(['postgres', chosen_template]))
templates_list = tuple({'postgres', chosen_template})
db = odoo.sql_db.db_connect('postgres')
with closing(db.cursor()) as cr:
try:
cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=current_user) and not datistemplate and datallowconn and datname not in %s order by datname", (templates_list,))
res = [odoo.tools.ustr(name) for (name,) in cr.fetchall()]
return [name for (name,) in cr.fetchall()]
except Exception:
_logger.exception('Listing databases failed:')
res = []
return res
return []
def list_db_incompatible(databases):
""""Check a list of databases if they are compatible with this version of Odoo
@ -458,7 +466,7 @@ def list_db_incompatible(databases):
server_version = '.'.join(str(v) for v in version_info[:2])
for database_name in databases:
with closing(db_connect(database_name).cursor()) as cr:
if odoo.tools.table_exists(cr, 'ir_module_module'):
if odoo.tools.sql.table_exists(cr, 'ir_module_module'):
cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ('base',))
base_version = cr.fetchone()
if not base_version or not base_version[0]:
@ -482,7 +490,7 @@ def exp_list(document=False):
return list_dbs()
def exp_list_lang():
return odoo.tools.scan_languages()
return odoo.tools.misc.scan_languages()
def exp_list_countries():
list_countries = []

View file

@ -6,20 +6,22 @@ import time
from collections.abc import Mapping, Sequence
from functools import partial
from psycopg2 import IntegrityError, OperationalError, errorcodes
from psycopg2 import IntegrityError, OperationalError, errorcodes, errors
import odoo
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.models import BaseModel
from odoo.http import request
from odoo.tools import DotDict
from odoo.tools.translate import _, translate_sql_constraint
from odoo.modules.registry import Registry
from odoo.tools import DotDict, lazy
from odoo.tools.translate import translate_sql_constraint
from . import security
from ..tools import lazy
_logger = logging.getLogger(__name__)
PG_CONCURRENCY_ERRORS_TO_RETRY = (errorcodes.LOCK_NOT_AVAILABLE, errorcodes.SERIALIZATION_FAILURE, errorcodes.DEADLOCK_DETECTED)
PG_CONCURRENCY_EXCEPTIONS_TO_RETRY = (errors.LockNotAvailable, errors.SerializationFailure, errors.DeadlockDetected)
MAX_TRIES_ON_CONCURRENCY_FAILURE = 5
@ -48,7 +50,7 @@ def dispatch(method, params):
threading.current_thread().dbname = db
threading.current_thread().uid = uid
registry = odoo.registry(db).check_signaling()
registry = Registry(db).check_signaling()
with registry.manage_changes():
if method == 'execute':
res = execute(db, uid, *params[3:])
@ -65,7 +67,7 @@ def execute_cr(cr, uid, obj, method, *args, **kw):
env = odoo.api.Environment(cr, uid, {})
recs = env.get(obj)
if recs is None:
raise UserError(_("Object %s doesn't exist", obj))
raise UserError(env._("Object %s doesn't exist", obj))
get_public_method(recs, method) # Don't use the result, call_kw will redo the getattr
result = retrying(partial(odoo.api.call_kw, recs, method, args, kw), env)
# force evaluation of lazy values before the cursor is closed, as it would
@ -80,7 +82,8 @@ def execute_kw(db, uid, obj, method, args, kw=None):
def execute(db, uid, obj, method, *args, **kw):
with odoo.registry(db).cursor() as cr:
# TODO could be conditionnaly readonly as in _call_kw_readonly
with Registry(db).cursor() as cr:
res = execute_cr(cr, uid, obj, method, *args, **kw)
if res is None:
_logger.info('The method %s of the object %s can not return `None`!', method, obj)
@ -90,47 +93,48 @@ def execute(db, uid, obj, method, *args, **kw):
def _as_validation_error(env, exc):
""" Return the IntegrityError encapsuled in a nice ValidationError """
unknown = _('Unknown')
model = DotDict({'_name': unknown.lower(), '_description': unknown})
field = DotDict({'name': unknown.lower(), 'string': unknown})
unknown = env._('Unknown')
model = DotDict({'_name': 'unknown', '_description': unknown})
field = DotDict({'name': 'unknown', 'string': unknown})
for _name, rclass in env.registry.items():
if exc.diag.table_name == rclass._table:
model = rclass
field = model._fields.get(exc.diag.column_name) or field
break
if exc.pgcode == errorcodes.NOT_NULL_VIOLATION:
return ValidationError(_(
"The operation cannot be completed:\n"
"- Create/update: a mandatory field is not set.\n"
"- Delete: another model requires the record being deleted."
" If possible, archive it instead.\n\n"
"Model: %(model_name)s (%(model_tech_name)s)\n"
"Field: %(field_name)s (%(field_tech_name)s)\n",
model_name=model._description,
model_tech_name=model._name,
field_name=field.string,
field_tech_name=field.name,
))
match exc:
case errors.NotNullViolation():
return ValidationError(env._(
"The operation cannot be completed:\n"
"- Create/update: a mandatory field is not set.\n"
"- Delete: another model requires the record being deleted."
" If possible, archive it instead.\n\n"
"Model: %(model_name)s (%(model_tech_name)s)\n"
"Field: %(field_name)s (%(field_tech_name)s)\n",
model_name=model._description,
model_tech_name=model._name,
field_name=field.string,
field_tech_name=field.name,
))
if exc.pgcode == errorcodes.FOREIGN_KEY_VIOLATION:
return ValidationError(_(
"The operation cannot be completed: another model requires "
"the record being deleted. If possible, archive it instead.\n\n"
"Model: %(model_name)s (%(model_tech_name)s)\n"
"Constraint: %(constraint)s\n",
model_name=model._description,
model_tech_name=model._name,
constraint=exc.diag.constraint_name,
))
case errors.ForeignKeyViolation():
return ValidationError(env._(
"The operation cannot be completed: another model requires "
"the record being deleted. If possible, archive it instead.\n\n"
"Model: %(model_name)s (%(model_tech_name)s)\n"
"Constraint: %(constraint)s\n",
model_name=model._description,
model_tech_name=model._name,
constraint=exc.diag.constraint_name,
))
if exc.diag.constraint_name in env.registry._sql_constraints:
return ValidationError(_(
return ValidationError(env._(
"The operation cannot be completed: %s",
translate_sql_constraint(env.cr, exc.diag.constraint_name, env.context.get('lang', 'en_US'))
))
return ValidationError(_("The operation cannot be completed: %s", exc.args[0]))
return ValidationError(env._("The operation cannot be completed: %s", exc.args[0]))
def retrying(func, env):
@ -169,7 +173,7 @@ def retrying(func, env):
raise RuntimeError(f"Cannot retry request on input file {filename!r} after serialization failure") from exc
if isinstance(exc, IntegrityError):
raise _as_validation_error(env, exc) from exc
if exc.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
if not isinstance(exc, PG_CONCURRENCY_EXCEPTIONS_TO_RETRY):
raise
if not tryleft:
_logger.info("%s, maximum number of tries reached!", errorcodes.lookup(exc.pgcode))

View file

@ -3,18 +3,23 @@
import odoo
import odoo.exceptions
from odoo.modules.registry import Registry
def check(db, uid, passwd):
res_users = odoo.registry(db)['res.users']
res_users = Registry(db)['res.users']
return res_users.check(db, uid, passwd)
def compute_session_token(session, env):
self = env['res.users'].browse(session.uid)
return self._compute_session_token(session.sid)
def check_session(session, env):
def check_session(session, env, request=None):
self = env['res.users'].browse(session.uid)
expected = self._compute_session_token(session.sid)
if expected and odoo.tools.misc.consteq(expected, session.session_token):
if request:
env['res.device.log']._update_device(request)
return True
return False

View file

@ -15,17 +15,12 @@ import subprocess
import sys
import threading
import time
import unittest
import contextlib
from email.utils import parsedate_to_datetime
from io import BytesIO
from itertools import chain
import psutil
import werkzeug.serving
from werkzeug.debug import DebuggedApplication
from ..tests import loader
if os.name == 'posix':
# Unix only for workers
@ -62,7 +57,8 @@ from odoo.modules import get_modules
from odoo.modules.registry import Registry
from odoo.release import nt_service_name
from odoo.tools import config
from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
from odoo.tools.cache import log_ormcache_stats
from odoo.tools.misc import stripped_sys_argv, dumpstacks
_logger = logging.getLogger(__name__)
@ -81,10 +77,15 @@ def memory_info(process):
def set_limit_memory_hard():
if platform.system() == 'Linux' and config['limit_memory_hard']:
if platform.system() != 'Linux':
return
limit_memory_hard = config['limit_memory_hard']
if odoo.evented and config['limit_memory_hard_gevent']:
limit_memory_hard = config['limit_memory_hard_gevent']
if limit_memory_hard:
rlimit = resource.RLIMIT_AS
soft, hard = resource.getrlimit(rlimit)
resource.setrlimit(rlimit, (config['limit_memory_hard'], hard))
resource.setrlimit(rlimit, (limit_memory_hard, hard))
def empty_pipe(fd):
try:
@ -255,21 +256,7 @@ class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.
t.start_time = time.time()
t.start()
# TODO: Remove this method as soon as either of the revision
# - python/cpython@8b1f52b5a93403acd7d112cd1c1bc716b31a418a for Python 3.6,
# - python/cpython@908082451382b8b3ba09ebba638db660edbf5d8e for Python 3.7,
# is included in all Python 3 releases installed on all operating systems supported by Odoo.
# These revisions are included in Python from releases 3.6.8 and Python 3.7.2 respectively.
def _handle_request_noblock(self):
"""
In the python module `socketserver` `process_request` loop,
the __shutdown_request flag is not checked between select and accept.
Thus when we set it to `True` thanks to the call `httpd.shutdown`,
a last request is accepted before exiting the loop.
We override this function to add an additional check before the accept().
"""
if self._BaseServer__shutdown_request:
return
if self.max_http_threads and not self.http_threads_sem.acquire(timeout=0.1):
# If the semaphore is full we will return immediately to the upstream (most probably
# socketserver.BaseServer's serve_forever loop which will retry immediately as the
@ -299,7 +286,7 @@ class FSWatcherBase(object):
except SyntaxError:
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
else:
if not getattr(odoo, 'phoenix', False):
if not server_phoenix:
_logger.info('autoreload: python code updated, autoreload activated')
restart()
return True
@ -450,7 +437,8 @@ class ThreadedServer(CommonServer):
os._exit(0)
elif sig == signal.SIGHUP:
# restart on kill -HUP
odoo.phoenix = True
global server_phoenix # noqa: PLW0603
server_phoenix = True
self.quit_signals_received += 1
# interrupt run() to start shutdown
raise KeyboardInterrupt()
@ -557,14 +545,13 @@ class ThreadedServer(CommonServer):
t.start()
_logger.debug("cron%d started!" % i)
def http_thread(self):
self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, self.app)
self.httpd.serve_forever()
def http_spawn(self):
t = threading.Thread(target=self.http_thread, name="odoo.service.httpd")
t.daemon = True
t.start()
self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, self.app)
threading.Thread(
target=self.httpd.serve_forever,
name="odoo.service.httpd",
daemon=True,
).start()
def start(self, stop=False):
_logger.debug("Setting signal handlers")
@ -589,7 +576,7 @@ class ThreadedServer(CommonServer):
def stop(self):
""" Shutdown the WSGI server. Wait for non daemon threads.
"""
if getattr(odoo, 'phoenix', None):
if server_phoenix:
_logger.info("Initiating server reload")
else:
_logger.info("Initiating shutdown")
@ -630,13 +617,13 @@ class ThreadedServer(CommonServer):
The first SIGINT or SIGTERM signal will initiate a graceful shutdown while
a second one if any will force an immediate exit.
"""
self.start(stop=stop)
rc = preload_registries(preload)
with Registry._lock:
self.start(stop=stop)
rc = preload_registries(preload)
if stop:
if config['test_enable']:
logger = odoo.tests.result._logger
from odoo.tests.result import _logger as logger # noqa: PLC0415
with Registry.registries._lock:
for db, registry in Registry.registries.d.items():
report = registry._assertion_report
@ -671,7 +658,7 @@ class ThreadedServer(CommonServer):
# `reload` increments `self.quit_signals_received`
# and the loop will end after this iteration,
# therefore leading to the server stop.
# `reload` also sets the `phoenix` flag
# `reload` also sets the `server_phoenix` flag
# to tell the server to restart the server after shutting down.
else:
time.sleep(1)
@ -697,7 +684,8 @@ class GeventServer(CommonServer):
_logger.warning("Gevent Parent changed: %s", self.pid)
restart = True
memory = memory_info(psutil.Process(self.pid))
if config['limit_memory_soft'] and memory > config['limit_memory_soft']:
limit_memory_soft = config['limit_memory_soft_gevent'] or config['limit_memory_soft']
if limit_memory_soft and memory > limit_memory_soft:
_logger.warning('Gevent virtual memory limit reached: %s', memory)
restart = True
if restart:
@ -891,7 +879,8 @@ class PreforkServer(CommonServer):
raise KeyboardInterrupt
elif sig == signal.SIGHUP:
# restart on kill -HUP
odoo.phoenix = True
global server_phoenix # noqa: PLW0603
server_phoenix = True
raise KeyboardInterrupt
elif sig == signal.SIGQUIT:
# dump stacks on kill -3
@ -1308,6 +1297,7 @@ class WorkerCron(Worker):
#----------------------------------------------------------
server = None
server_phoenix = False
def load_server_wide_modules():
server_wide_modules = list(odoo.conf.server_wide_modules)
@ -1336,9 +1326,11 @@ def _reexec(updated_modules=None):
# We should keep the LISTEN_* environment variabled in order to support socket activation on reexec
os.execve(sys.executable, args, os.environ)
def load_test_file_py(registry, test_file):
# pylint: disable=import-outside-toplevel
from odoo.tests.suite import OdooSuite
from odoo.tests import loader # noqa: PLC0415
from odoo.tests.suite import OdooSuite # noqa: PLC0415
threading.current_thread().testing = True
try:
test_path, _ = os.path.splitext(os.path.abspath(test_file))
@ -1346,8 +1338,7 @@ def load_test_file_py(registry, test_file):
for mod_mod in loader.get_test_modules(mod):
mod_path, _ = os.path.splitext(getattr(mod_mod, '__file__', ''))
if test_path == config._normalize(mod_path):
tests = loader.unwrap_suite(
unittest.TestLoader().loadTestsFromModule(mod_mod))
tests = loader.get_module_test_cases(mod_mod)
suite = OdooSuite(tests)
_logger.log(logging.INFO, 'running tests %s.', mod_mod.__name__)
suite(registry._assertion_report)
@ -1357,6 +1348,7 @@ def load_test_file_py(registry, test_file):
finally:
threading.current_thread().testing = False
def preload_registries(dbnames):
""" Preload a registries, possibly run a test file."""
# TODO: move all config checks to args dont check tools.config here
@ -1381,6 +1373,7 @@ def preload_registries(dbnames):
# run post-install tests
if config['test_enable']:
from odoo.tests import loader # noqa: PLC0415
t0 = time.time()
t0_sql = odoo.sql_db.sql_counter
module_names = (registry.updated_modules if update_module else
@ -1400,7 +1393,7 @@ def preload_registries(dbnames):
odoo.sql_db.sql_counter - t0_sql)
registry._assertion_report.log_stats()
if not registry._assertion_report.wasSuccessful():
if registry._assertion_report and not registry._assertion_report.wasSuccessful():
rc += 1
except Exception:
_logger.critical('Failed to initialize database `%s`.', dbname, exc_info=True)
@ -1421,11 +1414,6 @@ def start(preload=None, stop=False):
_logger.warning("Unit testing in workers mode could fail; use --workers 0.")
server = PreforkServer(odoo.http.root)
# Workaround for Python issue24291, fixed in 3.6 (see Python issue26721)
if sys.version_info[:2] == (3,5):
# turn on buffering also for wfile, to avoid partial writes (Default buffer = 8k)
werkzeug.serving.WSGIRequestHandler.wbufsize = -1
else:
if platform.system() == "Linux" and sys.maxsize > 2**32 and "MALLOC_ARENA_MAX" not in os.environ:
# glibc's malloc() uses arenas [1] in order to efficiently handle memory allocation of multi-threaded
@ -1471,7 +1459,7 @@ def start(preload=None, stop=False):
if watcher:
watcher.stop()
# like the legend of the phoenix, all ends with beginnings
if getattr(odoo, 'phoenix', False):
if server_phoenix:
_reexec()
return rc if rc else 0

View file

@ -1,11 +0,0 @@
import warnings
import odoo.http
def application(environ, start_response):
warnings.warn("The WSGI application entrypoint moved from "
"odoo.service.wsgi_server.application to odoo.http.root "
"in 15.3.",
DeprecationWarning, stacklevel=1)
return odoo.http.root(environ, start_response)