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", ]); } );