Coverage for adhoc-cicd-odoo-odoo / odoo / orm / utils.py: 77%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-09 18:05 +0000

1import re 

2import warnings 

3from collections.abc import Set as AbstractSet 

4 

5import dateutil.relativedelta 

6 

7from odoo.exceptions import AccessError, ValidationError 

8from odoo.tools import SQL 

9 

10regex_alphanumeric = re.compile(r'^[a-z0-9_]+$') 

11regex_object_name = re.compile(r'^[a-z0-9_.]+$') 

12regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.IGNORECASE) 

13# match private methods, to prevent their remote invocation 

14regex_private = re.compile(r'^(_.*|init)$') 

15 

16# types handled as collections 

17COLLECTION_TYPES = (list, tuple, AbstractSet) 

18# The hard-coded super-user id (a.k.a. root user, or OdooBot). 

19SUPERUSER_ID = 1 

20 

21# _read_group stuff 

22READ_GROUP_TIME_GRANULARITY = { 

23 'hour': dateutil.relativedelta.relativedelta(hours=1), 

24 'day': dateutil.relativedelta.relativedelta(days=1), 

25 'week': dateutil.relativedelta.relativedelta(days=7), 

26 'month': dateutil.relativedelta.relativedelta(months=1), 

27 'quarter': dateutil.relativedelta.relativedelta(months=3), 

28 'year': dateutil.relativedelta.relativedelta(years=1) 

29} 

30 

31READ_GROUP_NUMBER_GRANULARITY = { 

32 'year_number': 'year', 

33 'quarter_number': 'quarter', 

34 'month_number': 'month', 

35 'iso_week_number': 'week', # ISO week number because anything else than ISO is nonsense 

36 'day_of_year': 'doy', 

37 'day_of_month': 'day', 

38 'day_of_week': 'dow', 

39 'hour_number': 'hour', 

40 'minute_number': 'minute', 

41 'second_number': 'second', 

42} 

43 

44READ_GROUP_ALL_TIME_GRANULARITY = READ_GROUP_TIME_GRANULARITY | READ_GROUP_NUMBER_GRANULARITY 

45 

46 

47# SQL operators with spaces around them 

48# hardcoded to avoid changing SQL injection linting 

49SQL_OPERATORS = { 

50 "=": SQL(" = "), 

51 "!=": SQL(" != "), 

52 "in": SQL(" IN "), 

53 "not in": SQL(" NOT IN "), 

54 "<": SQL(" < "), 

55 ">": SQL(" > "), 

56 "<=": SQL(" <= "), 

57 ">=": SQL(" >= "), 

58 "like": SQL(" LIKE "), 

59 "ilike": SQL(" ILIKE "), 

60 "=like": SQL(" LIKE "), 

61 "=ilike": SQL(" ILIKE "), 

62 "not like": SQL(" NOT LIKE "), 

63 "not ilike": SQL(" NOT ILIKE "), 

64 "not =like": SQL(" NOT LIKE "), 

65 "not =ilike": SQL(" NOT ILIKE "), 

66} 

67 

68 

69def check_method_name(name): 

70 """ Raise an ``AccessError`` if ``name`` is a private method name. """ 

71 warnings.warn("Since 19.0, use odoo.service.model.get_public_method", DeprecationWarning) 

72 if regex_private.match(name): 

73 raise AccessError('Private methods (such as %s) cannot be called remotely.' % name) 

74 

75 

76def check_object_name(name): 

77 """ Check if the given name is a valid model name. 

78 

79 The _name attribute in osv and osv_memory object is subject to 

80 some restrictions. This function returns True or False whether 

81 the given name is allowed or not. 

82 

83 TODO: this is an approximation. The goal in this approximation 

84 is to disallow uppercase characters (in some places, we quote 

85 table/column names and in other not, which leads to this kind 

86 of errors: 

87 

88 psycopg2.ProgrammingError: relation "xxx" does not exist). 

89 

90 The same restriction should apply to both osv and osv_memory 

91 objects for consistency. 

92 

93 """ 

94 return regex_object_name.match(name) is not None 

95 

96 

97def check_pg_name(name): 

98 """ Check whether the given name is a valid PostgreSQL identifier name. """ 

99 if not regex_pg_name.match(name): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 raise ValidationError("Invalid characters in table name %r" % name) 

101 if len(name) > 63: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 raise ValidationError("Table name %r is too long" % name) 

103 

104 

105def parse_field_expr(field_expr: str) -> tuple[str, str | None]: 

106 if (property_index := field_expr.find(".")) >= 0: 

107 property_name = field_expr[property_index + 1:] 

108 field_expr = field_expr[:property_index] 

109 else: 

110 property_name = None 

111 if not field_expr: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 raise ValueError(f"Invalid field expression {field_expr!r}") 

113 return field_expr, property_name 

114 

115 

116def expand_ids(id0, ids): 

117 """ Return an iterator of unique ids from the concatenation of ``[id0]`` and 

118 ``ids``, and of the same kind (all real or all new). 

119 """ 

120 yield id0 

121 seen = {id0} 

122 kind = bool(id0) 

123 for id_ in ids: 

124 if id_ not in seen and bool(id_) == kind: 

125 yield id_ 

126 seen.add(id_) 

127 

128 

129class OriginIds: 

130 """ A reversible iterable returning the origin ids of a collection of ``ids``. 

131 Actual ids are returned as is, and ids without origin are not returned. 

132 """ 

133 __slots__ = ['ids'] 

134 

135 def __init__(self, ids): 

136 self.ids = ids 

137 

138 def __iter__(self): 

139 for id_ in self.ids: 

140 if id_ := id_ or getattr(id_, 'origin', None): 

141 yield id_ 

142 

143 def __reversed__(self): 

144 for id_ in reversed(self.ids): 

145 if id_ := id_ or getattr(id_, 'origin', None): 

146 yield id_ 

147 

148 

149origin_ids = OriginIds