This commit is contained in:
gsinghpal
2026-03-13 12:38:28 -04:00
parent db4b9aa278
commit fc3c966484
2975 changed files with 1614 additions and 498 deletions

View File

@@ -5,9 +5,27 @@ 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 } from "@odoo/owl";
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({
@@ -15,10 +33,20 @@ export class PDFViewerDialog extends Component {
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 = '/pdf_print_preview/static/lib/pdfjs/web/viewer.html';
const baseUrl = '/fusion_pdf_preview/static/lib/pdfjs/web/viewer.html';
return `${baseUrl}?file=${this.props.url}`;
}
@@ -43,20 +71,83 @@ export class PDFViewerDialog extends Component {
}
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 = 'pdf_print_preview.PDFViewerDialog';
PDFViewerDialog.template = 'fusion_pdf_preview.PDFViewerDialog';
PDFViewerDialog.components = { Dialog };
// Register for use in actions
registry.category("dialog").add("PDFViewerDialog", PDFViewerDialog);
export function openPDFViewer(env, url, title = "PDF Document") {
export function openPDFViewer(env, url, title = "PDF Document", meta = {}) {
const dialog = env.services.dialog;
return dialog.add(PDFViewerDialog, {
url: url,
title: title
title: title,
reportName: meta.reportName || '',
recordIds: meta.recordIds || '',
modelName: meta.modelName || '',
});
}
@@ -70,7 +161,7 @@ function handleAutomaticPrinting(url, env) {
const printFrame = document.createElement('iframe');
printFrame.style.display = 'none';
printFrame.src = url;
printFrame.onload = function() {
try {
printFrame.contentWindow.print();
@@ -88,12 +179,14 @@ function handleAutomaticPrinting(url, env) {
};
const cleanup = () => {
document.body.removeChild(printFrame);
if (printFrame.parentNode) {
document.body.removeChild(printFrame);
}
window.removeEventListener('focus', cleanup);
};
window.addEventListener('focus', cleanup);
document.body.appendChild(printFrame);
}
@@ -103,15 +196,17 @@ function handleAutomaticPrinting(url, env) {
*
* @private
* @param {ReportAction} action
* @param {env} env
* @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)
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));
@@ -125,7 +220,7 @@ function _getReportUrl(action, env, filename) {
return url;
}
async function PdfPrintPreview(action, options, env) {
async function FusionPdfPreview(action, options, env) {
const link = '<br><br><a href="http://wkhtmltopdf.org/" target="_blank">wkhtmltopdf.org</a>';
const WKHTMLTOPDF_MESSAGES = {
broken:
@@ -149,15 +244,17 @@ async function PdfPrintPreview(action, options, env) {
),
};
if (action.report_type === "qweb-pdf" && env.services.menu.getCurrentApp() !== undefined && (session.preview_print || session.automatic_printing)) {
let getReportResult = rpc("/pdf_print_preview/get_report_name", {
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 result = await getReportResult;
const state = result["wkhtmltopdf_state"];
const previewMode = result["fusion_preview_mode"] || "default";
// display a notification according to wkhtmltopdf's state
if (state in WKHTMLTOPDF_MESSAGES) {
env.services.notification.add(WKHTMLTOPDF_MESSAGES[state], {
sticky: true,
@@ -166,14 +263,40 @@ async function PdfPrintPreview(action, options, env) {
}
if (state === "upgrade" || state === "ok") {
let url = _getReportUrl(action, env, result["file_name"]);
if(session.preview_print) {
// PreviewDialog.createPreviewDialog(self, url, action.name);
openPDFViewer(env, url, action.name);
// 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 (session.automatic_printing) {
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;
}
@@ -182,4 +305,4 @@ async function PdfPrintPreview(action, options, env) {
registry
.category("ir.actions.report handlers")
.add("pdf_print_preview", PdfPrintPreview);
.add("fusion_pdf_preview", FusionPdfPreview);

View File

@@ -13,7 +13,7 @@ function reportPreviewConfigItem(env) {
description: _t("Report preview"),
callback: async function () {
const actionDescription = await rpc("/web/action/load", {
action_id: "pdf_print_preview.action_short_preview_print"
action_id: "fusion_pdf_preview.action_short_preview_print"
});
actionDescription.res_id = user.userId;
env.services.action.doAction(actionDescription);

View File

@@ -1,15 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="pdf_print_preview.PDFViewerDialog">
<t t-name="fusion_pdf_preview.PDFViewerDialog">
<Dialog size="getDialogSize()" footer="false">
<t t-set-slot="header">
<div class="d-flex align-items-center justify-content-between flex w-100">
<div style="width: 90px"></div>
<div style="width: 135px"></div>
<h4 class="modal-title text-break fw-normal">
<b><t t-esc="props.title" /></b>
</h4>
<div class="d-flex align-items-center" style="width: 45px">
<button type="button" class="btn" t-on-click="toggle">
<div class="d-flex align-items-center" style="width: 135px; justify-content: flex-end;">
<button type="button" class="btn" t-on-click="downloadPDF" title="Download (Ctrl+D)">
<i class="fa fa-download"/>
</button>
<button type="button" class="btn" t-on-click="openInNewTab" title="Open in new tab">
<i class="fa fa-external-link"/>
</button>
<button type="button" class="btn" t-on-click="toggle" title="Toggle fullscreen (F)">
<i t-if="!state.isMaximized" class="fa fa-square-o" />
<i t-else="" class="fa fa-clone" />
</button>
@@ -17,17 +23,17 @@
</div>
</div>
</t>
<div class="o_pdf_viewer position-relative">
<!-- Loading spinner -->
<div t-if="state.isLoading"
class="position-absolute w-100 h-100 d-flex justify-content-center align-items-center"
<div t-if="state.isLoading"
class="position-absolute w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index: 1;">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- PDF.js viewer iframe -->
<iframe t-att-src="state.viewerUrl"
class="w-100 border-0"