diff --git a/report_xml/__manifest__.py b/report_xml/__manifest__.py index 049ba93d2..c6ceddef5 100644 --- a/report_xml/__manifest__.py +++ b/report_xml/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). { "name": "XML Reports", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "category": "Reporting", "website": "https://github.com/OCA/reporting-engine", "development_status": "Production/Stable", diff --git a/report_xml/controllers/__init__.py b/report_xml/controllers/__init__.py index 1c68c5192..27f759ab0 100644 --- a/report_xml/controllers/__init__.py +++ b/report_xml/controllers/__init__.py @@ -1,3 +1,3 @@ # License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). -from . import main +from . import report diff --git a/report_xml/controllers/main.py b/report_xml/controllers/main.py deleted file mode 100644 index abaa4c767..000000000 --- a/report_xml/controllers/main.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2014-2015 Grupo ESOC -# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). - -import json - -from werkzeug.urls import url_decode - -from odoo.http import content_disposition, request, route, serialize_exception -from odoo.tools import html_escape -from odoo.tools.safe_eval import safe_eval, time - -from odoo.addons.web.controllers import main as report - - -class ReportController(report.ReportController): - @route() - def report_routes(self, reportname, docids=None, converter=None, **data): - if converter == "xml": - report = request.env["ir.actions.report"]._get_report_from_name(reportname) - context = dict(request.env.context) - - if docids: - docids = [int(i) for i in docids.split(",")] - if data.get("options"): - data.update(json.loads(data.pop("options"))) - if data.get("context"): - # Ignore 'lang' here, because the context in data is the one - # from the webclient *but* if the user explicitely wants to - # change the lang, this mechanism overwrites it. - data["context"] = json.loads(data["context"]) - if data["context"].get("lang"): - del data["context"]["lang"] - context.update(data["context"]) - - xml = report.with_context(**context)._render_qweb_xml(docids, data=data)[0] - xmlhttpheaders = [ - ("Content-Type", "text/xml"), - ("Content-Length", len(xml)), - ] - return request.make_response(xml, headers=xmlhttpheaders) - else: - return super().report_routes(reportname, docids, converter, **data) - - @route() - def report_download(self, data, context=None): - requestcontent = json.loads(data) - url, report_type = requestcontent[0], requestcontent[1] - if report_type == "qweb-xml": - try: - reportname = url.split("/report/xml/")[1].split("?")[0] - - docids = None - if "/" in reportname: - reportname, docids = reportname.split("/") - - if docids: - # Generic report: - response = self.report_routes( - reportname, - docids=docids, - converter="xml", - context=context, - ) - else: - # Particular report: - # decoding the args represented in JSON - data = dict(url_decode(url.split("?")[1]).items()) - if "context" in data: - context = json.loads(context or "{}") - data_context = json.loads(data.pop("context")) - context = json.dumps({**context, **data_context}) - response = self.report_routes( - reportname, converter="xml", context=context, **data - ) - - report_obj = request.env["ir.actions.report"] - report = report_obj._get_report_from_name(reportname) - filename = "%s.xml" % (report.name) - - if docids: - ids = [int(doc_id) for doc_id in docids.split(",")] - records = request.env[report.model].browse(ids) - if report.print_report_name and not len(records) > 1: - report_name = safe_eval( - report.print_report_name, {"object": records, "time": time} - ) - filename = "{}.xml".format(report_name) - response.headers.add( - "Content-Disposition", content_disposition(filename) - ) - return response - except Exception as e: - se = serialize_exception(e) - error = {"code": 200, "message": "Odoo Server Error", "data": se} - return request.make_response(html_escape(json.dumps(error))) - else: - return super().report_download(data, context) diff --git a/report_xml/controllers/report.py b/report_xml/controllers/report.py new file mode 100644 index 000000000..0064546c6 --- /dev/null +++ b/report_xml/controllers/report.py @@ -0,0 +1,90 @@ +# Copyright (C) 2014-2015 Grupo ESOC +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json +import logging + +from werkzeug.urls import url_parse + +from odoo.http import content_disposition, request, route, serialize_exception +from odoo.tools import html_escape +from odoo.tools.safe_eval import safe_eval, time + +from odoo.addons.web.controllers import report + +_logger = logging.getLogger(__name__) + + +class ReportController(report.ReportController): + @route() + def report_routes( + self, reportname, docids=None, converter=None, options=None, **kwargs + ): + if converter != "xml": + return super().report_routes( + reportname, + docids=docids, + converter=converter, + options=options, + **kwargs, + ) + docids = [int(_id) for _id in (docids or "").split(",")] + data = {**json.loads(options or "{}"), **kwargs} + context = dict(request.env.context) + if "context" in data: + data["context"] = json.loads(data["context"] or "{}") + # Ignore 'lang' here, because the context in data is the one from the + # webclient *but* if the user explicitely wants to change the lang, this + # mechanism overwrites it. + if "lang" in data["context"]: + del data["context"]["lang"] + context.update(data["context"]) + report_Obj = request.env["ir.actions.report"] + xml = report_Obj.with_context(**context)._render_qweb_xml( + reportname, docids, data=data + )[0] + xmlhttpheaders = [("Content-Type", "text/xml"), ("Content-Length", len(xml))] + return request.make_response(xml, headers=xmlhttpheaders) + + @route() + def report_download(self, data, context=None, token=None): + requestcontent = json.loads(data) + url, report_type = requestcontent[0], requestcontent[1] + reportname = "???" + if report_type != "qweb-xml": + return super().report_download(data, context=context, token=token) + try: + reportname = url.split("/report/xml/")[1].split("?")[0] + docids = None + if "/" in reportname: + reportname, docids = reportname.split("/") + report = request.env["ir.actions.report"]._get_report_from_name(reportname) + filename = None + if docids: + response = self.report_routes( + reportname, docids=docids, converter="xml", context=context + ) + ids = [int(x) for x in docids.split(",")] + obj = request.env[report.model].browse(ids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} + ) + filename = f"{report_name}.xml" + else: + data = url_parse(url).decode_query(cls=dict) + if "context" in data: + context = json.loads(context or "{}") + data_context = json.loads(data.pop("context")) + context = json.dumps({**context, **data_context}) + response = self.report_routes( + reportname, converter="xml", context=context, **data + ) + filename = filename or f"{report.name}.xml" + response.headers.add("Content-Disposition", content_disposition(filename)) + return response + except Exception as e: + _logger.exception(f"Error while generating report {reportname}") + se = serialize_exception(e) + error = {"code": 200, "message": "Odoo Server Error", "data": se} + return request.make_response(html_escape(json.dumps(error))) diff --git a/report_xml/models/ir_actions_report.py b/report_xml/models/ir_actions_report.py index 2fb8ec5df..f65a7555e 100644 --- a/report_xml/models/ir_actions_report.py +++ b/report_xml/models/ir_actions_report.py @@ -1,7 +1,7 @@ # Copyright (C) 2014-2015 Grupo ESOC # License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). -from odoo import fields, models +from odoo import api, fields, models class IrActionsReport(models.Model): @@ -13,30 +13,25 @@ class IrActionsReport(models.Model): xsd_schema = fields.Binary( string="XSD Validation Schema", attachment=True, - help=( - "File with XSD Schema for checking content of result report. " - "Can be empty if validation is not required." - ), + help="File with XSD Schema for checking content of result report. Can be empty " + "if validation is not required.", ) xml_encoding = fields.Selection( selection=[ ("UTF-8", "UTF-8") # will be used as default even if nothing is selected ], string="XML Encoding", - help=( - "Encoding for XML reports. If nothing is selected, " - "then UTF-8 will be applied." - ), + help="Encoding for XML reports. If nothing is selected, then UTF-8 will be " + "applied.", ) xml_declaration = fields.Boolean( string="XML Declaration", - help=( - """Add `` at the start """ - """of final report file.""" - ), + help='Add `` at the start of final report ' + "file.", ) - def _render_qweb_xml(self, docids, data=None): + @api.model + def _render_qweb_xml(self, report_ref, res_ids, data=None): """ Call `generate_report` method of report abstract class `report.` or of standard class for XML report @@ -50,15 +45,12 @@ class IrActionsReport(models.Model): * str - result content of report * str - type of result content """ - report_model_name = "report.{}".format(self.report_name) - - report_model = self.env.get(report_model_name) - if report_model is None: - report_model = self.env["report.report_xml.abstract"] - - content, ttype = report_model.generate_report( - ir_report=self, # will be used to get settings of report - docids=docids, - data=data, + report = self._get_report(report_ref) + report_model = self.env.get( + f"report.{report.report_name}", self.env["report.report_xml.abstract"] + ) + return report_model.generate_report( + ir_report=report, # will be used to get settings of report + docids=res_ids, + data=data or {}, ) - return content, ttype diff --git a/report_xml/reports/report_report_xml_abstract.py b/report_xml/reports/report_report_xml_abstract.py index 8875b9cb4..a691051c6 100644 --- a/report_xml/reports/report_report_xml_abstract.py +++ b/report_xml/reports/report_report_xml_abstract.py @@ -45,10 +45,9 @@ class ReportXmlAbstract(models.AbstractModel): * Default encoding is `UTF-8` """ # collect variable for rendering environment - if not data: - data = {} + data = data or {} data.setdefault("report_type", "text") - data = ir_report._get_rendering_context(docids, data) + data = ir_report._get_rendering_context(ir_report, docids, data) # render template result_bin = ir_report._render_template(ir_report.report_name, data) @@ -56,7 +55,7 @@ class ReportXmlAbstract(models.AbstractModel): # prettify result content # normalize indents parsed_result_bin = minidom.parseString(result_bin) - result = parsed_result_bin.toprettyxml(indent=" " * 4) + result = parsed_result_bin.toprettyxml(indent=" ") # remove empty lines utf8 = "UTF-8" @@ -118,6 +117,4 @@ class ReportXmlAbstract(models.AbstractModel): Returns: * dict - extra variables for report render """ - if not data: - data = {} - return data + return data or {} diff --git a/report_xml/static/src/js/report/action_manager_report.esm.js b/report_xml/static/src/js/report/action_manager_report.esm.js index 3bfe488af..e83200e01 100644 --- a/report_xml/static/src/js/report/action_manager_report.esm.js +++ b/report_xml/static/src/js/report/action_manager_report.esm.js @@ -3,64 +3,47 @@ import {download} from "@web/core/network/download"; import {registry} from "@web/core/registry"; -async function xmlReportHandler(action, options, env) { - if (action.report_type === "qweb-xml") { - // Workaround/hack: Odoo does not expose the _triggerDownload method on - // the service, so we have no way to access it from here. We therefore - // copy the code; as it is private, it doesn't really give any - // stability guarantees anyway - // If _triggerDownload were publically available on the service, the - // code below could be replaced by - // env.services.action._triggerDownload(action, options, "xml"); - - const type = "xml"; - // COPY actionManager._getReportUrl - let url_ = `/report/${type}/${action.report_name}`; - const actionContext = action.context || {}; - if (action.data && JSON.stringify(action.data) !== "{}") { - // Build a query string with `action.data` (it's the place where reports - // using a wizard to customize the output traditionally put their options) - const options_ = encodeURIComponent(JSON.stringify(action.data)); - const context_ = encodeURIComponent(JSON.stringify(actionContext)); - url_ += `?options=${options_}&context=${context_}`; - } else { - if (actionContext.active_ids) { - url_ += `/${actionContext.active_ids.join(",")}`; - } - if (type === "xml") { - const context = encodeURIComponent( - JSON.stringify(env.services.user.context) - ); - url_ += `?context=${context}`; - } - } - // COPY actionManager._triggerDownload - env.services.ui.block(); - try { - await download({ - url: "/report/download", - data: { - data: JSON.stringify([url_, action.report_type]), - context: JSON.stringify(env.services.user.context), - }, - }); - } finally { - env.services.ui.unblock(); - } - const onClose = options.onClose; - if (action.close_on_report_download) { - return env.services.action.doAction( - {type: "ir.actions.act_window_close"}, - {onClose} - ); - } else if (onClose) { - onClose(); - } - // DIFF: need to inform success to the original method. Otherwise it - // will think our hook function did nothing and run the original - // method. - return Promise.resolve(true); +function getReportUrl({report_name, context, data}, env) { + // Rough copy of action_service.js _getReportUrl method. + let url = `/report/xml/${report_name}`; + const actionContext = context || {}; + if (data && JSON.stringify(data) !== "{}") { + const encodedOptions = encodeURIComponent(JSON.stringify(data)); + const encodedContext = encodeURIComponent(JSON.stringify(actionContext)); + return `${url}?options=${encodedOptions}&context=${encodedContext}`; + } + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; + } + const userContext = encodeURIComponent(JSON.stringify(env.services.user.context)); + return `${url}?context=${userContext}`; +} +async function triggerDownload(action, {onClose}, env) { + // Rough copy of action_service.js _triggerDownload method. + env.services.ui.block(); + const data = JSON.stringify([getReportUrl(action, env), action.report_type]); + const context = JSON.stringify(env.services.user.context); + try { + await download({url: "/report/download", data: {data, context}}); + } finally { + env.services.ui.unblock(); + } + if (action.close_on_report_download) { + return env.services.action.doAction( + {type: "ir.actions.act_window_close"}, + {onClose} + ); + } + if (onClose) { + onClose(); } } - -registry.category("ir.actions.report handlers").add("xml_handler", xmlReportHandler); +registry + .category("ir.actions.report handlers") + .add("xml_handler", async function (action, options, env) { + if (action.report_type === "qweb-xml") { + await triggerDownload(action, options, env); + return true; + } + return false; + }); diff --git a/report_xml/tests/test_report_xml.py b/report_xml/tests/test_report_xml.py index b92ed9213..d3b9e552a 100644 --- a/report_xml/tests/test_report_xml.py +++ b/report_xml/tests/test_report_xml.py @@ -13,7 +13,7 @@ class TestXmlReport(common.TransactionCase): report = report_object._get_report_from_name(report_name) docs = self.env["res.company"].search([], limit=1) self.assertEqual(report.report_type, "qweb-xml") - result_report = report._render(docs.ids, {}) + result_report = report_object._render(report_name, docs.ids, {}) result_tree = etree.fromstring(result_report[0]) el = result_tree.xpath("/root/user/name") self.assertEqual(el[0].text, docs.ensure_one().name)