import re import warnings from collections.abc import Set as AbstractSet import dateutil.relativedelta from odoo.exceptions import AccessError, ValidationError from odoo.tools import SQL regex_alphanumeric = re.compile(r'^[a-z0-9_]+$') regex_object_name = re.compile(r'^[a-z0-9_.]+$') regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.IGNORECASE) # match private methods, to prevent their remote invocation regex_private = re.compile(r'^(_.*|init)$') # types handled as collections COLLECTION_TYPES = (list, tuple, AbstractSet) # The hard-coded super-user id (a.k.a. root user, or OdooBot). SUPERUSER_ID = 1 # _read_group stuff READ_GROUP_TIME_GRANULARITY = { 'hour': dateutil.relativedelta.relativedelta(hours=1), 'day': dateutil.relativedelta.relativedelta(days=1), 'week': dateutil.relativedelta.relativedelta(days=7), 'month': dateutil.relativedelta.relativedelta(months=1), 'quarter': dateutil.relativedelta.relativedelta(months=3), 'year': dateutil.relativedelta.relativedelta(years=1) } READ_GROUP_NUMBER_GRANULARITY = { 'year_number': 'year', 'quarter_number': 'quarter', 'month_number': 'month', 'iso_week_number': 'week', # ISO week number because anything else than ISO is nonsense 'day_of_year': 'doy', 'day_of_month': 'day', 'day_of_week': 'dow', 'hour_number': 'hour', 'minute_number': 'minute', 'second_number': 'second', } READ_GROUP_ALL_TIME_GRANULARITY = READ_GROUP_TIME_GRANULARITY | READ_GROUP_NUMBER_GRANULARITY # SQL operators with spaces around them # hardcoded to avoid changing SQL injection linting SQL_OPERATORS = { "=": SQL(" = "), "!=": SQL(" != "), "in": SQL(" IN "), "not in": SQL(" NOT IN "), "<": SQL(" < "), ">": SQL(" > "), "<=": SQL(" <= "), ">=": SQL(" >= "), "like": SQL(" LIKE "), "ilike": SQL(" ILIKE "), "=like": SQL(" LIKE "), "=ilike": SQL(" ILIKE "), "not like": SQL(" NOT LIKE "), "not ilike": SQL(" NOT ILIKE "), "not =like": SQL(" NOT LIKE "), "not =ilike": SQL(" NOT ILIKE "), } def check_method_name(name): """ Raise an ``AccessError`` if ``name`` is a private method name. """ warnings.warn("Since 19.0, use odoo.service.model.get_public_method", DeprecationWarning) if regex_private.match(name): raise AccessError('Private methods (such as %s) cannot be called remotely.' % name) def check_object_name(name): """ Check if the given name is a valid model name. The _name attribute in osv and osv_memory object is subject to some restrictions. This function returns True or False whether the given name is allowed or not. TODO: this is an approximation. The goal in this approximation is to disallow uppercase characters (in some places, we quote table/column names and in other not, which leads to this kind of errors: psycopg2.ProgrammingError: relation "xxx" does not exist). The same restriction should apply to both osv and osv_memory objects for consistency. """ return regex_object_name.match(name) is not None def check_pg_name(name): """ Check whether the given name is a valid PostgreSQL identifier name. """ if not regex_pg_name.match(name): raise ValidationError("Invalid characters in table name %r" % name) if len(name) > 63: raise ValidationError("Table name %r is too long" % name) def parse_field_expr(field_expr: str) -> tuple[str, str | None]: if (property_index := field_expr.find(".")) >= 0: property_name = field_expr[property_index + 1:] field_expr = field_expr[:property_index] else: property_name = None if not field_expr: raise ValueError(f"Invalid field expression {field_expr!r}") return field_expr, property_name def expand_ids(id0, ids): """ Return an iterator of unique ids from the concatenation of ``[id0]`` and ``ids``, and of the same kind (all real or all new). """ yield id0 seen = {id0} kind = bool(id0) for id_ in ids: if id_ not in seen and bool(id_) == kind: yield id_ seen.add(id_) class OriginIds: """ A reversible iterable returning the origin ids of a collection of ``ids``. Actual ids are returned as is, and ids without origin are not returned. """ __slots__ = ['ids'] def __init__(self, ids): self.ids = ids def __iter__(self): for id_ in self.ids: if id_ := id_ or getattr(id_, 'origin', None): yield id_ def __reversed__(self): for id_ in reversed(self.ids): if id_ := id_ or getattr(id_, 'origin', None): yield id_ origin_ids = OriginIds