Skip to content

Commit 8421fe5

Browse files
committed
[MIG] json_export_engine: Migration to 18.0
1 parent 1aa69a6 commit 8421fe5

18 files changed

Lines changed: 154 additions & 159 deletions

eslint.config.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ const config = [{
165165

166166
settings: {
167167
jsdoc: {
168+
allowedTags: [
169+
"odoo-module",
170+
],
168171
tagNamePreference: {
169172
arg: "param",
170173
argument: "param",

json_export_engine/__manifest__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
{
66
"name": "JSON Export Engine",
7-
"summary": "Universal JSON schema builder, REST API, webhooks and scheduled exports",
8-
"version": "16.0.1.0.0",
7+
"summary": (
8+
"Universal JSON schema builder, REST API, webhooks and" " scheduled exports"
9+
),
10+
"version": "18.0.1.0.0",
911
"category": "Tools",
1012
"website": "https://github.com/OCA/server-tools",
1113
"author": "kobros-tech, Odoo Community Association (OCA)",

json_export_engine/controllers/main.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def _fetch_paginated_records(
229229
model = request.env[schema.model_name].sudo()
230230
total = model.search_count(domain)
231231

232-
base_path = "/api/json_export/%s" % path.strip("/")
232+
base_path = f"/api/json_export/{path.strip('/')}"
233233
preserved_qs = self._build_preserved_query_string(params)
234234

235235
if endpoint.paginate:
@@ -251,17 +251,15 @@ def _fetch_paginated_records(
251251
)
252252

253253
nav = {
254-
"first": "%s?page=1%s" % (base_path, preserved_qs),
255-
"last": "%s?page=%d%s" % (base_path, total_pages, preserved_qs),
254+
"first": f"{base_path}?page=1{preserved_qs}",
255+
"last": f"{base_path}?page={total_pages}{preserved_qs}",
256256
"next": (
257-
"%s?page=%d%s" % (base_path, page + 1, preserved_qs)
257+
f"{base_path}?page={page + 1}{preserved_qs}"
258258
if page < total_pages
259259
else None
260260
),
261261
"prev": (
262-
"%s?page=%d%s" % (base_path, page - 1, preserved_qs)
263-
if page > 1
264-
else None
262+
f"{base_path}?page={page - 1}{preserved_qs}" if page > 1 else None
265263
),
266264
}
267265
else:

json_export_engine/data/ir_cron.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
<field name="code">model.autovacuum()</field>
88
<field name="interval_number">1</field>
99
<field name="interval_type">days</field>
10-
<field name="numbercall">-1</field>
1110
<field name="active" eval="False" />
12-
<field name="doall" eval="False" />
1311
</record>
1412
</odoo>

json_export_engine/models/json_export_endpoint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ def _compute_full_url(self):
120120
for rec in self:
121121
if rec.route_path:
122122
path = rec.route_path.strip("/")
123-
rec.full_url = "%s/api/json_export/%s" % (base_url, path)
124-
rec.schema_url = "%s/api/json_export/%s/schema" % (base_url, path)
123+
rec.full_url = f"{base_url}/api/json_export/{path}"
124+
rec.schema_url = f"{base_url}/api/json_export/{path}/schema"
125125
else:
126126
rec.full_url = ""
127127
rec.schema_url = ""

json_export_engine/models/json_export_schedule.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,16 @@ def _create_or_update_cron(self):
114114
"""Create or update the ir.cron record for this schedule."""
115115
self.ensure_one()
116116
cron_vals = {
117-
"name": "JSON Export: %s" % self.name,
117+
"name": f"JSON Export: {self.name}",
118118
"model_id": self.env["ir.model"]
119119
.sudo()
120120
.search([("model", "=", self._name)], limit=1)
121121
.id,
122122
"state": "code",
123-
"code": "model._cron_run_export(%d)" % self.id,
123+
"code": f"model._cron_run_export({self.id})",
124124
"interval_number": self.interval_number,
125125
"interval_type": self.interval_type,
126-
"numbercall": -1,
127126
"active": self.active,
128-
"doall": False,
129127
}
130128
if self.cron_id:
131129
self.cron_id.sudo().write(cron_vals)
@@ -140,7 +138,7 @@ def _cron_run_export(self, schedule_id):
140138
if schedule.exists() and schedule.active:
141139
if schedule.async_export and hasattr(schedule, "with_delay"):
142140
schedule.with_delay(
143-
description="Scheduled Export: %s" % schedule.name,
141+
description=f"Scheduled Export: {schedule.name}",
144142
)._run_scheduled_export()
145143
else:
146144
schedule._run_scheduled_export()
@@ -176,10 +174,9 @@ def _run_scheduled_export(self):
176174

177175
# Deliver
178176
if self.destination_type == "attachment":
179-
filename = "scheduled_%s_%s.%s" % (
180-
schema.model_name.replace(".", "_"),
181-
fields.Datetime.now().strftime("%Y%m%d_%H%M%S"),
182-
ext,
177+
filename = (
178+
f"scheduled_{schema.model_name.replace('.', '_')}_"
179+
f"{fields.Datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"
183180
)
184181
self.env["ir.attachment"].create(
185182
{

json_export_engine/models/json_export_schema.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@ class JsonExportSchema(models.Model):
2828
active = fields.Boolean(default=True)
2929
model_id = fields.Many2one(
3030
"ir.model",
31-
string="Model",
31+
string="Model Reference",
3232
required=True,
3333
ondelete="cascade",
3434
domain=[("transient", "=", False)],
3535
)
3636
model_name = fields.Char(
37-
related="model_id.model", store=True, readonly=True, index=True
37+
string="Model Name",
38+
related="model_id.model",
39+
store=True,
40+
readonly=True,
41+
index=True,
3842
)
3943
exporter_id = fields.Many2one("ir.exports", string="Field Selector")
4044
domain = fields.Char(string="Record Filter", default="[]")
@@ -130,11 +134,9 @@ def _compute_json_schema(self):
130134
def _wrap_api_response_schema(self, record_schema, endpoint=None):
131135
"""Wrap a record-level schema in the full API response envelope."""
132136
nullable_string = {"anyOf": [{"type": "string"}, {"type": "null"}]}
133-
description = (
134-
"%s Supports ?page=N to navigate pages "
135-
"and ?page=last to jump to the last page."
136-
% record_schema.get("description", "")
137-
)
137+
desc = record_schema.get("description", "")
138+
description = f"{desc} Supports ?page=N to navigate pages "
139+
description += "and ?page=last to jump to the last page."
138140
if endpoint:
139141
if endpoint.allow_filtering:
140142
description += (
@@ -152,7 +154,7 @@ def _wrap_api_response_schema(self, record_schema, endpoint=None):
152154
)
153155
return {
154156
"$schema": "http://json-schema.org/draft-07/schema#",
155-
"title": "%s — API Response" % record_schema.get("title", "Export"),
157+
"title": f"{record_schema.get('title', 'Export')} — API Response",
156158
"description": description,
157159
"type": "object",
158160
"required": ["success", "data", "pagination", "meta"],
@@ -249,16 +251,15 @@ def _wrap_api_response_schema(self, record_schema, endpoint=None):
249251
}
250252

251253
def _generate_json_schema(self):
252-
"""Generate a JSON Schema (draft-07) from the resolved parser and model fields."""
254+
"""Generate a JSON Schema from the resolved parser and model fields."""
253255
self.ensure_one()
254256
parser = self._get_parser()
255257
model = self.env[self.model_name]
256258
properties, required = self._parser_to_schema_properties(parser, model)
257259
return {
258260
"$schema": "http://json-schema.org/draft-07/schema#",
259261
"title": self.name,
260-
"description": "Auto-generated schema for %s (%s)"
261-
% (self.name, self.model_name),
262+
"description": f"Auto-generated schema for {self.name} ({self.model_name})",
262263
"type": "object",
263264
"properties": properties,
264265
"required": required,
@@ -383,7 +384,9 @@ def _get_parser(self):
383384
raise UserError(_("Please select a field selector (exporter) first."))
384385
# Remove broken export lines (e.g. name=False) before parsing,
385386
# otherwise jsonifier's get_json_parser() crashes on .split("/")
386-
bad_lines = self.exporter_id.export_fields.filtered(lambda l: not l.name)
387+
bad_lines = self.exporter_id.export_fields.filtered(
388+
lambda field: not field.name
389+
)
387390
if bad_lines:
388391
bad_lines.unlink()
389392
raw_parser = self.exporter_id.get_json_parser()
@@ -471,9 +474,9 @@ def _build_filter_domain(self, params, allowed_fields):
471474
raw_value = params[key]
472475

473476
if field_name not in allowed_fields:
474-
raise ValueError("Filtering on field '%s' is not allowed." % field_name)
477+
raise ValueError(f"Filtering on field '{field_name}' is not allowed.")
475478
if operator not in self.FILTER_OPERATORS:
476-
raise ValueError("Unknown filter operator '%s'." % operator)
479+
raise ValueError(f"Unknown filter operator '{operator}'.")
477480

478481
odoo_op = self.FILTER_OPERATORS[operator]
479482
value = self._coerce_filter_value(field_name, operator, raw_value)
@@ -507,18 +510,18 @@ def _coerce_single_value(field_type, raw):
507510
try:
508511
return int(raw)
509512
except (ValueError, TypeError) as err:
510-
raise ValueError("Expected integer value, got '%s'." % raw) from err
513+
raise ValueError(f"Expected integer value, got '{raw}'.") from err
511514
elif field_type in ("float", "monetary"):
512515
try:
513516
return float(raw)
514517
except (ValueError, TypeError) as err:
515-
raise ValueError("Expected numeric value, got '%s'." % raw) from err
518+
raise ValueError(f"Expected numeric value, got '{raw}'.") from err
516519
elif field_type == "boolean":
517520
if raw.lower() in ("true", "1", "yes"):
518521
return True
519522
elif raw.lower() in ("false", "0", "no"):
520523
return False
521-
raise ValueError("Expected boolean value, got '%s'." % raw)
524+
raise ValueError(f"Expected boolean value, got '{raw}'.")
522525
return raw
523526

524527
def _build_sort_order(self, sort_param, allowed_fields):
@@ -541,8 +544,8 @@ def _build_sort_order(self, sort_param, allowed_fields):
541544
field_name = token
542545
direction = "asc"
543546
if field_name not in allowed_fields:
544-
raise ValueError("Sorting on field '%s' is not allowed." % field_name)
545-
parts.append("%s %s" % (field_name, direction))
547+
raise ValueError(f"Sorting on field '{field_name}' is not allowed.")
548+
parts.append(f"{field_name} {direction}")
546549
return ", ".join(parts)
547550

548551
def _filter_parser(self, fields_param):
@@ -558,7 +561,7 @@ def _filter_parser(self, fields_param):
558561
invalid = requested - allowed
559562
if invalid:
560563
raise ValueError(
561-
"Field selection on '%s' is not allowed." % "', '".join(sorted(invalid))
564+
f"Field selection on '{', '.join(sorted(invalid))}' is not allowed."
562565
)
563566
full_parser = self._get_parser()
564567
filtered = []
@@ -583,9 +586,9 @@ def action_export_json(self):
583586
records = self._get_records()
584587
data = self._serialize_records(records)
585588
content = json.dumps(data, indent=2, ensure_ascii=False)
586-
filename = "export_%s_%s.json" % (
587-
self.model_name.replace(".", "_"),
588-
fields.Datetime.now().strftime("%Y%m%d_%H%M%S"),
589+
filename = (
590+
f"export_{self.model_name.replace('.', '_')}_"
591+
f"{fields.Datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
589592
)
590593
attachment = self.env["ir.attachment"].create(
591594
{
@@ -601,7 +604,7 @@ def action_export_json(self):
601604
self._create_log("manual", "success", len(records), duration)
602605
return {
603606
"type": "ir.actions.act_url",
604-
"url": "/web/content/%s?download=true" % attachment.id,
607+
"url": f"/web/content/{attachment.id}?download=true",
605608
"target": "new",
606609
}
607610
except Exception as e:
@@ -616,7 +619,7 @@ def action_view_logs(self):
616619
"type": "ir.actions.act_window",
617620
"name": _("Export Logs"),
618621
"res_model": "json.export.log",
619-
"view_mode": "tree,form",
622+
"view_mode": "list,form",
620623
"domain": [("schema_id", "=", self.id)],
621624
"context": {"default_schema_id": self.id},
622625
}

json_export_engine/models/json_export_webhook.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def action_reset_state(self):
6969
@api.model
7070
def _fire_for_model(self, model_name, event_type, records):
7171
"""Find matching webhooks and fire them for the given model and event."""
72-
event_field = "on_%s" % event_type
72+
event_field = f"on_{event_type}"
7373
webhooks = self.search(
7474
[
7575
("active", "=", True),
@@ -115,7 +115,7 @@ def _trigger_webhook(self, event_type, records):
115115
# Async delivery via queue_job when available
116116
if self.async_delivery and hasattr(self, "with_delay"):
117117
self.with_delay(
118-
description="Webhook: %s (%s)" % (self.name, event_type),
118+
description=f"Webhook: {self.name} ({event_type})",
119119
)._send_payload(payload, delivery_id=delivery_id)
120120
duration = int((time.time() - start_time) * 1000)
121121
self.sudo().write(
@@ -168,7 +168,7 @@ def _trigger_webhook(self, event_type, records):
168168
self.sudo().write(
169169
{
170170
"last_call_date": fields.Datetime.now(),
171-
"last_call_status": "error: %s" % str(e)[:200],
171+
"last_call_status": f"error: {str(e)[:200]}",
172172
"state": "error",
173173
}
174174
)

json_export_engine/static/src/json_export_widget.esm.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
1-
/** @odoo-module **/
1+
/** @odoo-module */
22

33
/* Copyright 2026 KOBROS-TECH LTD (https://kobros-tech.com).
44
@author Mohamed Alkobrosli <mohamed@kobros-tech.com>
55
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
66

7-
import {ExportDataDialog} from "@web/views/view_dialogs/export_data_dialog";
8-
import {Many2OneField} from "@web/views/fields/many2one/many2one_field";
7+
import {Many2OneField, many2OneField} from "@web/views/fields/many2one/many2one_field";
8+
import {_t} from "@web/core/l10n/translation";
9+
import {rpc} from "@web/core/network/rpc";
910
import {registry} from "@web/core/registry";
1011
import {useService} from "@web/core/utils/hooks";
11-
12-
const {onWillDestroy} = owl;
12+
import {ExportDataDialog} from "@web/views/view_dialogs/export_data_dialog";
1313

1414
class JsonExportDialog extends ExportDataDialog {
1515
setup() {
1616
super.setup();
1717
Object.assign(this.state, {
1818
showApplyTemplateButton: false,
1919
});
20-
this.title = this.env._t("Select Fields for JSON Export");
21-
// Swap the model from props to load the correct export fields
22-
// for the schema's target model, not the schema model itself.
23-
this.swapResModel = this.props.root.resModel;
24-
this.props.root.resModel = this.props.context.resModel;
20+
this.title = _t("Select Fields for JSON Export");
2521
if (this.props.context.exporter_id && this.props.context.exporter_id[0]) {
2622
this.state.templateId = this.props.context.exporter_id[0];
2723
} else {
2824
this.state.templateId = "new_template";
2925
}
30-
// Restore original model when dialog is destroyed
31-
onWillDestroy(() => {
32-
this.props.root.resModel = this.swapResModel;
33-
});
3426
}
3527

3628
async onChangeExportList(ev) {
@@ -105,7 +97,6 @@ JsonExportDialog.template = "json_export_engine.JsonExportDialog";
10597
class JsonExportFieldSelector extends Many2OneField {
10698
setup() {
10799
super.setup();
108-
this.rpc = useService("rpc");
109100
this.orm = useService("orm");
110101
this.dialogService = useService("dialog");
111102
this.quickOverlap = (templ) => {
@@ -121,9 +112,11 @@ class JsonExportFieldSelector extends Many2OneField {
121112
}
122113

123114
async getExportedFields(model, import_compat, parentParams) {
124-
return await this.rpc("/web/export/get_fields", {
115+
const domain = [];
116+
return await rpc("/web/export/get_fields", {
125117
...parentParams,
126-
model,
118+
model: this.props.record.data.model_name,
119+
domain,
127120
import_compat,
128121
});
129122
}
@@ -154,8 +147,10 @@ class JsonExportFieldSelector extends Many2OneField {
154147

155148
JsonExportFieldSelector.template = "json_export_engine.JsonExportFieldSelector";
156149
JsonExportFieldSelector.supportedTypes = ["many2one"];
157-
JsonExportFieldSelector.fieldDependencies = {
158-
model_name: {type: "char"},
150+
151+
export const jsonExportFieldSelector = {
152+
...many2OneField,
153+
component: JsonExportFieldSelector,
159154
};
160155

161-
registry.category("fields").add("json_export_field_selector", JsonExportFieldSelector);
156+
registry.category("fields").add("json_export_field_selector", jsonExportFieldSelector);

json_export_engine/tests/test_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def setUpClass(cls):
139139

140140
def _get(self, path, headers=None):
141141
"""Helper to perform a GET request."""
142-
url = "/api/json_export/%s" % path
142+
url = f"/api/json_export/{path}"
143143
return self.url_open(url, headers=headers or {})
144144

145145
# -- No auth tests --

0 commit comments

Comments
 (0)