From bc9203ab0b522b7546eadaea2d92ce3f5a90279b Mon Sep 17 00:00:00 2001 From: Christopher Ormaza Date: Tue, 7 Dec 2021 14:07:53 -0500 Subject: [PATCH] [FIX] report_xlsx: refactor controller and report hamdler --- report_xlsx/README.rst | 3 +- report_xlsx/__manifest__.py | 8 +- report_xlsx/controllers/main.py | 105 ++++++++++---- report_xlsx/readme/CONTRIBUTORS.rst | 1 + report_xlsx/report/report_abstract_xlsx.py | 2 +- .../js/report/action_manager_report.esm.js | 130 ++++++------------ 6 files changed, 128 insertions(+), 121 deletions(-) diff --git a/report_xlsx/README.rst b/report_xlsx/README.rst index 1973f28c2..4b91a387b 100644 --- a/report_xlsx/README.rst +++ b/report_xlsx/README.rst @@ -23,7 +23,7 @@ Base report xlsx :target: https://runbot.odoo-community.org/runbot/143/14.0 :alt: Try me on Runbot -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module provides a basic report class to generate xlsx report. @@ -109,6 +109,7 @@ Contributors * Cristian Salamea * Rod Schouteden * Eugene Molotov +* Christopher Ormaza Maintainers ~~~~~~~~~~~ diff --git a/report_xlsx/__manifest__.py b/report_xlsx/__manifest__.py index 93492ba8f..f0063d224 100644 --- a/report_xlsx/__manifest__.py +++ b/report_xlsx/__manifest__.py @@ -14,8 +14,8 @@ "demo": ["demo/report.xml"], "installable": True, "assets": { - "web.assets_backend": [ - "report_xlsx/static/src/js/report/action_manager_report.esm.js", - ], - }, + "web.assets_backend": [ + "report_xlsx/static/src/js/report/action_manager_report.esm.js", + ], + }, } diff --git a/report_xlsx/controllers/main.py b/report_xlsx/controllers/main.py index 4ffb1919a..9f68a0f9f 100644 --- a/report_xlsx/controllers/main.py +++ b/report_xlsx/controllers/main.py @@ -1,45 +1,40 @@ # Copyright (C) 2017 Creu Blanca +# Copyright 2021 ForgeFlow S.L. # License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). import json +import logging -from odoo.http import content_disposition, request, route, serialize_exception +from werkzeug.urls import url_decode + +from odoo.http import ( + content_disposition, + request, + route, + serialize_exception as _serialize_exception, +) from odoo.tools import html_escape -from odoo.tools.safe_eval import safe_eval +from odoo.tools.safe_eval import safe_eval, time from odoo.addons.web.controllers import main as report +_logger = logging.getLogger(__name__) + class ReportController(report.ReportController): @route() def report_routes(self, reportname, docids=None, converter=None, **data): + 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"): + data["context"] = json.loads(data["context"]) + context.update(data["context"]) if converter == "xlsx": - return self._report_routes_xlsx(reportname, docids, converter, **data) - return super(ReportController, self).report_routes( - reportname, docids, converter, **data - ) - - def _report_routes_xlsx(self, reportname, docids=None, converter=None, **data): - try: - 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"]) xlsx = report.with_context(**context)._render_xlsx(docids, data=data)[0] - report_name = report.report_file - if report.print_report_name and not len(docids) > 1: - obj = request.env[report.model].browse(docids[0]) - report_name = safe_eval(report.print_report_name, {"object": obj}) xlsxhttpheaders = [ ( "Content-Type", @@ -47,10 +42,62 @@ class ReportController(report.ReportController): "officedocument.spreadsheetml.sheet", ), ("Content-Length", len(xlsx)), - ("Content-Disposition", content_disposition(report_name + ".xlsx")), ] return request.make_response(xlsx, headers=xlsxhttpheaders) + return super(ReportController, self).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] + try: + if report_type == "xlsx": + reportname = url.split("/report/xlsx/")[1].split("?")[0] + docids = None + if "/" in reportname: + reportname, docids = reportname.split("/") + if docids: + # Generic report: + response = self.report_routes( + reportname, docids=docids, converter="xlsx", context=context + ) + else: + # Particular report: + data = dict( + url_decode(url.split("?")[1]).items() + ) # decoding the args represented in JSON + if "context" in data: + context, data_context = json.loads(context or "{}"), json.loads( + data.pop("context") + ) + context = json.dumps({**context, **data_context}) + response = self.report_routes( + reportname, converter="xlsx", context=context, **data + ) + + report = request.env["ir.actions.report"]._get_report_from_name( + reportname + ) + filename = "%s.%s" % (report.name, "xlsx") + + if docids: + 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 = "%s.%s" % (report_name, "xlsx") + response.headers.add( + "Content-Disposition", content_disposition(filename) + ) + return response + else: + return super(ReportController, self).report_download(data, context) except Exception as e: - se = serialize_exception(e) + _logger.exception("Error while generating report %s", 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_xlsx/readme/CONTRIBUTORS.rst b/report_xlsx/readme/CONTRIBUTORS.rst index 8e49a2cb5..527206a7c 100644 --- a/report_xlsx/readme/CONTRIBUTORS.rst +++ b/report_xlsx/readme/CONTRIBUTORS.rst @@ -6,3 +6,4 @@ * Cristian Salamea * Rod Schouteden * Eugene Molotov +* Christopher Ormaza diff --git a/report_xlsx/report/report_abstract_xlsx.py b/report_xlsx/report/report_abstract_xlsx.py index 9190dd5b4..af7ce658f 100644 --- a/report_xlsx/report/report_abstract_xlsx.py +++ b/report_xlsx/report/report_abstract_xlsx.py @@ -47,7 +47,7 @@ try: # Only up to 100 duplicates deduplicated_secuence = "~{:02d}".format(duplicated_secuence + 1) if duplicated_secuence > 99: - raise xlsxwriter.exceptions.DuplicateWorksheetName + raise xlsxwriter.exceptions.DuplicateWorksheetName # noqa: B904 if duplicated_secuence: sheetname = re.sub(pattern, deduplicated_secuence, sheetname) elif len(sheetname) <= 28: diff --git a/report_xlsx/static/src/js/report/action_manager_report.esm.js b/report_xlsx/static/src/js/report/action_manager_report.esm.js index 38c69512f..df85df04b 100644 --- a/report_xlsx/static/src/js/report/action_manager_report.esm.js +++ b/report_xlsx/static/src/js/report/action_manager_report.esm.js @@ -1,95 +1,53 @@ /** @odoo-module **/ + import {download} from "@web/core/network/download"; import {registry} from "@web/core/registry"; - import core from "web.core"; - import framework from "web.framework"; - import session from "web.session"; - var _t = core._t; - - - async function _downloadReportXLSX(url, actions) { - var self = this; - framework.blockUI(); - var type = "xlsx"; - var cloned_action = _.clone(actions); - var new_url = url; - - if ( - _.isUndefined(cloned_action.data) || - _.isNull(cloned_action.data) || - (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data)) - ) { - if (cloned_action.context.active_ids) { - new_url += "/" + cloned_action.context.active_ids.join(","); - } +registry + .category("ir.actions.report handlers") + .add("xlsx_handler", async function (action, options, env) { + if (action.report_type === "xlsx") { + const type = action.report_type; + 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 action_options = encodeURIComponent(JSON.stringify(action.data)); + const context = encodeURIComponent(JSON.stringify(actionContext)); + url += `?options=${action_options}&context=${context}`; } else { - new_url += - "?options=" + - encodeURIComponent(JSON.stringify(cloned_action.data)); - new_url += - "&context=" + - encodeURIComponent(JSON.stringify(cloned_action.context)); - } - - return new Promise(function (resolve, reject) { - var blocked = !session.get_file({ - url: new_url, - data: { - data: JSON.stringify([new_url, type]), - }, - success: resolve, - error: (error) => { - self.call("crash_manager", "rpc_error", error); - reject(); - }, - complete: framework.unblockUI, - }); - if (blocked) { - // AAB: this check should be done in get_file service directly, - // should not be the concern of the caller (and that way, get_file - // could return a deferred) - var message = _t( - "A popup window with your report was blocked. You " + - "may need to change your browser settings to allow " + - "popup windows for this page." - ); - this.do_warn(_t("Warning"), message, true); + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; } - }); - } - - async function _triggerDownload (action, options, type) { - var self = this; - var reportUrls = this._makeReportUrls(action); - if (type === "xlsx") { - return this._downloadReportXLSX(reportUrls[type], action).then( - function () { - if (action.close_on_report_download) { - var closeAction = {type: "ir.actions.act_window_close"}; - return self.doAction( - closeAction, - _.pick(options, "on_close") - ); - } - return options.on_close(); - } + if (type === "xlsx") { + const context = encodeURIComponent( + JSON.stringify(env.services.user.context) + ); + url += `?context=${context}`; + } + } + 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(); } - return this._super.apply(this, arguments); } - - async function _makeReportUrls (action) { - var reportUrls = this._super.apply(this, arguments); - reportUrls.xlsx = "/report/xlsx/" + action.report_name; - return reportUrls; - } - - async function _executeReportAction (action, options) { - var self = this; - if (action.report_type === "xlsx") { - return self._triggerDownload(action, options, "xlsx"); - } - return this._super.apply(this, arguments); - } - + return Promise.resolve(true); + });