From db4b11141d1581ebf1cd5b401801e66e77748040 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 4 May 2023 11:03:16 +0000 Subject: [PATCH] [REF] point_of_sale, web: factor report downloads out of action service Previously, downloading reports was only possible by going through the action service. In a few places, we want to download reports from the PoS, but it doesn't make sense to load the action service and all its dependencies in the PoS. In order to allow reports to be downloaded without the action services, some functions are extracted out of the action service itself and into report utils, which the action service calls. These utils can now be used from the PoS without requiring the action service and its dependencies be included in the PoS assets. Part-of: odoo/odoo#120070 --- .../static/src/app/report_service.js | 33 +++++ .../static/src/js/Popups/ClosePosPopup.js | 10 +- .../js/Screens/PaymentScreen/PaymentScreen.js | 10 +- .../ControlButtons/InvoiceButton.js | 12 +- .../src/webclient/actions/action_service.js | 132 ++++-------------- .../src/webclient/actions/reports/utils.js | 83 +++++++++++ .../actions/reports/report_action_tests.js | 7 +- 7 files changed, 162 insertions(+), 125 deletions(-) create mode 100644 addons/point_of_sale/static/src/app/report_service.js create mode 100644 addons/web/static/src/webclient/actions/reports/utils.js diff --git a/addons/point_of_sale/static/src/app/report_service.js b/addons/point_of_sale/static/src/app/report_service.js new file mode 100644 index 00000000000..74a1d5ffd30 --- /dev/null +++ b/addons/point_of_sale/static/src/app/report_service.js @@ -0,0 +1,33 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { downloadReport } from "@web/webclient/actions/reports/utils"; + +export const reportService = { + dependencies: ["rpc", "user", "ui"], + start(env, { rpc, user, ui }) { + const reportActionsCache = {}; + return { + async download(reportXmlId, active_ids) { + ui.block(); + try { + reportActionsCache[reportXmlId] ||= rpc("/web/action/load", { + action_id: reportXmlId, + }); + const reportAction = await reportActionsCache[reportXmlId]; + // await instead of return because we want the ui to stay blocked + await downloadReport( + rpc, + { ...reportAction, context: { active_ids } }, + "pdf", + user.context + ); + } finally { + ui.unblock(); + } + }, + }; + }, +}; + +registry.category("services").add("report", reportService); diff --git a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js index 9b71ac54f38..2873844aba1 100644 --- a/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ClosePosPopup.js @@ -23,7 +23,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { this.pos = usePos(); this.popup = useService("popup"); this.orm = useService("orm"); - this.action = useService("action"); + this.report = useService("report"); this.hardwareProxy = useService("hardware_proxy"); this.manualInputCashCount = false; this.cashControl = this.pos.globalState.config.cash_control; @@ -92,11 +92,9 @@ export class ClosePosPopup extends AbstractAwaitablePopup { } } async downloadSalesReport() { - await this.action.doAction("point_of_sale.sale_details_report", { - additionalContext: { - active_ids: [this.pos.globalState.pos_session.id], - }, - }); + return this.report.download("point_of_sale.sale_details_report", [ + this.pos.globalState.pos_session.id, + ]); } handleInputChange(paymentId) { let expectedAmount; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 8584024e16c..5631e38c3b8 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -31,7 +31,7 @@ export class PaymentScreen extends Component { this.pos = usePos(); this.orm = useService("orm"); this.popup = useService("popup"); - this.action = useService("action"); + this.report = useService("report"); this.notification = useService("pos_notification"); this.hardwareProxy = useService("hardware_proxy"); this.payment_methods_from_config = this.env.pos.payment_methods.filter((method) => @@ -235,11 +235,9 @@ export class PaymentScreen extends Component { // 2. Invoice. if (this.currentOrder.is_to_invoice()) { if (syncOrderResult.length) { - await this.action.doAction("account.account_invoices", { - additionalContext: { - active_ids: [syncOrderResult[0].account_move], - }, - }); + await this.report.download("account.account_invoices", [ + syncOrderResult[0].account_move, + ]); } else { throw { code: 401, diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js index 2e6c84cd1ac..ab2af422bd8 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js @@ -15,7 +15,7 @@ export class InvoiceButton extends Component { this.invoiceButton = useRef("invoice-button"); this.popup = useService("popup"); this.orm = useService("orm"); - this.action = useService("action"); + this.report = useService("report"); } get isAlreadyInvoiced() { if (!this.props.order) { @@ -38,12 +38,10 @@ export class InvoiceButton extends Component { ["account_move"], { load: false } ); - if (orderWithInvoice && orderWithInvoice.account_move) { - await this.action.doAction("account.account_invoices", { - additionalContext: { - active_ids: [orderWithInvoice.account_move], - }, - }); + if (orderWithInvoice?.account_move) { + await this.report.download("account.account_invoices", [ + orderWithInvoice.account_move, + ]); } } catch (error) { if (error instanceof Error) { diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index e2692a5f3ed..81634a11774 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -3,7 +3,6 @@ import { browser } from "@web/core/browser/browser"; import { makeContext } from "@web/core/context"; import { useDebugCategory } from "@web/core/debug/debug_context"; -import { download } from "@web/core/network/download"; import { evaluateExpr } from "@web/core/py_js/py"; import { registry } from "@web/core/registry"; import { KeepLast } from "@web/core/utils/concurrency"; @@ -26,6 +25,7 @@ import { xml, reactive, } from "@odoo/owl"; +import { downloadReport, getReportUrl } from "./reports/utils"; const actionHandlersRegistry = registry.category("action_handlers"); const actionRegistry = registry.category("actions"); @@ -819,8 +819,9 @@ function makeActionManager(env) { */ function _executeActURLAction(action, options) { let url = action.url; - if (url && !(url.startsWith('http') || url.startsWith('/'))) - url = '/' + url; + if (url && !(url.startsWith("http") || url.startsWith("/"))) { + url = "/" + url; + } if (action.target === "self") { let willUnload = false; const onUnload = () => { @@ -984,92 +985,6 @@ function makeActionManager(env) { // ir.actions.report // --------------------------------------------------------------------------- - // messages that might be shown to the user dependening on the state of wkhtmltopdf - const link = '

wkhtmltopdf.org'; - const WKHTMLTOPDF_MESSAGES = { - broken: - env._t( - "Your installation of Wkhtmltopdf seems to be broken. The report will be shown " + - "in html." - ) + link, - install: - env._t( - "Unable to find Wkhtmltopdf on this system. The report will be shown in " + "html." - ) + link, - upgrade: - env._t( - "You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to " + - "get a correct display of headers and footers as well as support for " + - "table-breaking between pages." - ) + link, - workers: env._t( - "You need to start Odoo with at least two workers to print a pdf version of " + - "the reports." - ), - }; - - // only check the wkhtmltopdf state once, so keep the rpc promise - let wkhtmltopdfStateProm; - - /** - * Generates the report url given a report action. - * - * @private - * @param {ReportAction} action - * @param {ReportType} type - * @returns {string} - */ - function _getReportUrl(action, 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 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 === "html") { - const context = encodeURIComponent(JSON.stringify(env.services.user.context)); - url += `?context=${context}`; - } - } - return url; - } - - /** - * Launches download action of the report - * - * @private - * @param {ReportAction} action - * @param {ActionOptions} options - * @returns {Promise} - */ - async function _triggerDownload(action, options, type) { - const url = _getReportUrl(action, type); - 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 doAction({ type: "ir.actions.act_window_close" }, { onClose }); - } else if (onClose) { - onClose(); - } - } - function _executeReportClientAction(action, options) { const props = Object.assign({}, options.props, { data: action.data, @@ -1077,7 +992,7 @@ function makeActionManager(env) { name: action.name, report_file: action.report_file, report_name: action.report_name, - report_url: _getReportUrl(action, "html"), + report_url: getReportUrl(action, "html", env.services.user.context), context: Object.assign({}, action.context), }); @@ -1112,28 +1027,35 @@ function makeActionManager(env) { } if (action.report_type === "qweb-html") { return _executeReportClientAction(action, options); - } else if (action.report_type === "qweb-pdf") { - // check the state of wkhtmltopdf before proceeding - if (!wkhtmltopdfStateProm) { - wkhtmltopdfStateProm = env.services.rpc("/report/check_wkhtmltopdf"); + } else if (action.report_type === "qweb-pdf" || action.report_type === "qweb-text") { + const type = action.report_type.slice(5); + let success, message; + env.services.ui.block(); + try { + ({ success, message } = await downloadReport( + env.services.rpc, + action, + type, + env.services.user.context + )); + } finally { + env.services.ui.unblock(); } - const state = await wkhtmltopdfStateProm; - // display a notification according to wkhtmltopdf's state - if (state in WKHTMLTOPDF_MESSAGES) { - env.services.notification.add(WKHTMLTOPDF_MESSAGES[state], { + if (message) { + env.services.notification.add(message, { sticky: true, title: env._t("Report"), }); } - if (state === "upgrade" || state === "ok") { - // trigger the download of the PDF report - return _triggerDownload(action, options, "pdf"); - } else { - // open the report in the client action if generating the PDF is not possible + if (!success) { return _executeReportClientAction(action, options); } - } else if (action.report_type === "qweb-text") { - return _triggerDownload(action, options, "text"); + const { onClose } = options; + if (action.close_on_report_download) { + return doAction({ type: "ir.actions.act_window_close" }, { onClose }); + } else if (onClose) { + onClose(); + } } else { console.error( `The ActionManager can't handle reports of type ${action.report_type}`, diff --git a/addons/web/static/src/webclient/actions/reports/utils.js b/addons/web/static/src/webclient/actions/reports/utils.js new file mode 100644 index 00000000000..2d8af86e264 --- /dev/null +++ b/addons/web/static/src/webclient/actions/reports/utils.js @@ -0,0 +1,83 @@ +/** @odoo-module */ + +import { _t } from "@web/core/l10n/translation"; +import { download } from "@web/core/network/download"; + +/** + * Generates the report url given a report action. + * + * @param {Object} action the report action + * @param {"text"|"qweb"|"html"} type the type of the report + * @param {Object} userContext the user context + * @returns {string} + */ +export function getReportUrl(action, type, userContext) { + 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 === "html") { + const context = encodeURIComponent(JSON.stringify(userContext)); + url += `?context=${context}`; + } + } + return url; +} + +// messages that might be shown to the user dependening on the state of wkhtmltopdf +const link = '

wkhtmltopdf.org'; // FIXME missing markup +const WKHTMLTOPDF_MESSAGES = { + broken: + _t( + "Your installation of Wkhtmltopdf seems to be broken. The report will be shown in html." + ) + link, + install: + _t("Unable to find Wkhtmltopdf on this system. The report will be shown in html.") + link, + upgrade: + _t( + "You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as support for table-breaking between pages." + ) + link, + workers: _t( + "You need to start Odoo with at least two workers to print a pdf version of the reports." + ), +}; + +/** + * Launches download action of the report + * + * @param {Function} rpc a function to perform RPCs + * @param {Object} action the report action + * @param {"pdf"|"text"} type the type of the report to download + * @param {Object} userContext the user context + * @returns {Promise<{success: boolean, message?: string}>} + */ +export async function downloadReport(rpc, action, type, userContext) { + let message; + if (type === "pdf") { + // Cache the wkhtml status on the function. In prod this means is only + // checked once, but we can reset it between tests to test multiple statuses. + downloadReport.wkhtmltopdfStatusProm ||= rpc("/report/check_wkhtmltopdf"); + const status = await downloadReport.wkhtmltopdfStatusProm; + message = WKHTMLTOPDF_MESSAGES[status]; + if (!["upgrade", "ok"].includes(status)) { + return { success: false, message }; + } + } + const url = getReportUrl(action, type); + await download({ + url: "/report/download", + data: { + data: JSON.stringify([url, action.report_type]), + context: JSON.stringify(userContext), + }, + }); + return { success: true, message }; +} diff --git a/addons/web/static/tests/webclient/actions/reports/report_action_tests.js b/addons/web/static/tests/webclient/actions/reports/report_action_tests.js index 0b4f101a2cc..1b4f62ef12b 100644 --- a/addons/web/static/tests/webclient/actions/reports/report_action_tests.js +++ b/addons/web/static/tests/webclient/actions/reports/report_action_tests.js @@ -12,6 +12,8 @@ import { doAction, getActionManagerServerData, } from "@web/../tests/webclient/helpers"; +import { downloadReport } from "@web/webclient/actions/reports/utils"; +import { registerCleanup } from "../../../helpers/cleanup"; let serverData; let target; @@ -23,6 +25,9 @@ QUnit.module("ActionManager", (hooks) => { serverData = getActionManagerServerData(); target = getFixture(); clearRegistryWithCleanup(registry.category("main_components")); + registerCleanup(() => { + delete downloadReport.wkhtmltopdfStatusProm; + }); }); QUnit.module("Report actions"); @@ -111,8 +116,8 @@ QUnit.module("ActionManager", (hooks) => { "/web/webclient/load_menus", "/web/action/load", "/report/check_wkhtmltopdf", - "notify", "/report/download", + "notify", ]); } );