Coverage for adhoc-cicd-odoo-odoo / odoo / orm / fields_misc.py: 87%

79 statements  

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

1from __future__ import annotations 

2 

3import copy 

4import json 

5import typing 

6 

7from psycopg2.extras import Json as PsycopgJson 

8 

9from odoo.tools import SQL, json_default 

10 

11from .fields import Field 

12from .identifiers import IdType 

13 

14if typing.TYPE_CHECKING: 

15 from .models import BaseModel 

16 from odoo.tools import Query 

17 

18# integer needs to be imported before Id because of `type` attribute clash 

19from . import fields_numeric # noqa: F401 

20 

21 

22class Boolean(Field[bool]): 

23 """ Encapsulates a :class:`bool`. """ 

24 type = 'boolean' 

25 _column_type = ('bool', 'bool') 

26 falsy_value = False 

27 

28 def convert_to_column(self, value, record, values=None, validate=True): 

29 return bool(value) 

30 

31 def convert_to_cache(self, value, record, validate=True): 

32 return bool(value) 

33 

34 def convert_to_export(self, value, record): 

35 return bool(value) 

36 

37 def _condition_to_sql(self, field_expr: str, operator: str, value, model: BaseModel, alias: str, query: Query) -> SQL: 

38 if operator not in ('in', 'not in'): 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true

39 return super()._condition_to_sql(field_expr, operator, value, model, alias, query) 

40 

41 # get field and check access 

42 sql_field = model._field_to_sql(alias, field_expr, query) 

43 

44 # express all conditions as (field_expr, 'in', possible_values) 

45 possible_values = ( 

46 {bool(v) for v in value} if operator == 'in' else 

47 {True, False} - {bool(v) for v in value} # operator == 'not in' 

48 ) 

49 if len(possible_values) != 1: 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true

50 return SQL("TRUE") if possible_values else SQL("FALSE") 

51 is_true = True in possible_values 

52 return SQL("%s IS TRUE", sql_field) if is_true else SQL("%s IS NOT TRUE", sql_field) 

53 

54 

55class Json(Field): 

56 """ JSON Field that contain unstructured information in jsonb PostgreSQL column. 

57 

58 Some features won't be implemented, including: 

59 * searching 

60 * indexing 

61 * mutating the values. 

62 """ 

63 

64 type = 'json' 

65 _column_type = ('jsonb', 'jsonb') 

66 

67 def convert_to_record(self, value, record): 

68 """ Return a copy of the value """ 

69 return False if value is None else copy.deepcopy(value) 

70 

71 def convert_to_cache(self, value, record, validate=True): 

72 if not value: 

73 return None 

74 return json.loads(json.dumps(value, ensure_ascii=False, default=json_default)) 

75 

76 def convert_to_column(self, value, record, values=None, validate=True): 

77 if validate: 

78 value = self.convert_to_cache(value, record) 

79 if value is None: 

80 return None 

81 return PsycopgJson(value) 

82 

83 def convert_to_export(self, value, record): 

84 if not value: 

85 return '' 

86 return json.dumps(value) 

87 

88 

89class Id(Field[IdType | typing.Literal[False]]): 

90 """ Special case for field 'id'. """ 

91 # Note: This field type is not necessarily an integer! 

92 type = 'integer' # note this conflicts with Integer 

93 column_type = ('int4', 'int4') 

94 

95 string = 'ID' 

96 store = True 

97 readonly = True 

98 prefetch = False 

99 

100 def update_db(self, model, columns): 

101 pass # this column is created with the table 

102 

103 def __get__(self, record, owner=None): 

104 if record is None: 

105 return self # the field is accessed through the class owner 

106 

107 # the code below is written to make record.id as quick as possible 

108 ids = record._ids 

109 size = len(ids) 

110 if size == 0: 

111 return False 

112 elif size == 1: 112 ↛ 114line 112 didn't jump to line 114 because the condition on line 112 was always true

113 return ids[0] 

114 raise ValueError("Expected singleton: %s" % record) 

115 

116 def __set__(self, record, value): 

117 raise TypeError("field 'id' cannot be assigned") 

118 

119 def convert_to_column(self, value, record, values=None, validate=True): 

120 return value 

121 

122 def to_sql(self, model: BaseModel, alias: str) -> SQL: 

123 # do not flush, just return the identifier 

124 assert self.store, 'id field must be stored' 

125 # id is never flushed 

126 return SQL.identifier(alias, self.name) 

127 

128 def expression_getter(self, field_expr): 

129 if field_expr != 'id.origin': 

130 return super().expression_getter(field_expr) 

131 

132 def getter(record): 

133 return (id_ := record._ids[0]) or getattr(id_, 'origin', None) or False 

134 

135 return getter