Coverage for adhoc-cicd-odoo-odoo / odoo / orm / fields_misc.py: 84%
79 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:22 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:22 +0000
1from __future__ import annotations
3import copy
4import json
5import typing
7from psycopg2.extras import Json as PsycopgJson
9from odoo.tools import SQL, json_default
11from .fields import Field
12from .identifiers import IdType
14if typing.TYPE_CHECKING:
15 from .models import BaseModel
16 from odoo.tools import Query
18# integer needs to be imported before Id because of `type` attribute clash
19from . import fields_numeric # noqa: F401
22class Boolean(Field[bool]):
23 """ Encapsulates a :class:`bool`. """
24 type = 'boolean'
25 _column_type = ('bool', 'bool')
26 falsy_value = False
28 def convert_to_column(self, value, record, values=None, validate=True):
29 return bool(value)
31 def convert_to_cache(self, value, record, validate=True):
32 return bool(value)
34 def convert_to_export(self, value, record):
35 return bool(value)
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)
41 # get field and check access
42 sql_field = model._field_to_sql(alias, field_expr, query)
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)
55class Json(Field):
56 """ JSON Field that contain unstructured information in jsonb PostgreSQL column.
58 Some features won't be implemented, including:
59 * searching
60 * indexing
61 * mutating the values.
62 """
64 type = 'json'
65 _column_type = ('jsonb', 'jsonb')
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)
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))
76 def convert_to_column(self, value, record, values=None, validate=True):
77 if validate: 77 ↛ 79line 77 didn't jump to line 79 because the condition on line 77 was always true
78 value = self.convert_to_cache(value, record)
79 if value is None: 79 ↛ 81line 79 didn't jump to line 81 because the condition on line 79 was always true
80 return None
81 return PsycopgJson(value)
83 def convert_to_export(self, value, record):
84 if not value:
85 return ''
86 return json.dumps(value)
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')
95 string = 'ID'
96 store = True
97 readonly = True
98 prefetch = False
100 def update_db(self, model, columns):
101 pass # this column is created with the table
103 def __get__(self, record, owner=None):
104 if record is None:
105 return self # the field is accessed through the class owner
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)
116 def __set__(self, record, value):
117 raise TypeError("field 'id' cannot be assigned")
119 def convert_to_column(self, value, record, values=None, validate=True):
120 return value
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)
128 def expression_getter(self, field_expr):
129 if field_expr != 'id.origin':
130 return super().expression_getter(field_expr)
132 def getter(record):
133 return (id_ := record._ids[0]) or getattr(id_, 'origin', None) or False
135 return getter