xsdata-odoo 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xsdata_odoo/__init__.py +1 -0
- xsdata_odoo/codegen/resolver.py +23 -0
- xsdata_odoo/filters.py +536 -0
- xsdata_odoo/generator.py +370 -0
- xsdata_odoo/hook.py +100 -0
- xsdata_odoo/templates/class.jinja2 +27 -0
- xsdata_odoo/templates/enum.jinja2 +10 -0
- xsdata_odoo/templates/imports.jinja2 +15 -0
- xsdata_odoo/templates/module.jinja2 +13 -0
- xsdata_odoo/templates/package.jinja2 +14 -0
- xsdata_odoo/text_utils.py +182 -0
- xsdata_odoo-0.8.0.dist-info/METADATA +36 -0
- xsdata_odoo-0.8.0.dist-info/RECORD +17 -0
- xsdata_odoo-0.8.0.dist-info/WHEEL +5 -0
- xsdata_odoo-0.8.0.dist-info/entry_points.txt +2 -0
- xsdata_odoo-0.8.0.dist-info/licenses/LICENSE +21 -0
- xsdata_odoo-0.8.0.dist-info/top_level.txt +1 -0
xsdata_odoo/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from xsdata.codegen.models import Class
|
|
4
|
+
from xsdata.codegen.resolver import DependenciesResolver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OdooDependenciesResolver(DependenciesResolver):
|
|
8
|
+
def sorted_classes(self) -> List[Class]:
|
|
9
|
+
"""Sorted: Enumerations 1st"""
|
|
10
|
+
|
|
11
|
+
result = []
|
|
12
|
+
for name in sorted(
|
|
13
|
+
self.class_list,
|
|
14
|
+
key=lambda x: (
|
|
15
|
+
self.class_map.get(x) is not None
|
|
16
|
+
and not self.class_map.get(x).is_enumeration
|
|
17
|
+
),
|
|
18
|
+
):
|
|
19
|
+
obj = self.class_map.get(name)
|
|
20
|
+
if obj is not None:
|
|
21
|
+
self.apply_aliases(obj)
|
|
22
|
+
result.append(obj)
|
|
23
|
+
return result
|
xsdata_odoo/filters.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from jinja2 import Environment
|
|
7
|
+
from xsdata.codegen.models import Attr, Class
|
|
8
|
+
from xsdata.formats.dataclass.filters import Filters
|
|
9
|
+
from xsdata.logger import logger
|
|
10
|
+
from xsdata.models.config import GeneratorConfig, ObjectType
|
|
11
|
+
from xsdata.utils import namespaces
|
|
12
|
+
|
|
13
|
+
from .text_utils import extract_string_and_help, wrap_text
|
|
14
|
+
|
|
15
|
+
INTEGER_TYPES = ("integer", "positiveInteger")
|
|
16
|
+
FLOAT_TYPES = ("float", "decimal")
|
|
17
|
+
# Odoo has no Decimal field and all in all it's better to guess float by default
|
|
18
|
+
# and override if required.
|
|
19
|
+
# DECIMAL_TYPES = ("decimal",)
|
|
20
|
+
MONETARY_DIGITS = 2
|
|
21
|
+
CHAR_TYPES = (
|
|
22
|
+
"string",
|
|
23
|
+
"NMTOKEN",
|
|
24
|
+
"ID",
|
|
25
|
+
"IDREF",
|
|
26
|
+
"IDREFS",
|
|
27
|
+
"anyURI",
|
|
28
|
+
"base64Binary",
|
|
29
|
+
"normalizedString",
|
|
30
|
+
"language",
|
|
31
|
+
)
|
|
32
|
+
DATE_TYPES = ("date", "TData")
|
|
33
|
+
DATETIME_TYPES = ("dateTime", "TDateTimeUTC")
|
|
34
|
+
BOOLEAN_TYPES = "boolean"
|
|
35
|
+
|
|
36
|
+
# generally it's not interresting to generate mixins for signature
|
|
37
|
+
SIGNATURE_CLASS_SKIP = [
|
|
38
|
+
"^Signature$",
|
|
39
|
+
"^SignatureType$",
|
|
40
|
+
"^SignatureValueType$",
|
|
41
|
+
"^SignedInfoType$",
|
|
42
|
+
"^ReferenceType$",
|
|
43
|
+
"^DigestMethodType$",
|
|
44
|
+
"^TransformsType$",
|
|
45
|
+
"^TransformType$",
|
|
46
|
+
"^KeyInfoType$",
|
|
47
|
+
"^X509DataType$",
|
|
48
|
+
"^CanonicalizationMethodType$",
|
|
49
|
+
"^SignatureMethodType$",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OdooFilters(Filters):
|
|
54
|
+
__slots__ = (
|
|
55
|
+
"files_to_etree",
|
|
56
|
+
"all_simple_types",
|
|
57
|
+
"all_complex_types",
|
|
58
|
+
"registry_names",
|
|
59
|
+
"implicit_many2ones",
|
|
60
|
+
"schema",
|
|
61
|
+
"version",
|
|
62
|
+
"python_inherit_model",
|
|
63
|
+
"inherit_model",
|
|
64
|
+
"xsd_extra_info",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
config: GeneratorConfig,
|
|
70
|
+
all_simple_types: List[Class],
|
|
71
|
+
all_complex_types: List[Class],
|
|
72
|
+
registry_names: Dict,
|
|
73
|
+
implicit_many2ones: Dict,
|
|
74
|
+
schema: str = "spec",
|
|
75
|
+
version: str = "10",
|
|
76
|
+
python_inherit_model: str = "models.AbstractModel",
|
|
77
|
+
inherit_model=None,
|
|
78
|
+
):
|
|
79
|
+
super().__init__(config)
|
|
80
|
+
self.all_simple_types = all_simple_types
|
|
81
|
+
self.all_complex_types = all_complex_types
|
|
82
|
+
self.registry_names = registry_names
|
|
83
|
+
self.implicit_many2ones = implicit_many2ones
|
|
84
|
+
self.files_to_etree: Dict[str, Any] = {}
|
|
85
|
+
self.schema = schema
|
|
86
|
+
self.version = version
|
|
87
|
+
self.python_inherit_model = python_inherit_model
|
|
88
|
+
if inherit_model is None:
|
|
89
|
+
inherit_model = f"spec.mixin.{schema}"
|
|
90
|
+
self.inherit_model = inherit_model
|
|
91
|
+
self.xsd_extra_info: Dict[str, Any] = {}
|
|
92
|
+
|
|
93
|
+
def register(self, env: Environment):
|
|
94
|
+
super().register(env)
|
|
95
|
+
env.filters.update(
|
|
96
|
+
{
|
|
97
|
+
"enum_skip": self.enum_skip,
|
|
98
|
+
"pattern_skip": self.pattern_skip,
|
|
99
|
+
"registry_name": self.registry_name,
|
|
100
|
+
"odoo_python_inherit_model": self.odoo_python_inherit_model,
|
|
101
|
+
"odoo_inherit_model": self.odoo_inherit_model,
|
|
102
|
+
"clean_docstring": self.clean_docstring,
|
|
103
|
+
"binding_type": self.binding_type,
|
|
104
|
+
"odoo_class_name": self.odoo_class_name,
|
|
105
|
+
"class_properties": self.class_properties,
|
|
106
|
+
"odoo_class_description": self.odoo_class_description,
|
|
107
|
+
"odoo_field_definition": self.odoo_field_definition,
|
|
108
|
+
"odoo_implicit_many2ones": self.odoo_implicit_many2ones,
|
|
109
|
+
"import_class": self.import_class,
|
|
110
|
+
"enum_docstring": self.enum_docstring,
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def enum_skip(self, obj: Class, name: str) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Avoids Postgres errors with fields.Selection
|
|
117
|
+
by disabling case insensitive duplicates.
|
|
118
|
+
For instance:
|
|
119
|
+
TENDEREMI_XPAIS = [
|
|
120
|
+
("Brasil", "Brasil"),
|
|
121
|
+
# ("BRASIL", "BRASIL"),
|
|
122
|
+
]
|
|
123
|
+
"""
|
|
124
|
+
lower_vals = set()
|
|
125
|
+
allowed_vals = set()
|
|
126
|
+
for attr in obj.attrs:
|
|
127
|
+
if attr.default.lower() not in lower_vals:
|
|
128
|
+
lower_vals.add(attr.default.lower())
|
|
129
|
+
allowed_vals.add(attr.default)
|
|
130
|
+
if name in allowed_vals:
|
|
131
|
+
return False
|
|
132
|
+
else:
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
def pattern_skip(self, name: str, parents: List[Class]) -> bool:
|
|
136
|
+
"""Should class or field be skipped?"""
|
|
137
|
+
if parents is None:
|
|
138
|
+
parents = []
|
|
139
|
+
class_skip = SIGNATURE_CLASS_SKIP.copy()
|
|
140
|
+
if os.environ.get("XSDATA_SKIP"):
|
|
141
|
+
class_skip += os.environ["XSDATA_SKIP"].split("|")
|
|
142
|
+
for pattern in class_skip:
|
|
143
|
+
# do we have a simple match? (no scoping can be risky)
|
|
144
|
+
if re.search(pattern, name):
|
|
145
|
+
return True
|
|
146
|
+
part_count = pattern.count(".") + 1
|
|
147
|
+
if part_count > 1:
|
|
148
|
+
# eventually we search for the class with its parents scope
|
|
149
|
+
parent_pattern = ".".join(
|
|
150
|
+
[namespaces.local_name(c.qname) for c in parents[-part_count:]]
|
|
151
|
+
)
|
|
152
|
+
if re.search(pattern, parent_pattern):
|
|
153
|
+
return True
|
|
154
|
+
# we now search for the field_name with its parents scope
|
|
155
|
+
field_parent_pattern = ".".join(
|
|
156
|
+
[namespaces.local_name(c.qname) for c in parents[-part_count + 1 :]]
|
|
157
|
+
+ [name]
|
|
158
|
+
)
|
|
159
|
+
if re.search(pattern, field_parent_pattern):
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def odoo_class_description(self, obj: Class) -> str:
|
|
165
|
+
if obj.help:
|
|
166
|
+
return """textwrap.dedent(" %s" % (__doc__,))"""
|
|
167
|
+
else:
|
|
168
|
+
# TODO some inner classes have no obj.help
|
|
169
|
+
# but we can read the XML annotations. Ex: "TretEnviNfe.InfRec", "Tnfe.InfNfe"...
|
|
170
|
+
return f'"{obj.name}"'
|
|
171
|
+
|
|
172
|
+
def enum_docstring(self, obj: Class) -> str:
|
|
173
|
+
"""Works well for Brazilian fiscal xsd, may need adaptations for your
|
|
174
|
+
xsd."""
|
|
175
|
+
# see https://github.com/akretion/generateds-odoo/blob/465539b46e4216a5b94f1b0dabf39b34e7f4624c/gends_extract_simple_types.py#L385
|
|
176
|
+
# for possible improvement
|
|
177
|
+
|
|
178
|
+
if "_" in obj.name:
|
|
179
|
+
type_qname = obj.qname.split("_")[0]
|
|
180
|
+
field_name = obj.name.split("_")[1]
|
|
181
|
+
|
|
182
|
+
for klass in self.all_complex_types:
|
|
183
|
+
if klass.qname == type_qname:
|
|
184
|
+
for idx, item in enumerate(obj.attrs):
|
|
185
|
+
for field in klass.attrs:
|
|
186
|
+
if field.name == field_name and field.help:
|
|
187
|
+
item_help = False
|
|
188
|
+
separators = (" - ", "-", " – ", "–")
|
|
189
|
+
for separator in separators:
|
|
190
|
+
split = field.help.split(
|
|
191
|
+
f"{item.default}{separator}"
|
|
192
|
+
)
|
|
193
|
+
if len(split) > 1:
|
|
194
|
+
# TODO sometimes the line may continue
|
|
195
|
+
# until the next value or may end at next value...
|
|
196
|
+
item_help = (
|
|
197
|
+
split[1].splitlines()[0].split(";")[0]
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
if item_help:
|
|
201
|
+
item.help = item_help
|
|
202
|
+
if idx == 0 and len(split) > 1:
|
|
203
|
+
obj.help, help_trash = extract_string_and_help(
|
|
204
|
+
obj.name,
|
|
205
|
+
field.name,
|
|
206
|
+
split[0],
|
|
207
|
+
set(),
|
|
208
|
+
1024,
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
item.help = item.default
|
|
212
|
+
if (
|
|
213
|
+
idx == 0
|
|
214
|
+
and not obj.help
|
|
215
|
+
and not field.help.startswith(item.default)
|
|
216
|
+
):
|
|
217
|
+
obj.help = (
|
|
218
|
+
field.help
|
|
219
|
+
) # no split but better than no docstring
|
|
220
|
+
|
|
221
|
+
return self.clean_docstring(obj.help)
|
|
222
|
+
|
|
223
|
+
def odoo_class_name(self, obj: Class, parents: List[Class] = []):
|
|
224
|
+
"""
|
|
225
|
+
Same as class_name with the --unnest-classes option but without the
|
|
226
|
+
option side effects.
|
|
227
|
+
"""
|
|
228
|
+
full_name = ".".join([self.class_name(c.name) for c in parents])
|
|
229
|
+
return self.registry_names[full_name].replace(".", "")
|
|
230
|
+
|
|
231
|
+
def registry_name(
|
|
232
|
+
self,
|
|
233
|
+
name: str = "",
|
|
234
|
+
parents: List[Class] = [],
|
|
235
|
+
type_names: List[str] = [],
|
|
236
|
+
) -> str:
|
|
237
|
+
if parents:
|
|
238
|
+
full_name = ".".join([self.class_name(c.name) for c in parents])
|
|
239
|
+
else:
|
|
240
|
+
if type_names:
|
|
241
|
+
full_name = ".".join(type_names)
|
|
242
|
+
else:
|
|
243
|
+
full_name = self.class_name(name)
|
|
244
|
+
# NOTE we cannot use the class ref as a key because only type names
|
|
245
|
+
# are provided by xsdata for fields
|
|
246
|
+
unique_name = self.registry_names[full_name].replace(".", "_")
|
|
247
|
+
return f"{self.schema}.{self.version}.{unique_name.lower()}"
|
|
248
|
+
|
|
249
|
+
def odoo_inherit_model(self, obj: Class) -> str:
|
|
250
|
+
return self.inherit_model
|
|
251
|
+
|
|
252
|
+
def odoo_python_inherit_model(self, obj: Class) -> str:
|
|
253
|
+
return self.python_inherit_model
|
|
254
|
+
|
|
255
|
+
def registry_comodel(self, type_name: str):
|
|
256
|
+
# NOTE: we take only the last part of inner Types with .split(".")[-1]
|
|
257
|
+
# but if that were to create Type duplicates we could change that.
|
|
258
|
+
clean_type_names = type_name.replace('"', "").split(".")
|
|
259
|
+
return self.registry_name(clean_type_names[-1], type_names=clean_type_names)
|
|
260
|
+
|
|
261
|
+
def clean_docstring(self, string: Optional[str], escape: bool = True) -> str:
|
|
262
|
+
"""Prepare string for docstring generation."""
|
|
263
|
+
if not string:
|
|
264
|
+
return "" # TODO read from parent field if any
|
|
265
|
+
return "\n {}".format(wrap_text(string, 4, 79))
|
|
266
|
+
|
|
267
|
+
def class_properties(
|
|
268
|
+
self,
|
|
269
|
+
obj: Class,
|
|
270
|
+
parents: List[Class],
|
|
271
|
+
) -> str:
|
|
272
|
+
"""Return the name of the xsdata class for a given Odoo model."""
|
|
273
|
+
if os.environ.get("XSDATA_GENDS"):
|
|
274
|
+
return (
|
|
275
|
+
f'_binding_type = "{self.binding_type(obj, parents)}"\n'
|
|
276
|
+
f' _generateds_type = "{self.generateds_type(obj, parents)}"'
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
return f'_binding_type = "{self.binding_type(obj, parents)}"\n'
|
|
280
|
+
|
|
281
|
+
def binding_type(
|
|
282
|
+
self,
|
|
283
|
+
obj: Class,
|
|
284
|
+
parents: List[Class],
|
|
285
|
+
) -> str:
|
|
286
|
+
"""Return the name of the xsdata class for a given Odoo model."""
|
|
287
|
+
return ".".join([self.class_name(p.name) for p in parents])
|
|
288
|
+
|
|
289
|
+
def generateds_type(
|
|
290
|
+
self,
|
|
291
|
+
obj: Class,
|
|
292
|
+
parents: List[Class],
|
|
293
|
+
) -> str:
|
|
294
|
+
"""
|
|
295
|
+
DEPRECATED!
|
|
296
|
+
Return the name of the GenerateDS class for a given Odoo model.
|
|
297
|
+
This is for backward compatibility: it allows to use Odoo mixins
|
|
298
|
+
generated with xsdata along with legacy GenerateDS Python bindings.
|
|
299
|
+
"""
|
|
300
|
+
if len(parents) > 1:
|
|
301
|
+
return obj.qname.split("}")[1] + "Type"
|
|
302
|
+
else:
|
|
303
|
+
return obj.qname.split("}")[1]
|
|
304
|
+
|
|
305
|
+
def odoo_implicit_many2ones(self, obj: Class, parents: List[Class]) -> str:
|
|
306
|
+
"""The m2o fields for the o2m keys."""
|
|
307
|
+
fields = []
|
|
308
|
+
implicit_many2ones = self.implicit_many2ones.get(
|
|
309
|
+
self.binding_type(obj, parents).lower(),
|
|
310
|
+
[],
|
|
311
|
+
# NOTE: strangely lower is required (Brazilian CTe)
|
|
312
|
+
)
|
|
313
|
+
for implicit_many2one_data in implicit_many2ones:
|
|
314
|
+
kwargs = OrderedDict()
|
|
315
|
+
kwargs["comodel_name"] = implicit_many2one_data[0]
|
|
316
|
+
kwargs["xsd_implicit"] = True
|
|
317
|
+
# kwargs["required"] = True # FIXME seems it creates ORM issues
|
|
318
|
+
kwargs["ondelete"] = "cascade"
|
|
319
|
+
target_field = implicit_many2one_data[1]
|
|
320
|
+
fields.append(
|
|
321
|
+
f"{target_field} = fields.Many2one({self.format_arguments(kwargs, 4)})"
|
|
322
|
+
)
|
|
323
|
+
return ("\n").join(fields)
|
|
324
|
+
|
|
325
|
+
def field_name(self, name: str, class_name: str) -> str:
|
|
326
|
+
"""Like xsdata but you can enforce the prefix or safe_name
|
|
327
|
+
conversion."""
|
|
328
|
+
prefix = self.field_safe_prefix
|
|
329
|
+
name = self.apply_substitutions(name, ObjectType.FIELD)
|
|
330
|
+
|
|
331
|
+
if self.field_safe_prefix in ("", "value"): # do like standard xsdata
|
|
332
|
+
name = self.safe_name(name, prefix, self.field_case, class_name=class_name)
|
|
333
|
+
if self.field_safe_prefix == "NO_PREFIX_NO_SAFE_NAME": # keep my field name!
|
|
334
|
+
pass
|
|
335
|
+
elif self.field_safe_prefix.endswith("SAFE_NAME"): # prefix + python field name
|
|
336
|
+
prefix = prefix.split("SAFE_NAME")[0]
|
|
337
|
+
name = f"{prefix}{name}"
|
|
338
|
+
name = self.safe_name(name, prefix, self.field_case, class_name=class_name)
|
|
339
|
+
else: # prefix + keep my field name
|
|
340
|
+
name = f"{prefix}{name}"
|
|
341
|
+
|
|
342
|
+
return self.apply_substitutions(name, ObjectType.FIELD)
|
|
343
|
+
|
|
344
|
+
def odoo_field_definition(
|
|
345
|
+
self,
|
|
346
|
+
attr: Attr,
|
|
347
|
+
parents: List[Class],
|
|
348
|
+
) -> str:
|
|
349
|
+
"""Return the Odoo field definition."""
|
|
350
|
+
|
|
351
|
+
# 1st some checks inspired from xsdata Filters:
|
|
352
|
+
obj = parents[-1]
|
|
353
|
+
type_names = self._field_type_names(obj, attr)
|
|
354
|
+
# collections.unique_sequence(
|
|
355
|
+
# self._field_type_name(x, [p.name for p in parents]) for x in attr.types
|
|
356
|
+
# )
|
|
357
|
+
if len(type_names) > 1:
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"len(type_names) > 1 (Union) not implemented yet! class: {obj.name} attr: {attr}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if attr.is_tokens:
|
|
363
|
+
logger.warning(
|
|
364
|
+
f"attr.is_tokens not implemented yet! class: {obj.name} attr: {attr}"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if attr.is_dict:
|
|
368
|
+
logger.warning(
|
|
369
|
+
f"attr.is_dict not implemented yet! class: {obj.name} attr: {attr}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# default_value = self.field_default_value(attr, {})
|
|
373
|
+
|
|
374
|
+
kwargs = self._extract_field_attributes(parents, attr)
|
|
375
|
+
|
|
376
|
+
if attr.types[0].datatype: # simple field
|
|
377
|
+
return self._simple_field_definition(obj, attr, type_names, kwargs)
|
|
378
|
+
|
|
379
|
+
else: # relational field
|
|
380
|
+
field = self._try_one2many_field_definition(obj, attr, type_names, kwargs)
|
|
381
|
+
if field is None:
|
|
382
|
+
field = self._try_selection_field_definition(
|
|
383
|
+
obj, attr, type_names, kwargs
|
|
384
|
+
)
|
|
385
|
+
if field is None:
|
|
386
|
+
field = self._try_many2one_field_definition(
|
|
387
|
+
obj, attr, type_names, kwargs
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if field is not None:
|
|
391
|
+
return field
|
|
392
|
+
|
|
393
|
+
message = f"Missing class {attr.types[0]}! class: {obj.name} attr: {attr}"
|
|
394
|
+
logger.warning(message)
|
|
395
|
+
return message
|
|
396
|
+
|
|
397
|
+
def _extract_field_attributes(
|
|
398
|
+
self, parents: List[Class], attr: Attr
|
|
399
|
+
) -> OrderedDict[str, Any]:
|
|
400
|
+
obj = parents[-1]
|
|
401
|
+
kwargs: OrderedDict[str, Any] = OrderedDict()
|
|
402
|
+
if not hasattr(obj, "unique_labels"):
|
|
403
|
+
obj.unique_labels = set() # will avoid repeating field labels
|
|
404
|
+
string, help_attr = extract_string_and_help(
|
|
405
|
+
obj.name, attr.name, attr.help, obj.unique_labels
|
|
406
|
+
)
|
|
407
|
+
kwargs["string"] = string
|
|
408
|
+
|
|
409
|
+
metadata = self.field_metadata(obj, attr, None)
|
|
410
|
+
if metadata.get("required"):
|
|
411
|
+
# we choose not to put required=True (required in database) to avoid
|
|
412
|
+
# messing with existing Odoo modules.
|
|
413
|
+
kwargs["xsd_required"] = True
|
|
414
|
+
|
|
415
|
+
kwargs.update(self.xsd_extra_info.get(f"{obj.name}#{attr.name}", {}))
|
|
416
|
+
if help_attr and not kwargs.get("help"):
|
|
417
|
+
kwargs["help"] = help_attr # (help as the last attribute)
|
|
418
|
+
|
|
419
|
+
return kwargs
|
|
420
|
+
|
|
421
|
+
def _extract_number_attrs(
|
|
422
|
+
self, obj: Class, attr: Attr, kwargs: OrderedDict[str, Any]
|
|
423
|
+
):
|
|
424
|
+
"""
|
|
425
|
+
Monetary vs Float field detection.
|
|
426
|
+
|
|
427
|
+
Detection tends to be brittle but in general it doesn't impact
|
|
428
|
+
XML serialization/desrialization as both types are floats and we
|
|
429
|
+
usually take the xsd_type into account.
|
|
430
|
+
|
|
431
|
+
You can somewhat customize the default behavior with ENV VARs:
|
|
432
|
+
XSDATA_MONETARY_TYPE: xsd type to force fields.Monetary
|
|
433
|
+
XSDATA_NUM_TYPE: xsd type for fields.Float or fields.Monetary eventually
|
|
434
|
+
you can use a prefix followed by brackets to indicate where to take the
|
|
435
|
+
floor part and the decimal part like Prefix[in_start:int_stop.dec_start:dec_stop]
|
|
436
|
+
"""
|
|
437
|
+
python_type = attr.types[0].datatype.code
|
|
438
|
+
if python_type in FLOAT_TYPES or python_type in CHAR_TYPES:
|
|
439
|
+
xsd_type = kwargs.get("xsd_type", "")
|
|
440
|
+
|
|
441
|
+
monetary_type = os.environ.get("XSDATA_MONETARY_TYPE")
|
|
442
|
+
|
|
443
|
+
num_type_complete = os.environ.get("XSDATA_NUM_TYPE", "TDec_[5:7.7:9]")
|
|
444
|
+
if "[" in num_type_complete:
|
|
445
|
+
int_part = num_type_complete.split("[")[1].split(".")[0]
|
|
446
|
+
int_start = int(int_part.split(":")[0])
|
|
447
|
+
int_stop = int(int_part.split(":")[1])
|
|
448
|
+
dec_part = num_type_complete.split(".")[1].split("]")[0]
|
|
449
|
+
dec_start = int(dec_part.split(":")[0])
|
|
450
|
+
dec_stop = int(dec_part.split(":")[1])
|
|
451
|
+
num_type = num_type_complete.split("[")[0]
|
|
452
|
+
conditional_monetary = True
|
|
453
|
+
else:
|
|
454
|
+
num_type = num_type_complete
|
|
455
|
+
conditional_monetary = False
|
|
456
|
+
|
|
457
|
+
if monetary_type and xsd_type.startswith(monetary_type):
|
|
458
|
+
kwargs["currency_field"] = "brl_currency_id" # TODO use spec_curreny_id
|
|
459
|
+
elif xsd_type.startswith(num_type):
|
|
460
|
+
if conditional_monetary:
|
|
461
|
+
if int(
|
|
462
|
+
xsd_type.replace("03v", "03")[dec_start:dec_stop]
|
|
463
|
+
) != MONETARY_DIGITS or (
|
|
464
|
+
# for Brazilian edocs, pSomething means percentualSomething ->Float
|
|
465
|
+
attr.name[0] == "p" and attr.name[1].isupper()
|
|
466
|
+
):
|
|
467
|
+
kwargs["digits"] = (
|
|
468
|
+
int(xsd_type.replace("03v", "03")[int_start:int_stop]),
|
|
469
|
+
int(xsd_type.replace("03v", "03")[dec_start:dec_stop]),
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
kwargs["currency_field"] = (
|
|
473
|
+
"brl_currency_id" # TODO use spec_curreny_id
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
kwargs["digits"] = (16, 4)
|
|
477
|
+
|
|
478
|
+
def _simple_field_definition(
|
|
479
|
+
self, obj: Class, attr: Attr, type_names: str, kwargs: OrderedDict
|
|
480
|
+
):
|
|
481
|
+
self._extract_number_attrs(obj, attr, kwargs)
|
|
482
|
+
if kwargs.get("help"):
|
|
483
|
+
kwargs.move_to_end("help", last=True)
|
|
484
|
+
python_type = attr.types[0].datatype.code
|
|
485
|
+
if python_type in DATE_TYPES or kwargs.get("xsd_type") in DATE_TYPES:
|
|
486
|
+
return f"fields.Date({self.format_arguments(kwargs, 4)})"
|
|
487
|
+
elif python_type in DATETIME_TYPES or kwargs.get("xsd_type") in DATETIME_TYPES:
|
|
488
|
+
return f"fields.Datetime({self.format_arguments(kwargs, 4)})"
|
|
489
|
+
elif python_type in INTEGER_TYPES:
|
|
490
|
+
return f"fields.Integer({self.format_arguments(kwargs, 4)})"
|
|
491
|
+
if kwargs.get("currency_field"):
|
|
492
|
+
return f"fields.Monetary({self.format_arguments(kwargs, 4)})"
|
|
493
|
+
elif python_type in FLOAT_TYPES or kwargs.get("digits"):
|
|
494
|
+
return f"fields.Float({self.format_arguments(kwargs, 4)})"
|
|
495
|
+
elif python_type in CHAR_TYPES:
|
|
496
|
+
return f"fields.Char({self.format_arguments(kwargs, 4)})"
|
|
497
|
+
elif python_type in BOOLEAN_TYPES:
|
|
498
|
+
return f"fields.Boolean({self.format_arguments(kwargs, 4)})"
|
|
499
|
+
else:
|
|
500
|
+
logger.warning(
|
|
501
|
+
f"{python_type} {attr.types[0].datatype} not implemented yet! class: {obj.name} attr: {attr}"
|
|
502
|
+
)
|
|
503
|
+
return ""
|
|
504
|
+
|
|
505
|
+
def _try_one2many_field_definition(
|
|
506
|
+
self, obj: Class, attr: Attr, type_names: str, kwargs: OrderedDict
|
|
507
|
+
):
|
|
508
|
+
if attr.is_list:
|
|
509
|
+
comodel_key = self.field_name(f"{attr.name}_{obj.name}_id", obj.name)
|
|
510
|
+
return f"""fields.One2many("{self.registry_comodel(type_names)}", "{comodel_key}",{self.format_arguments(kwargs, 4)})"""
|
|
511
|
+
|
|
512
|
+
def _try_selection_field_definition(
|
|
513
|
+
self, obj: Class, attr: Attr, type_names: str, kwargs: OrderedDict
|
|
514
|
+
):
|
|
515
|
+
for klass in self.all_simple_types:
|
|
516
|
+
if attr.types[0].qname == klass.qname:
|
|
517
|
+
return f"fields.Selection({klass.name.upper()},{self.format_arguments(kwargs, 4)})"
|
|
518
|
+
|
|
519
|
+
def _try_many2one_field_definition(
|
|
520
|
+
self, obj: Class, attr: Attr, type_names: str, kwargs: OrderedDict
|
|
521
|
+
):
|
|
522
|
+
for klass in self.all_complex_types:
|
|
523
|
+
if attr.types[0].qname == klass.qname:
|
|
524
|
+
# Many2one
|
|
525
|
+
kwargs["comodel_name"] = self.registry_comodel(type_names)
|
|
526
|
+
kwargs.move_to_end("comodel_name", last=False)
|
|
527
|
+
return f"fields.Many2one({self.format_arguments(kwargs, 4)})"
|
|
528
|
+
|
|
529
|
+
def import_class(self, name: str, alias: Optional[str]) -> str:
|
|
530
|
+
"""Convert import class name with alias support."""
|
|
531
|
+
if alias:
|
|
532
|
+
return f"{self.class_name(name)} as {self.class_name(alias)}"
|
|
533
|
+
if name in [klass.name for klass in self.all_simple_types]:
|
|
534
|
+
return name.upper() # const are upcase in Odoo
|
|
535
|
+
else:
|
|
536
|
+
return self.class_name(name)
|