mangono-addon-export_json 18.0.1.0.4__py3-none-any.whl → 19.0.1.0.1__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.
- {mangono_addon_export_json-18.0.1.0.4.dist-info → mangono_addon_export_json-19.0.1.0.1.dist-info}/METADATA +2 -3
- mangono_addon_export_json-19.0.1.0.1.dist-info/RECORD +52 -0
- odoo/addons/export_json/README.rst +2 -2
- odoo/addons/export_json/__init__.py +4 -0
- odoo/addons/export_json/__manifest__.py +10 -3
- odoo/addons/export_json/controller/main.py +1 -1
- odoo/addons/export_json/demo/export_demo.xml +7 -0
- odoo/addons/export_json/demo/ir.exports.line.csv +16 -0
- odoo/addons/export_json/demo/resolver_demo.xml +12 -0
- odoo/addons/export_json/jsonifier/README.rst +284 -0
- odoo/addons/export_json/jsonifier/__init__.py +1 -0
- odoo/addons/export_json/jsonifier/__manifest__.old +27 -0
- odoo/addons/export_json/jsonifier/demo/export_demo.xml +7 -0
- odoo/addons/export_json/jsonifier/demo/ir.exports.line.csv +16 -0
- odoo/addons/export_json/jsonifier/demo/resolver_demo.xml +12 -0
- odoo/addons/export_json/jsonifier/exceptions.py +7 -0
- odoo/addons/export_json/jsonifier/i18n/ca.po +234 -0
- odoo/addons/export_json/jsonifier/i18n/es.po +266 -0
- odoo/addons/export_json/jsonifier/i18n/it.po +277 -0
- odoo/addons/export_json/jsonifier/i18n/zh_CN.po +260 -0
- odoo/addons/export_json/jsonifier/models/__init__.py +5 -0
- odoo/addons/export_json/jsonifier/models/ir_exports.py +124 -0
- odoo/addons/export_json/jsonifier/models/ir_exports_line.py +55 -0
- odoo/addons/export_json/jsonifier/models/ir_exports_resolver.py +58 -0
- odoo/addons/export_json/jsonifier/models/models.py +263 -0
- odoo/addons/export_json/jsonifier/models/utils.py +35 -0
- odoo/addons/export_json/jsonifier/pyproject.toml +3 -0
- odoo/addons/export_json/jsonifier/readme/CONTRIBUTORS.md +8 -0
- odoo/addons/export_json/jsonifier/readme/CREDITS.md +1 -0
- odoo/addons/export_json/jsonifier/readme/DESCRIPTION.md +163 -0
- odoo/addons/export_json/jsonifier/readme/USAGE.md +26 -0
- odoo/addons/export_json/jsonifier/security/ir.model.access.csv +2 -0
- odoo/addons/export_json/jsonifier/tests/__init__.py +3 -0
- odoo/addons/export_json/jsonifier/tests/test_get_parser.py +436 -0
- odoo/addons/export_json/jsonifier/tests/test_helpers.py +45 -0
- odoo/addons/export_json/jsonifier/tests/test_ir_exports_line.py +66 -0
- odoo/addons/export_json/jsonifier/views/ir_exports_resolver_view.xml +26 -0
- odoo/addons/export_json/jsonifier/views/ir_exports_view.xml +38 -0
- odoo/addons/export_json/models/file_json.py +1 -2
- odoo/addons/export_json/tests/test_export_json.py +14 -11
- mangono_addon_export_json-18.0.1.0.4.dist-info/RECORD +0 -20
- {mangono_addon_export_json-18.0.1.0.4.dist-info → mangono_addon_export_json-19.0.1.0.1.dist-info}/WHEEL +0 -0
- {mangono_addon_export_json-18.0.1.0.4.dist-info → mangono_addon_export_json-19.0.1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright 2017 ACSONE SA/NV
|
|
2
|
+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
3
|
+
|
|
4
|
+
from odoo import _, api, fields, models
|
|
5
|
+
from odoo.exceptions import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IrExportsLine(models.Model):
|
|
9
|
+
_inherit = "ir.exports.line"
|
|
10
|
+
|
|
11
|
+
target = fields.Char(
|
|
12
|
+
help="The complete path to the field where you can specify a target on the step as field:target",
|
|
13
|
+
)
|
|
14
|
+
active = fields.Boolean(default=True)
|
|
15
|
+
lang_id = fields.Many2one(
|
|
16
|
+
comodel_name="res.lang",
|
|
17
|
+
string="Language",
|
|
18
|
+
help="If set, the language in which the field is exported",
|
|
19
|
+
)
|
|
20
|
+
resolver_id = fields.Many2one(
|
|
21
|
+
comodel_name="ir.exports.resolver",
|
|
22
|
+
string="Custom resolver",
|
|
23
|
+
help="If set, will apply the resolver on the field value",
|
|
24
|
+
)
|
|
25
|
+
instance_method_name = fields.Char(
|
|
26
|
+
string="Function",
|
|
27
|
+
help="A method defined on the model that takes a record and a field_name",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@api.constrains("resolver_id", "instance_method_name")
|
|
31
|
+
def _check_function_resolver(self):
|
|
32
|
+
for rec in self:
|
|
33
|
+
if rec.resolver_id and rec.instance_method_name:
|
|
34
|
+
msg = _("Either set a function or a resolver, not both.")
|
|
35
|
+
raise ValidationError(msg)
|
|
36
|
+
|
|
37
|
+
@api.constrains("target", "name")
|
|
38
|
+
def _check_target(self):
|
|
39
|
+
for rec in self:
|
|
40
|
+
if not rec.target:
|
|
41
|
+
continue
|
|
42
|
+
names = rec.name.split("/")
|
|
43
|
+
names_with_target = rec.target.split("/")
|
|
44
|
+
if len(names) != len(names_with_target):
|
|
45
|
+
raise ValidationError(_("Name and Target must have the same hierarchy depth"))
|
|
46
|
+
for name, name_with_target in zip(names, names_with_target, strict=True):
|
|
47
|
+
field_name = name_with_target.split(":")[0]
|
|
48
|
+
if name != field_name:
|
|
49
|
+
raise ValidationError(
|
|
50
|
+
_(
|
|
51
|
+
"The target must reference the same field as in "
|
|
52
|
+
"name '%(name)s' not in '%(name_with_target)s'"
|
|
53
|
+
)
|
|
54
|
+
% dict(name=name, name_with_target=name_with_target)
|
|
55
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright 2020 ACSONE SA/NV
|
|
2
|
+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
from odoo.tools.safe_eval import safe_eval
|
|
6
|
+
|
|
7
|
+
help_message = [
|
|
8
|
+
"Compute the result from 'value' by setting the variable 'result'.",
|
|
9
|
+
"\nFor fields resolvers:",
|
|
10
|
+
":param record: the record",
|
|
11
|
+
":param name: name of the field",
|
|
12
|
+
":param value: value of the field",
|
|
13
|
+
":param field_type: type of the field",
|
|
14
|
+
"\nFor global resolvers:",
|
|
15
|
+
":param value: JSON dict",
|
|
16
|
+
":param record: the record",
|
|
17
|
+
"\n"
|
|
18
|
+
"In both types, you can override the final json key."
|
|
19
|
+
"\nTo achieve this, simply return a dict like: "
|
|
20
|
+
"\n{'result': {'_value': $value, '_json_key': $new_json_key}}",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FieldResolver(models.Model):
|
|
25
|
+
"""Arbitrary function to process a field or a dict at export time."""
|
|
26
|
+
|
|
27
|
+
_name = "ir.exports.resolver"
|
|
28
|
+
_description = "Export Resolver"
|
|
29
|
+
|
|
30
|
+
name = fields.Char()
|
|
31
|
+
type = fields.Selection([("field", "Field"), ("global", "Global")])
|
|
32
|
+
python_code = fields.Text(
|
|
33
|
+
default="\n".join(["# " + h for h in help_message] + ["result = value"]),
|
|
34
|
+
help="\n".join(help_message),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def resolve(self, param, records):
|
|
38
|
+
self.ensure_one()
|
|
39
|
+
result = []
|
|
40
|
+
context = records.env.context
|
|
41
|
+
if self.type == "global":
|
|
42
|
+
assert len(param) == len(records)
|
|
43
|
+
for value, record in zip(param, records, strict=True):
|
|
44
|
+
values = {"value": value, "record": record, "context": context}
|
|
45
|
+
safe_eval(self.python_code, values, mode="exec", nocopy=True)
|
|
46
|
+
result.append(values["result"])
|
|
47
|
+
else: # param is a field
|
|
48
|
+
for record in records:
|
|
49
|
+
values = {
|
|
50
|
+
"record": record,
|
|
51
|
+
"value": record[param.name],
|
|
52
|
+
"name": param.name,
|
|
53
|
+
"field_type": param.type,
|
|
54
|
+
"context": context,
|
|
55
|
+
}
|
|
56
|
+
safe_eval(self.python_code, values, mode="exec", nocopy=True)
|
|
57
|
+
result.append(values["result"])
|
|
58
|
+
return result
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Copyright 2017 Akretion (http://www.akretion.com)
|
|
2
|
+
# Sébastien BEAU <sebastien.beau@akretion.com>
|
|
3
|
+
# Raphaël Reverdy <raphael.reverdy@akretion.com>
|
|
4
|
+
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
|
5
|
+
# Simone Orsi <simahawk@gmail.com>
|
|
6
|
+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from odoo import api, fields, models, tools
|
|
11
|
+
from odoo.exceptions import UserError
|
|
12
|
+
from odoo.tools.misc import format_duration
|
|
13
|
+
from odoo.tools.translate import _
|
|
14
|
+
|
|
15
|
+
from ..exceptions import SwallableException
|
|
16
|
+
from .utils import convert_simple_to_full_parser
|
|
17
|
+
|
|
18
|
+
_logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Base(models.AbstractModel):
|
|
22
|
+
_inherit = "base"
|
|
23
|
+
|
|
24
|
+
@api.model
|
|
25
|
+
def __parse_field(self, parser_field):
|
|
26
|
+
"""Deduct how to handle a field from its parser."""
|
|
27
|
+
return parser_field if isinstance(parser_field, tuple) else (parser_field, None)
|
|
28
|
+
|
|
29
|
+
@api.model
|
|
30
|
+
def _jsonify_bad_parser_error(self, field_name):
|
|
31
|
+
raise UserError(_("Wrong parser configuration for field: `%s`") % field_name)
|
|
32
|
+
|
|
33
|
+
def _function_value(self, record, function, field_name):
|
|
34
|
+
if function in dir(record):
|
|
35
|
+
method = getattr(record, function, None)
|
|
36
|
+
return method(field_name)
|
|
37
|
+
elif callable(function):
|
|
38
|
+
return function(record, field_name)
|
|
39
|
+
else:
|
|
40
|
+
return self._jsonify_bad_parser_error(field_name)
|
|
41
|
+
|
|
42
|
+
@api.model
|
|
43
|
+
def _jsonify_value(self, field, value):
|
|
44
|
+
"""Override this function to support new field types."""
|
|
45
|
+
if value is False and field.type != "boolean":
|
|
46
|
+
value = None
|
|
47
|
+
elif field.type == "date":
|
|
48
|
+
value = fields.Date.to_date(value).isoformat()
|
|
49
|
+
elif field.type == "datetime":
|
|
50
|
+
# Ensures value is a datetime
|
|
51
|
+
value = fields.Datetime.to_datetime(value)
|
|
52
|
+
value = value.isoformat()
|
|
53
|
+
elif field.type in ("many2one", "reference"):
|
|
54
|
+
value = value.display_name if value else None
|
|
55
|
+
elif field.type in ("one2many", "many2many"):
|
|
56
|
+
value = [v.display_name for v in value]
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
@api.model
|
|
60
|
+
def _add_json_key(self, values, json_key, value):
|
|
61
|
+
"""To manage defaults, you can use a specific resolver."""
|
|
62
|
+
key, sep, marshaller = json_key.partition("=")
|
|
63
|
+
if marshaller == "list": # sublist field
|
|
64
|
+
if not values.get(key):
|
|
65
|
+
values[key] = []
|
|
66
|
+
values[key].append(value)
|
|
67
|
+
else:
|
|
68
|
+
values[key] = value
|
|
69
|
+
|
|
70
|
+
@api.model
|
|
71
|
+
def _jsonify_record(self, parser, rec, root):
|
|
72
|
+
"""JSONify one record (rec). Private function called by jsonify."""
|
|
73
|
+
strict = self.env.context.get("jsonify_record_strict", False)
|
|
74
|
+
for field_key in parser:
|
|
75
|
+
field_dict, subparser = rec.__parse_field(field_key)
|
|
76
|
+
function = field_dict.get("function")
|
|
77
|
+
try:
|
|
78
|
+
self._jsonify_record_validate_field(rec, field_dict, strict)
|
|
79
|
+
except SwallableException:
|
|
80
|
+
if not function:
|
|
81
|
+
# If we have a function we can use it to get the value
|
|
82
|
+
# even if the field is not available.
|
|
83
|
+
# If not, well there's nothing we can do.
|
|
84
|
+
continue
|
|
85
|
+
json_key = field_dict.get("target", field_dict["name"])
|
|
86
|
+
if function:
|
|
87
|
+
try:
|
|
88
|
+
value = self._jsonify_record_handle_function(rec, field_dict, strict)
|
|
89
|
+
except SwallableException:
|
|
90
|
+
continue
|
|
91
|
+
elif subparser:
|
|
92
|
+
try:
|
|
93
|
+
value = self._jsonify_record_handle_subparser(rec, field_dict, strict, subparser)
|
|
94
|
+
except SwallableException:
|
|
95
|
+
continue
|
|
96
|
+
else:
|
|
97
|
+
field = rec._fields[field_dict["name"]]
|
|
98
|
+
value = rec._jsonify_value(field, rec[field.name])
|
|
99
|
+
resolver = field_dict.get("resolver")
|
|
100
|
+
if resolver:
|
|
101
|
+
if isinstance(resolver, int):
|
|
102
|
+
# cached versions of the parser are stored as integer
|
|
103
|
+
resolver = self.env["ir.exports.resolver"].browse(resolver)
|
|
104
|
+
value, json_key = self._jsonify_record_handle_resolver(rec, field, resolver, json_key)
|
|
105
|
+
# whatever json value we have found in subparser or not ass a sister key
|
|
106
|
+
# on the same level _fieldname_{json_key}
|
|
107
|
+
if rec.env.context.get("with_fieldname"):
|
|
108
|
+
json_key_fieldname = "_fieldname_" + json_key
|
|
109
|
+
# check if we are in a subparser has already the fieldname sister keys
|
|
110
|
+
fieldname_value = rec._fields[field_dict["name"]].string
|
|
111
|
+
self._add_json_key(root, json_key_fieldname, fieldname_value)
|
|
112
|
+
self._add_json_key(root, json_key, value)
|
|
113
|
+
return root
|
|
114
|
+
|
|
115
|
+
def _jsonify_record_validate_field(self, rec, field_dict, strict):
|
|
116
|
+
field_name = field_dict["name"]
|
|
117
|
+
if field_name not in rec._fields:
|
|
118
|
+
if strict:
|
|
119
|
+
# let it fail
|
|
120
|
+
rec._fields[field_name] # pylint: disable=pointless-statement
|
|
121
|
+
else:
|
|
122
|
+
if not tools.config["test_enable"]:
|
|
123
|
+
# If running live, log proper error
|
|
124
|
+
# so that techies can track it down
|
|
125
|
+
_logger.warning(
|
|
126
|
+
"%(model)s.%(fname)s not available",
|
|
127
|
+
{"model": self._name, "fname": field_name},
|
|
128
|
+
)
|
|
129
|
+
raise SwallableException()
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def _jsonify_record_handle_function(self, rec, field_dict, strict):
|
|
133
|
+
field_name = field_dict["name"]
|
|
134
|
+
function = field_dict["function"]
|
|
135
|
+
try:
|
|
136
|
+
return self._function_value(rec, function, field_name)
|
|
137
|
+
except UserError as err:
|
|
138
|
+
if strict:
|
|
139
|
+
raise
|
|
140
|
+
if not tools.config["test_enable"]:
|
|
141
|
+
_logger.error(
|
|
142
|
+
"%(model)s.%(func)s not available",
|
|
143
|
+
{"model": self._name, "func": str(function)},
|
|
144
|
+
)
|
|
145
|
+
raise SwallableException() from err
|
|
146
|
+
|
|
147
|
+
def _jsonify_record_handle_subparser(self, rec, field_dict, strict, subparser):
|
|
148
|
+
field_name = field_dict["name"]
|
|
149
|
+
field = rec._fields[field_name]
|
|
150
|
+
if not (field.relational or field.type == "reference"):
|
|
151
|
+
if strict:
|
|
152
|
+
self._jsonify_bad_parser_error(field_name)
|
|
153
|
+
if not tools.config["test_enable"]:
|
|
154
|
+
_logger.error(
|
|
155
|
+
"%(model)s.%(fname)s not relational",
|
|
156
|
+
{"model": self._name, "fname": field_name},
|
|
157
|
+
)
|
|
158
|
+
raise SwallableException()
|
|
159
|
+
value = [self._jsonify_record(subparser, r, {}) for r in rec[field_name]]
|
|
160
|
+
|
|
161
|
+
if field.type in ("many2one", "reference"):
|
|
162
|
+
value = value[0] if value else None
|
|
163
|
+
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def _jsonify_record_handle_resolver(self, rec, field, resolver, json_key):
|
|
167
|
+
value = rec._jsonify_value(field, rec[field.name])
|
|
168
|
+
value = resolver.resolve(field, rec)[0] if resolver else value
|
|
169
|
+
if isinstance(value, dict) and "_json_key" in value and "_value" in value:
|
|
170
|
+
# Allow override of json_key.
|
|
171
|
+
# In this case,
|
|
172
|
+
# the final value must be encapsulated into _value key
|
|
173
|
+
value, json_key = value["_value"], value["_json_key"]
|
|
174
|
+
return value, json_key
|
|
175
|
+
|
|
176
|
+
def jsonify(self, parser, one=False, with_fieldname=False):
|
|
177
|
+
"""Convert the record according to the given parser.
|
|
178
|
+
|
|
179
|
+
Example of (simple) parser:
|
|
180
|
+
parser = [
|
|
181
|
+
'name',
|
|
182
|
+
'number',
|
|
183
|
+
'create_date',
|
|
184
|
+
('partner_id', ['id', 'display_name', 'ref'])
|
|
185
|
+
('shipping_id', callable)
|
|
186
|
+
('delivery_id', "record_method")
|
|
187
|
+
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
In order to be consistent with the Odoo API the jsonify method always
|
|
191
|
+
returns a list of objects even if there is only one element in input.
|
|
192
|
+
You can change this behavior by passing `one=True` to get only one element.
|
|
193
|
+
|
|
194
|
+
By default the key into the JSON is the name of the field extracted
|
|
195
|
+
from the model. If you need to specify an alternate name to use as
|
|
196
|
+
key, you can define your mapping as follow into the parser definition:
|
|
197
|
+
|
|
198
|
+
parser = [
|
|
199
|
+
'field_name:json_key'
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
if one:
|
|
204
|
+
self.ensure_one()
|
|
205
|
+
if isinstance(parser, list):
|
|
206
|
+
parser = convert_simple_to_full_parser(parser)
|
|
207
|
+
resolver = parser.get("resolver")
|
|
208
|
+
if isinstance(resolver, int):
|
|
209
|
+
# cached versions of the parser are stored as integer
|
|
210
|
+
resolver = self.env["ir.exports.resolver"].browse(resolver)
|
|
211
|
+
results = [{} for record in self]
|
|
212
|
+
parsers = {False: parser["fields"]} if "fields" in parser else parser["langs"]
|
|
213
|
+
for lang in parsers:
|
|
214
|
+
translate = lang or parser.get("language_agnostic")
|
|
215
|
+
new_ctx = {}
|
|
216
|
+
if translate:
|
|
217
|
+
new_ctx["lang"] = lang
|
|
218
|
+
if with_fieldname:
|
|
219
|
+
new_ctx["with_fieldname"] = True
|
|
220
|
+
records = self.with_context(**new_ctx) if new_ctx else self
|
|
221
|
+
for record, json in zip(records, results, strict=False):
|
|
222
|
+
self._jsonify_record(parsers[lang], record, json)
|
|
223
|
+
|
|
224
|
+
if resolver:
|
|
225
|
+
results = resolver.resolve(results, self)
|
|
226
|
+
return results[0] if one else results
|
|
227
|
+
|
|
228
|
+
# HELPERS
|
|
229
|
+
|
|
230
|
+
def _jsonify_m2o_to_id(self, fname):
|
|
231
|
+
"""Helper to get an ID only from a m2o field.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
|
|
235
|
+
<field name="name">m2o_id</field>
|
|
236
|
+
<field name="target">m2o_id:rel_id</field>
|
|
237
|
+
<field name="instance_method_name">_jsonify_m2o_to_id</field>
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
return self[fname].id
|
|
241
|
+
|
|
242
|
+
def _jsonify_x2m_to_ids(self, fname):
|
|
243
|
+
"""Helper to get a list of IDs only from a o2m or m2m field.
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
|
|
247
|
+
<field name="name">m2m_ids</field>
|
|
248
|
+
<field name="target">m2m_ids:rel_ids</field>
|
|
249
|
+
<field name="instance_method_name">_jsonify_x2m_to_ids</field>
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
return self[fname].ids
|
|
253
|
+
|
|
254
|
+
def _jsonify_format_duration(self, fname):
|
|
255
|
+
"""Helper to format a Float-like duration to string 00:00.
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
|
|
259
|
+
<field name="name">duration</field>
|
|
260
|
+
<field name="instance_method_name">_jsonify_format_duration</field>
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
return format_duration(self[fname])
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
def convert_simple_to_full_parser(parser):
|
|
2
|
+
"""Convert a simple API style parser to a full parser"""
|
|
3
|
+
assert isinstance(parser, list)
|
|
4
|
+
return {"fields": _convert_parser(parser)}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _convert_field(fld, function=None):
|
|
8
|
+
"""Return a dict from the string encoding a field to export.
|
|
9
|
+
The : is used as a separator to specify a target, if any.
|
|
10
|
+
"""
|
|
11
|
+
name, sep, target = fld.partition(":")
|
|
12
|
+
field_dict = {"name": name}
|
|
13
|
+
if target:
|
|
14
|
+
field_dict["target"] = target
|
|
15
|
+
if function:
|
|
16
|
+
field_dict["function"] = function
|
|
17
|
+
return field_dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _convert_parser(parser):
|
|
21
|
+
"""Recursively process each list to replace encoded fields as string
|
|
22
|
+
by dicts specifying each attribute by its relevant key.
|
|
23
|
+
"""
|
|
24
|
+
result = []
|
|
25
|
+
for line in parser:
|
|
26
|
+
if isinstance(line, str):
|
|
27
|
+
field_def = _convert_field(line)
|
|
28
|
+
else:
|
|
29
|
+
fld, sub = line
|
|
30
|
+
if callable(sub) or isinstance(sub, str):
|
|
31
|
+
field_def = _convert_field(fld, sub)
|
|
32
|
+
else:
|
|
33
|
+
field_def = (_convert_field(fld), _convert_parser(sub))
|
|
34
|
+
result.append(field_def)
|
|
35
|
+
return result
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- BEAU Sébastien \<<sebastien.beau@akretion.com>\>
|
|
2
|
+
- Raphaël Reverdy \<<raphael.reverdy@akretion.com>\>
|
|
3
|
+
- Laurent Mignon \<<laurent.mignon@acsone.eu>\>
|
|
4
|
+
- Nans Lefebvre \<<nans.lefebvre@acsone.eu>\>
|
|
5
|
+
- Simone Orsi \<<simone.orsi@camptocamp.com>\>
|
|
6
|
+
- Iván Todorovich \<<ivan.todorovich@camptocamp.com>\>
|
|
7
|
+
- Nguyen Minh Chien \<<chien@trobz.com>\>
|
|
8
|
+
- Thien Vo \<<thienvh@trobz.com>\>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
This module adds a 'jsonify' method to every model of the ORM. It works
|
|
2
|
+
on the current recordset and requires a single argument 'parser' that
|
|
3
|
+
specify the field to extract.
|
|
4
|
+
|
|
5
|
+
Example of a simple parser:
|
|
6
|
+
|
|
7
|
+
``` python
|
|
8
|
+
parser = [
|
|
9
|
+
'name',
|
|
10
|
+
'number',
|
|
11
|
+
'create_date',
|
|
12
|
+
('partner_id', ['id', 'display_name', 'ref'])
|
|
13
|
+
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
|
|
14
|
+
]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In order to be consistent with the Odoo API the jsonify method always
|
|
18
|
+
returns a list of objects even if there is only one element in the
|
|
19
|
+
recordset.
|
|
20
|
+
|
|
21
|
+
By default the key into the JSON is the name of the field extracted from
|
|
22
|
+
the model. If you need to specify an alternate name to use as key, you
|
|
23
|
+
can define your mapping as follow into the parser definition:
|
|
24
|
+
|
|
25
|
+
``` python
|
|
26
|
+
parser = [
|
|
27
|
+
'field_name:json_key'
|
|
28
|
+
]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
``` python
|
|
32
|
+
parser = [
|
|
33
|
+
'name',
|
|
34
|
+
'number',
|
|
35
|
+
'create_date:creationDate',
|
|
36
|
+
('partner_id:partners', ['id', 'display_name', 'ref'])
|
|
37
|
+
('line_id:lines', ['id', ('product_id', ['name']), 'price_unit'])
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If you need to parse the value of a field in a custom way, you can pass
|
|
42
|
+
a callable or the name of a method on the model:
|
|
43
|
+
|
|
44
|
+
``` python
|
|
45
|
+
parser = [
|
|
46
|
+
('name', "jsonify_name") # method name
|
|
47
|
+
('number', lambda rec, field_name: rec[field_name] * 2)) # callable
|
|
48
|
+
]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Also the module provide a method "get_json_parser" on the ir.exports
|
|
52
|
+
object that generate a parser from an ir.exports configuration.
|
|
53
|
+
|
|
54
|
+
Further features are available for advanced uses. It defines a simple
|
|
55
|
+
"resolver" model that has a "python_code" field and a resolve function
|
|
56
|
+
so that arbitrary functions can be configured to transform fields, or
|
|
57
|
+
process the resulting dictionary. It is also to specify a lang to
|
|
58
|
+
extract the translation of any given field.
|
|
59
|
+
|
|
60
|
+
To use these features, a full parser follows the following structure:
|
|
61
|
+
|
|
62
|
+
``` python
|
|
63
|
+
parser = {
|
|
64
|
+
"resolver": 3,
|
|
65
|
+
"language_agnostic": True,
|
|
66
|
+
"langs": {
|
|
67
|
+
False: [
|
|
68
|
+
{'name': 'description'},
|
|
69
|
+
{'name': 'number', 'resolver': 5},
|
|
70
|
+
({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}])
|
|
71
|
+
],
|
|
72
|
+
'fr_FR': [
|
|
73
|
+
{'name': 'description', 'target': 'descriptions_fr'},
|
|
74
|
+
({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}])
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
One would get a result having this structure (note that the translated
|
|
81
|
+
fields are merged in the same dictionary):
|
|
82
|
+
|
|
83
|
+
``` python
|
|
84
|
+
exported_json == {
|
|
85
|
+
"description": "English description",
|
|
86
|
+
"description_fr": "French description, voilà",
|
|
87
|
+
"number": 42,
|
|
88
|
+
"partner": {
|
|
89
|
+
"display_name": "partner name",
|
|
90
|
+
"description_fr": "French description of that partner",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note that a resolver can be passed either as a recordset or as an id, so
|
|
96
|
+
as to be fully serializable. A slightly simpler version in case the
|
|
97
|
+
translation of fields is not needed, but other features like custom
|
|
98
|
+
resolvers are:
|
|
99
|
+
|
|
100
|
+
``` python
|
|
101
|
+
parser = {
|
|
102
|
+
"resolver": 3,
|
|
103
|
+
"fields": [
|
|
104
|
+
{'name': 'description'},
|
|
105
|
+
{'name': 'number', 'resolver': 5},
|
|
106
|
+
({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]),
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
By passing the fields key instead of langs, we have essentially the same
|
|
112
|
+
behaviour as simple parsers, with the added benefit of being able to use
|
|
113
|
+
resolvers.
|
|
114
|
+
|
|
115
|
+
Standard use-cases of resolvers are: - give field-specific defaults
|
|
116
|
+
(e.g. "" instead of None) - cast a field type (e.g. int()) - alias a
|
|
117
|
+
particular field for a specific export - ...
|
|
118
|
+
|
|
119
|
+
A simple parser is simply translated into a full parser at export.
|
|
120
|
+
|
|
121
|
+
If the global resolver is given, then the json_dict goes through:
|
|
122
|
+
|
|
123
|
+
``` python
|
|
124
|
+
resolver.resolve(dict, record)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Which allows to add external data from the context or transform the
|
|
128
|
+
dictionary if necessary. Similarly if given for a field the resolver
|
|
129
|
+
evaluates the result.
|
|
130
|
+
|
|
131
|
+
It is possible for a target to have a marshaller by ending the target
|
|
132
|
+
with '=list': in that case the result is put into a list.
|
|
133
|
+
|
|
134
|
+
``` python
|
|
135
|
+
parser = {
|
|
136
|
+
fields: [
|
|
137
|
+
{'name': 'name'},
|
|
138
|
+
{'name': 'field_1', 'target': 'customTags=list'},
|
|
139
|
+
{'name': 'field_2', 'target': 'customTags=list'},
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Would result in the following JSON structure:
|
|
145
|
+
|
|
146
|
+
``` python
|
|
147
|
+
{
|
|
148
|
+
'name': 'record_name',
|
|
149
|
+
'customTags': ['field_1_value', 'field_2_value'],
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The intended use-case is to be compatible with APIs that require all
|
|
154
|
+
translated parameters to be exported simultaneously, and ask for custom
|
|
155
|
+
properties to be put in a sub-dictionary. Since it is often the case
|
|
156
|
+
that some of these requirements are optional, new requirements could be
|
|
157
|
+
met without needing to add field or change any code.
|
|
158
|
+
|
|
159
|
+
Note that the export values with the simple parser depends on the
|
|
160
|
+
record's lang; this is in contrast with full parsers which are designed
|
|
161
|
+
to be language agnostic.
|
|
162
|
+
|
|
163
|
+
NOTE: this module was named base_jsonify till version 14.0.1.5.0.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## with_fieldname parameter
|
|
2
|
+
|
|
3
|
+
The with_fieldname option of jsonify() method, when true, will inject on
|
|
4
|
+
the same level of the data "\_fieldname_\$field" keys that will
|
|
5
|
+
contain the field name, in the language of the current user.
|
|
6
|
+
|
|
7
|
+
> Examples of with_fieldname usage:
|
|
8
|
+
|
|
9
|
+
``` python
|
|
10
|
+
# example 1
|
|
11
|
+
parser = [('name')]
|
|
12
|
+
a.jsonify(parser=parser)
|
|
13
|
+
[{'name': 'SO3996'}]
|
|
14
|
+
>>> a.jsonify(parser=parser, with_fieldname=False)
|
|
15
|
+
[{'name': 'SO3996'}]
|
|
16
|
+
>>> a.jsonify(parser=parser, with_fieldname=True)
|
|
17
|
+
[{'fieldname_name': 'Order Reference', 'name': 'SO3996'}}]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# example 2 - with a subparser-
|
|
21
|
+
parser=['name', 'create_date', ('order_line', ['id' , 'product_uom', 'is_expense'])]
|
|
22
|
+
>>> a.jsonify(parser=parser, with_fieldname=False)
|
|
23
|
+
[{'name': 'SO3996', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'order_line': [{'id': 16649, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16651, 'product_uom': 'stuks', 'is_expense': False}, {'id': 16650, 'product_uom': 'stuks', 'is_expense': False}]}]
|
|
24
|
+
>>> a.jsonify(parser=parser, with_fieldname=True)
|
|
25
|
+
[{'fieldname_name': 'Order Reference', 'name': 'SO3996', 'fieldname_create_date': 'Creation Date', 'create_date': '2015-06-02T12:18:26.279909+00:00', 'fieldname_order_line': 'Order Lines', 'order_line': [{'fieldname_id': 'ID', 'id': 16649, 'fieldname_product_uom': 'Unit of Measure', 'product_uom': 'stuks', 'fieldname_is_expense': 'Is expense', 'is_expense': False}]}]
|
|
26
|
+
```
|