[FIX] report_xlsx: refactor controller and report hamdler
parent
aec96ed82c
commit
087cfe0021
|
@ -109,6 +109,7 @@ Contributors
|
||||||
* Cristian Salamea <cs@prisehub.com>
|
* Cristian Salamea <cs@prisehub.com>
|
||||||
* Rod Schouteden <rod.schouteden@dynapps.be>
|
* Rod Schouteden <rod.schouteden@dynapps.be>
|
||||||
* Eugene Molotov <molotov@it-projects.info>
|
* Eugene Molotov <molotov@it-projects.info>
|
||||||
|
* Christopher Ormaza <chris.ormaza@forgeflow.com>
|
||||||
|
|
||||||
Maintainers
|
Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
# Copyright (C) 2017 Creu Blanca
|
# Copyright (C) 2017 Creu Blanca
|
||||||
|
# Copyright 2021 ForgeFlow S.L.
|
||||||
# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html).
|
# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html).
|
||||||
|
|
||||||
import json
|
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 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
|
from odoo.addons.web.controllers import main as report
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ReportController(report.ReportController):
|
class ReportController(report.ReportController):
|
||||||
@route()
|
@route()
|
||||||
def report_routes(self, reportname, docids=None, converter=None, **data):
|
def report_routes(self, reportname, docids=None, converter=None, **data):
|
||||||
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)
|
report = request.env["ir.actions.report"]._get_report_from_name(reportname)
|
||||||
context = dict(request.env.context)
|
context = dict(request.env.context)
|
||||||
if docids:
|
if docids:
|
||||||
|
@ -28,18 +31,10 @@ class ReportController(report.ReportController):
|
||||||
if data.get("options"):
|
if data.get("options"):
|
||||||
data.update(json.loads(data.pop("options")))
|
data.update(json.loads(data.pop("options")))
|
||||||
if data.get("context"):
|
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"])
|
data["context"] = json.loads(data["context"])
|
||||||
if data["context"].get("lang"):
|
|
||||||
del data["context"]["lang"]
|
|
||||||
context.update(data["context"])
|
context.update(data["context"])
|
||||||
|
if converter == "xlsx":
|
||||||
xlsx = report.with_context(**context)._render_xlsx(docids, data=data)[0]
|
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 = [
|
xlsxhttpheaders = [
|
||||||
(
|
(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
|
@ -47,10 +42,62 @@ class ReportController(report.ReportController):
|
||||||
"officedocument.spreadsheetml.sheet",
|
"officedocument.spreadsheetml.sheet",
|
||||||
),
|
),
|
||||||
("Content-Length", len(xlsx)),
|
("Content-Length", len(xlsx)),
|
||||||
("Content-Disposition", content_disposition(report_name + ".xlsx")),
|
|
||||||
]
|
]
|
||||||
return request.make_response(xlsx, headers=xlsxhttpheaders)
|
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:
|
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}
|
error = {"code": 200, "message": "Odoo Server Error", "data": se}
|
||||||
return request.make_response(html_escape(json.dumps(error)))
|
return request.make_response(html_escape(json.dumps(error)))
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
* Cristian Salamea <cs@prisehub.com>
|
* Cristian Salamea <cs@prisehub.com>
|
||||||
* Rod Schouteden <rod.schouteden@dynapps.be>
|
* Rod Schouteden <rod.schouteden@dynapps.be>
|
||||||
* Eugene Molotov <molotov@it-projects.info>
|
* Eugene Molotov <molotov@it-projects.info>
|
||||||
|
* Christopher Ormaza <chris.ormaza@forgeflow.com>
|
||||||
|
|
|
@ -47,7 +47,7 @@ try:
|
||||||
# Only up to 100 duplicates
|
# Only up to 100 duplicates
|
||||||
deduplicated_secuence = "~{:02d}".format(duplicated_secuence + 1)
|
deduplicated_secuence = "~{:02d}".format(duplicated_secuence + 1)
|
||||||
if duplicated_secuence > 99:
|
if duplicated_secuence > 99:
|
||||||
raise xlsxwriter.exceptions.DuplicateWorksheetName
|
raise xlsxwriter.exceptions.DuplicateWorksheetName # noqa: B904
|
||||||
if duplicated_secuence:
|
if duplicated_secuence:
|
||||||
sheetname = re.sub(pattern, deduplicated_secuence, sheetname)
|
sheetname = re.sub(pattern, deduplicated_secuence, sheetname)
|
||||||
elif len(sheetname) <= 28:
|
elif len(sheetname) <= 28:
|
||||||
|
|
|
@ -1,95 +1,53 @@
|
||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import {download} from "@web/core/network/download";
|
import {download} from "@web/core/network/download";
|
||||||
import {registry} from "@web/core/registry";
|
import {registry} from "@web/core/registry";
|
||||||
|
|
||||||
import core from "web.core";
|
registry
|
||||||
import framework from "web.framework";
|
.category("ir.actions.report handlers")
|
||||||
import session from "web.session";
|
.add("xlsx_handler", async function (action, options, env) {
|
||||||
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(",");
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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") {
|
if (action.report_type === "xlsx") {
|
||||||
return self._triggerDownload(action, options, "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 {
|
||||||
|
if (actionContext.active_ids) {
|
||||||
|
url += `/${actionContext.active_ids.join(",")}`;
|
||||||
}
|
}
|
||||||
return this._super.apply(this, arguments);
|
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 Promise.resolve(true);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue