diff --git a/frappe/__init__.py b/frappe/__init__.py index 3c4c37303f..52c3a4b218 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -11,7 +11,6 @@ be used to build database driven apps. Read the documentation: https://frappeframework.com/docs """ -import copy import functools import importlib import inspect @@ -1673,14 +1672,22 @@ def get_file_json(path): return json.load(f) -def read_file(path, raise_not_found=False): - """Open a file and return its content as Unicode.""" +def read_file(path, raise_not_found=False, as_base64=False): + """Open a file and return its content as Unicode or Base64 string.""" if isinstance(path, str): path = path.encode("utf-8") if os.path.exists(path): - with open(path) as f: - return as_unicode(f.read()) + if as_base64: + import base64 + + with open(path, "rb") as f: + content = f.read() + return base64.b64encode(content).decode("utf-8") + else: + with open(path) as f: + content = f.read() + return as_unicode(content) elif raise_not_found: raise OSError(f"{path} Not Found") else: @@ -2076,52 +2083,6 @@ def format(*args, **kwargs): return frappe.utils.formatters.format_value(*args, **kwargs) -def get_print( - doctype=None, - name=None, - print_format=None, - style=None, - as_pdf=False, - doc=None, - output=None, - no_letterhead=0, - password=None, - pdf_options=None, - letterhead=None, -): - """Get Print Format for given document. - - :param doctype: DocType of document. - :param name: Name of document. - :param print_format: Print Format name. Default 'Standard', - :param style: Print Format style. - :param as_pdf: Return as PDF. Default False. - :param password: Password to encrypt the pdf with. Default None""" - from frappe.utils.pdf import get_pdf - from frappe.website.serve import get_response_without_exception_handling - - original_form_dict = copy.deepcopy(local.form_dict) - try: - local.form_dict.doctype = doctype - local.form_dict.name = name - local.form_dict.format = print_format - local.form_dict.style = style - local.form_dict.doc = doc - local.form_dict.no_letterhead = no_letterhead - local.form_dict.letterhead = letterhead - - pdf_options = pdf_options or {} - if password: - pdf_options["password"] = password - - response = get_response_without_exception_handling("printview", 200) - html = str(response.data, "utf-8") - finally: - local.form_dict = original_form_dict - - return get_pdf(html, options=pdf_options, output=output) if as_pdf else html - - def attach_print( doctype, name, @@ -2376,5 +2337,6 @@ from frappe.config import get_common_site_config, get_site_config from frappe.core.doctype.system_settings.system_settings import get_system_settings from frappe.utils import parse_json from frappe.utils.error import log_error +from frappe.utils.print_utils import get_print frappe._optimizations.optimize_all() diff --git a/frappe/patches.txt b/frappe/patches.txt index 4d51a2ce96..b3d42a656d 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -242,3 +242,4 @@ execute:frappe.db.set_single_value("Workspace Settings", "workspace_setup_comple frappe.patches.v16_0.add_app_launcher_in_navbar_settings frappe.desk.doctype.workspace.patches.update_app frappe.patches.v16_0.move_role_desk_settings_to_user +frappe.printing.doctype.print_format.patches.sets_wkhtmltopdf_as_default_for_pdf_generator_field diff --git a/frappe/printing/doctype/print_format/patches/__init__.py b/frappe/printing/doctype/print_format/patches/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/printing/doctype/print_format/patches/sets_wkhtmltopdf_as_default_for_pdf_generator_field.py b/frappe/printing/doctype/print_format/patches/sets_wkhtmltopdf_as_default_for_pdf_generator_field.py new file mode 100644 index 0000000000..09e593686b --- /dev/null +++ b/frappe/printing/doctype/print_format/patches/sets_wkhtmltopdf_as_default_for_pdf_generator_field.py @@ -0,0 +1,7 @@ +import frappe + + +def execute(): + """sets "wkhtmltopdf" as default for pdf_generator field""" + for pf in frappe.get_all("Print Format", pluck="name"): + frappe.db.set_value("Print Format", pf, "pdf_generator", "wkhtmltopdf", update_modified=False) diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index a9a24d5d48..b8654a0816 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -13,6 +13,7 @@ "standard", "custom_format", "disabled", + "pdf_generator", "section_break_6", "print_format_type", "raw_printing", @@ -255,12 +256,19 @@ "fieldtype": "Select", "label": "Page Number", "options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right" + }, + { + "default": "wkhtmltopdf", + "fieldname": "pdf_generator", + "fieldtype": "Select", + "label": "PDF Generator", + "options": "wkhtmltopdf" } ], "icon": "fa fa-print", "idx": 1, "links": [], - "modified": "2024-03-23 16:03:34.964767", + "modified": "2025-02-14 14:49:39.181074", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py index e399181ce8..4e5029944b 100644 --- a/frappe/printing/doctype/print_format/print_format.py +++ b/frappe/printing/doctype/print_format/print_format.py @@ -40,6 +40,7 @@ class PrintFormat(Document): page_number: DF.Literal[ "Hide", "Top Left", "Top Center", "Top Right", "Bottom Left", "Bottom Center", "Bottom Right" ] + pdf_generator: DF.Literal["wkhtmltopdf"] print_format_builder: DF.Check print_format_builder_beta: DF.Check print_format_type: DF.Literal["Jinja", "JS"] diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 4f9b874278..9f0469f0b4 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -33,9 +33,11 @@ PDF_CONTENT_ERRORS = [ ] -def pdf_header_html(soup, head, content, styles, html_id, css): +def pdf_header_html(soup, head, content, styles, html_id, css, path=None): + if not path: + path = "templates/print_formats/pdf_header_footer.html" return frappe.render_template( - "templates/print_formats/pdf_header_footer.html", + path, { "head": head, "content": content, @@ -75,8 +77,10 @@ def _guess_template_error_line_number(template) -> int | None: return frame.lineno -def pdf_footer_html(soup, head, content, styles, html_id, css): - return pdf_header_html(soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css) +def pdf_footer_html(soup, head, content, styles, html_id, css, path=None): + return pdf_header_html( + soup=soup, head=head, content=content, styles=styles, html_id=html_id, css=css, path=path + ) def get_pdf(html, options=None, output: PdfWriter | None = None): @@ -316,7 +320,8 @@ def prepare_header_footer(soup: BeautifulSoup): toggle_visible_pdf(content) id_map = {"header-html": "pdf_header_html", "footer-html": "pdf_footer_html"} hook_func = frappe.get_hooks(id_map.get(html_id)) - html = frappe.get_attr(hook_func[-1])( + html = frappe.call( + hook_func[-1], soup=soup, head=head, content=content, diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index 5b651054e4..e64ad10545 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -3,6 +3,7 @@ import json import os import uuid from io import BytesIO +from typing import Literal from pypdf import PdfWriter @@ -218,14 +219,28 @@ from frappe.deprecation_dumpster import read_multi_pdf @frappe.whitelist(allow_guest=True) def download_pdf( - doctype: str, name: str, format=None, doc=None, no_letterhead=0, language=None, letterhead=None + doctype: str, + name: str, + format=None, + doc=None, + no_letterhead=0, + language=None, + letterhead=None, + pdf_generator: Literal["wkhtmltopdf", "chrome"] | None = None, ): doc = doc or frappe.get_doc(doctype, name) validate_print_permission(doc) with print_language(language): pdf_file = frappe.get_print( - doctype, name, format, doc=doc, as_pdf=True, letterhead=letterhead, no_letterhead=no_letterhead + doctype, + name, + format, + doc=doc, + as_pdf=True, + letterhead=letterhead, + no_letterhead=no_letterhead, + pdf_generator=pdf_generator, ) frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-")) diff --git a/frappe/utils/print_utils.py b/frappe/utils/print_utils.py new file mode 100644 index 0000000000..9eb6357efe --- /dev/null +++ b/frappe/utils/print_utils.py @@ -0,0 +1,89 @@ +from typing import Literal + +import frappe + + +def get_print( + doctype=None, + name=None, + print_format=None, + style=None, + as_pdf=False, + doc=None, + output=None, + no_letterhead=0, + password=None, + pdf_options=None, + letterhead=None, + pdf_generator: Literal["wkhtmltopdf", "chrome"] | None = None, +): + """Get Print Format for given document. + :param doctype: DocType of document. + :param name: Name of document. + :param print_format: Print Format name. Default 'Standard', + :param style: Print Format style. + :param as_pdf: Return as PDF. Default False. + :param password: Password to encrypt the pdf with. Default None + :param pdf_generator: PDF generator to use. Default 'wkhtmltopdf' + """ + + """ + local.form_dict.pdf_generator is set from before_request hook (print designer app) for download_pdf endpoint + if it is not set (internal function call) then set it + """ + import copy + + from frappe.utils.pdf import get_pdf + from frappe.website.serve import get_response_without_exception_handling + + local = frappe.local + if "pdf_generator" not in local.form_dict: + # if arg is passed, use that, else get setting from print format + if pdf_generator is None: + pdf_generator = ( + frappe.get_cached_value("Print Format", print_format, "pdf_generator") or "wkhtmltopdf" + ) + local.form_dict.pdf_generator = pdf_generator + + original_form_dict = copy.deepcopy(local.form_dict) + try: + local.form_dict.doctype = doctype + local.form_dict.name = name + local.form_dict.format = print_format + local.form_dict.style = style + local.form_dict.doc = doc + local.form_dict.no_letterhead = no_letterhead + local.form_dict.letterhead = letterhead + + pdf_options = pdf_options or {} + if password: + pdf_options["password"] = password + + response = get_response_without_exception_handling("printview", 200) + html = str(response.data, "utf-8") + finally: + local.form_dict = original_form_dict + + if not as_pdf: + return html + + if local.form_dict.pdf_generator != "wkhtmltopdf": + hook_func = frappe.get_hooks("pdf_generator") + for hook in hook_func: + """ + check pdf_generator value in your hook function. + if it matches run and return pdf else return None + """ + pdf = frappe.call( + hook, + print_format=print_format, + html=html, + options=pdf_options, + output=output, + pdf_generator=local.form_dict.pdf_generator, + ) + # if hook returns a value, assume it was the correct pdf_generator and return it + if pdf: + return pdf + + return get_pdf(html, options=pdf_options, output=output) diff --git a/frappe/www/printview.py b/frappe/www/printview.py index a1ae6a7b33..7dad803961 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -72,6 +72,9 @@ def get_context(context) -> PrintContext: print_format = get_print_format_doc(None, meta=meta) + make_access_log( + doctype=frappe.form_dict.doctype, document=frappe.form_dict.name, file_type="PDF", method="Print" + ) body = get_rendered_template( doc, print_format=print_format, @@ -99,6 +102,7 @@ def get_context(context) -> PrintContext: "print_format": getattr(print_format, "name", None), "letterhead": letterhead, "no_letterhead": frappe.form_dict.no_letterhead, + "pdf_generator": frappe.form_dict.get("pdf_generator", "wkhtmltopdf"), } @@ -173,7 +177,7 @@ def get_rendered_template( template = None if hook_func := frappe.get_hooks("get_print_format_template"): - template = frappe.get_attr(hook_func[-1])(jenv=jenv, print_format=print_format) + template = frappe.call(hook_func[-1], jenv=jenv, print_format=print_format) if template: pass