From 101983c7af22b5158a22c20434e943e48b0d384b Mon Sep 17 00:00:00 2001 From: dataCenter430 Date: Mon, 2 Mar 2026 21:45:07 +0100 Subject: [PATCH] feat: add consolidated print backend route Adds a new WWW page at /consolidated_printview that renders multiple documents of the same DocType in a single continuous HTML layout with no forced page breaks between records. - get_context() iterates over a JSON list of names, renders each doc via the existing get_rendered_template() pipeline (falls back to WeasyPrint for beta print-format-builder formats), and joins the results with a thin visual separator - Per-document page-break-after is suppressed via CSS override so all records flow as one uninterrupted printable page - trigger_print=1 fires window.print() automatically, matching the behaviour of the existing single-doc /printview route - Permission and render errors are caught per-document and logged so one bad record does not abort the whole batch - Access log entry recorded for auditing (first 10 doc names) Made-with: Cursor --- frappe/www/consolidated_printview.html | 81 +++++++++++++++++ frappe/www/consolidated_printview.py | 120 +++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 frappe/www/consolidated_printview.html create mode 100644 frappe/www/consolidated_printview.py diff --git a/frappe/www/consolidated_printview.html b/frappe/www/consolidated_printview.html new file mode 100644 index 0000000000..e75c3343a3 --- /dev/null +++ b/frappe/www/consolidated_printview.html @@ -0,0 +1,81 @@ + + + + + + {{ title }} + + {{ include_style('print.bundle.css') }} + {% if print_style %} + + {% endif %} + + + + + + + +{%- if doc_count -%} + +{%- endif -%} + diff --git a/frappe/www/consolidated_printview.py b/frappe/www/consolidated_printview.py new file mode 100644 index 0000000000..9683f6d840 --- /dev/null +++ b/frappe/www/consolidated_printview.py @@ -0,0 +1,120 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import json + +import frappe +from frappe import _ +from frappe.core.doctype.access_log.access_log import make_access_log +from frappe.utils import cint +from frappe.utils.jinja_globals import is_rtl +from frappe.www.printview import ( + get_print_format_doc, + get_print_style, + get_rendered_template, + set_link_titles, + trigger_print_script, +) + +no_cache = 1 + +MAX_CONSOLIDATED_PRINT_DOCS = 100 + + +def get_context(context): + """Build context for consolidated (multi-document, no page-break) print.""" + doctype = frappe.form_dict.get("doctype") + names_json = frappe.form_dict.get("names") + + if not doctype or not names_json: + return { + "body": "

Error

Parameters doctype and names are required.

", + "print_style": "", + "title": "Error", + "lang": "en", + "layout_direction": "ltr", + "doc_count": 0, + } + + try: + names = json.loads(names_json) + except (json.JSONDecodeError, TypeError): + frappe.throw(_("Invalid names parameter: must be a JSON array of document names")) + + if not isinstance(names, list) or len(names) == 0: + frappe.throw(_("At least one document name is required")) + + if len(names) > MAX_CONSOLIDATED_PRINT_DOCS: + frappe.throw( + _("Cannot consolidate more than {0} documents at a time").format(MAX_CONSOLIDATED_PRINT_DOCS) + ) + + format_name = frappe.form_dict.get("format") or None + no_letterhead = frappe.form_dict.get("no_letterhead", 0) + letterhead = frappe.form_dict.get("letterhead") or None + trigger_print = cint(frappe.form_dict.get("trigger_print", 0)) + + meta = frappe.get_meta(doctype) + print_format = get_print_format_doc(format_name, meta=meta) + + all_html_parts = [] + + for name in names: + try: + doc = frappe.get_lazy_doc(doctype, name) + set_link_titles(doc) + html = _get_doc_print_html(doc, print_format, meta, no_letterhead, letterhead) + all_html_parts.append(html) + except frappe.PermissionError: + frappe.log_error( + title="Consolidated Print Permission Error", + message=f"No permission to print {doctype} {name}", + ) + except Exception: + frappe.log_error( + title="Consolidated Print Render Error", + message=f"Error rendering {doctype} {name}", + ) + + separator = '' + body = separator.join( + f'
{html}
' for html in all_html_parts + ) + + if trigger_print: + body += trigger_print_script + + make_access_log( + doctype=doctype, + document=", ".join(names[:10]), + file_type="PDF", + method="Print", + page=f"Consolidated Print Format: {getattr(print_format, 'name', 'Standard')} ({len(names)} docs)", + ) + + return { + "body": body, + "print_style": get_print_style(frappe.form_dict.get("style"), print_format), + "title": f"Consolidated Print — {doctype}", + "lang": frappe.local.lang, + "layout_direction": "rtl" if is_rtl() else "ltr", + "doctype": doctype, + "doc_count": len(all_html_parts), + } + + +def _get_doc_print_html(doc, print_format, meta, no_letterhead, letterhead): + """Return rendered HTML for a single document, handling both standard and beta print formats.""" + if print_format and print_format.get("print_format_builder_beta"): + from frappe.utils.weasyprint import get_html + + return get_html(doctype=doc.doctype, name=doc.name, print_format=print_format.name) + + return get_rendered_template( + doc=doc, + print_format=print_format, + meta=meta, + no_letterhead=no_letterhead, + letterhead=letterhead, + trigger_print=False, + )