/** @odoo-module **/ import { registry } from "@web/core/registry"; import { _t } from "@web/core/l10n/translation"; import { session } from "@web/session"; import { rpc } from "@web/core/network/rpc"; import { user } from "@web/core/user"; import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl"; import { Dialog } from "@web/core/dialog/dialog"; /** * Fire-and-forget audit log for PDF preview/print/download actions. */ async function logPreviewAction(reportName, actionType, recordIds, modelName) { try { await rpc("/fusion_pdf_preview/log_action", { report_name: reportName, action_type: actionType, record_ids: recordIds || '', model_name: modelName || '', }); } catch (err) { console.warn("Failed to log PDF preview action:", err); } } export class PDFViewerDialog extends Component { setup() { this.state = useState({ isLoading: true, viewerUrl: this.getViewerUrl(), isMaximized: false }); this._onKeyDown = this._onKeyDown.bind(this); onMounted(() => { document.addEventListener('keydown', this._onKeyDown); }); onWillUnmount(() => { document.removeEventListener('keydown', this._onKeyDown); }); } getViewerUrl() { const baseUrl = '/fusion_pdf_preview/static/lib/pdfjs/web/viewer.html'; return `${baseUrl}?file=${this.props.url}`; } onIframeLoad() { this.state.isLoading = false; } toggle() { this.state.isMaximized = !this.state.isMaximized; } getDialogSize() { if (this.state.isMaximized) { return 'fullscreen'; } return 'xl'; } getFrameStyle() { if (this.state.isMaximized) { return 'height: calc(98vh - 141px) !important;'; } return 'height: calc(90vh - 100px) !important;'; } downloadPDF() { const link = document.createElement('a'); link.href = this.props.url; link.download = this.props.title ? `${this.props.title}.pdf` : 'document.pdf'; document.body.appendChild(link); link.click(); document.body.removeChild(link); if (this.props.reportName) { logPreviewAction(this.props.reportName, 'download', this.props.recordIds, this.props.modelName); } } openInNewTab() { window.open(this.props.url, '_blank'); } printPDF() { try { const iframe = this.el?.querySelector('iframe'); if (iframe && iframe.contentWindow) { iframe.contentWindow.print(); } } catch (err) { window.open(this.props.url, '_blank'); } if (this.props.reportName) { logPreviewAction(this.props.reportName, 'print', this.props.recordIds, this.props.modelName); } } _onKeyDown(ev) { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlOrCmd = isMac ? ev.metaKey : ev.ctrlKey; // F key (no modifiers) - toggle fullscreen if ((ev.key === 'f' || ev.key === 'F') && !ctrlOrCmd && !ev.altKey && !ev.shiftKey) { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) { return; } ev.preventDefault(); this.toggle(); } // Ctrl+P / Cmd+P - print if (ctrlOrCmd && (ev.key === 'p' || ev.key === 'P')) { ev.preventDefault(); this.printPDF(); } // Ctrl+D / Cmd+D - download if (ctrlOrCmd && (ev.key === 'd' || ev.key === 'D')) { ev.preventDefault(); this.downloadPDF(); } } } PDFViewerDialog.template = 'fusion_pdf_preview.PDFViewerDialog'; PDFViewerDialog.components = { Dialog }; registry.category("dialog").add("PDFViewerDialog", PDFViewerDialog); export function openPDFViewer(env, url, title = "PDF Document", meta = {}) { const dialog = env.services.dialog; return dialog.add(PDFViewerDialog, { url: url, title: title, reportName: meta.reportName || '', recordIds: meta.recordIds || '', modelName: meta.modelName || '', }); } /** * Helper function to handle automatic printing * @param {string} url - URL of the PDF to print * @param {Object} env - Environment object for notifications */ function handleAutomaticPrinting(url, env) { const printFrame = document.createElement('iframe'); printFrame.style.display = 'none'; printFrame.src = url; printFrame.onload = function() { try { printFrame.contentWindow.print(); } catch (err) { env.services.notification.add( _t("Failed to print automatically. Please check your browser settings."), { type: 'warning', sticky: true, title: _t("Printing Error"), } ); document.body.removeChild(printFrame); } }; const cleanup = () => { if (printFrame.parentNode) { document.body.removeChild(printFrame); } window.removeEventListener('focus', cleanup); }; window.addEventListener('focus', cleanup); document.body.appendChild(printFrame); } /** * Generates the report url given a report action. * * @private * @param {ReportAction} action * @param {Object} env * @param {string} filename * @returns {string} */ function _getReportUrl(action, env, filename) { let url = `/report/pdf/${action.report_name}`; const actionContext = action.context || {}; filename = filename || action.name; if (filename !== undefined) { filename = filename.replace(/[/?%#&=]/g, "_") + ".pdf"; } if (action.data && JSON.stringify(action.data) !== "{}") { const options = encodeURIComponent(JSON.stringify(action.data)); const context = encodeURIComponent(JSON.stringify(actionContext)); url += `?filename=${filename}&options=${options}&context=${context}&`; } else { if (actionContext.active_ids) { url += `/${actionContext.active_ids.join(",")}?filename=${filename}&context=${encodeURIComponent(JSON.stringify(user.context))}&`; } } return url; } async function FusionPdfPreview(action, options, env) { const link = '

wkhtmltopdf.org'; 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." ), }; if (action.report_type === "qweb-pdf" && env.services.menu.getCurrentApp() !== undefined) { const userWantsPreview = session.preview_print; const userWantsAutoPrint = session.automatic_printing; const result = await rpc("/fusion_pdf_preview/get_report_name", { report_name: action.report_name, data: JSON.stringify(action.context) }); const state = result["wkhtmltopdf_state"]; const previewMode = result["fusion_preview_mode"] || "default"; if (state in WKHTMLTOPDF_MESSAGES) { env.services.notification.add(WKHTMLTOPDF_MESSAGES[state], { sticky: true, title: _t("Report"), }); } if (state === "upgrade" || state === "ok") { // Determine effective behavior based on report-level override let shouldPreview = userWantsPreview; let shouldAutoPrint = userWantsAutoPrint; if (previewMode === 'preview') { shouldPreview = true; shouldAutoPrint = false; } else if (previewMode === 'download') { return false; } else if (previewMode === 'auto_print') { shouldPreview = false; shouldAutoPrint = true; } if (!shouldPreview && !shouldAutoPrint) { return false; } const url = _getReportUrl(action, env, result["file_name"]); const actionContext = action.context || {}; const meta = { reportName: action.report_name, recordIds: (actionContext.active_ids || []).join(','), modelName: actionContext.active_model || '', }; if (shouldPreview) { openPDFViewer(env, url, action.name, meta); logPreviewAction(meta.reportName, 'preview', meta.recordIds, meta.modelName); } if (shouldAutoPrint) { handleAutomaticPrinting(url, env); logPreviewAction(meta.reportName, 'print', meta.recordIds, meta.modelName); } return true; } } } registry .category("ir.actions.report handlers") .add("fusion_pdf_preview", FusionPdfPreview);