* fix(diff): add type hints to whitelisted methods * fix(global_search): add type hints to whitelisted methods * fix(custom_html_block): add type hints to whitelisted methods * fix(deleted_document): add type hints to whitelisted methods * fix(log_settings): add type hints to whitelisted methods * fix(role): add type hints to whitelisted methods * fix(user_type): add type hints to whitelisted methods * fix(rq_job): add type hints to whitelisted methods * fix(link_preview): add type hints to whitelisted methods * fix(email_account): add type hints to whitelisted methods * fix(web_form): add type hints to whitelisted methods * fix(web_page_view): add type hints to whitelisted methods * fix(csvutils): add type hints to whitelisted methods * fix(file_manager): add type hints to whitelisted methods * fix(email_body): add type hints to whitelisted methods * fix(email_queue): add type hints to whitelisted methods * fix(email_template): add type hints to whitelisted methods * fix(notification): add type hints to whitelisted methods * fix(email_group): add type hints to whitelisted methods * fix(inbox): add type hints to whitelisted methods * fix(recorder): add type hints to whitelisted methods * fix(sms_settings): add type hints to whitelisted methods * fix: tighten type hints * fix(data_import): add type hints to whitelisted methods * fix(user_permission): add type hints to whitelisted methods * fix(gantt): add type hints to whitelisted methods * fix(like): add type hints to whitelisted methods * fix(search): add type hints to whitelisted methods * fix(onboarding_step): add type hints to whitelisted methods * fix(system_console): add type hints to whitelisted methods * fix(workspace_sidebar): add type hints to whitelisted methods * fix(todo): add type hints to whitelisted methods * fix: correct type hints * fix(print_format): add type hints to whitelisted methods * fix(client): add type hints to whitelisted methods
352 lines
9.3 KiB
Python
352 lines
9.3 KiB
Python
import http
|
|
import json
|
|
import os
|
|
import uuid
|
|
from io import BytesIO
|
|
from typing import Literal
|
|
from urllib.parse import urlparse
|
|
|
|
from pypdf import PdfWriter
|
|
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.core.doctype.access_log.access_log import make_access_log
|
|
from frappe.model.document import Document
|
|
from frappe.translate import print_language
|
|
from frappe.utils.jinja import render_template
|
|
from frappe.utils.pdf import get_pdf
|
|
|
|
no_cache = 1
|
|
|
|
base_template_path = "www/printview.html"
|
|
standard_format = "templates/print_formats/standard.html"
|
|
|
|
from frappe.www.printview import validate_print_permission
|
|
|
|
|
|
@frappe.whitelist()
|
|
def download_multi_pdf(
|
|
doctype: str | dict[str, list[str]],
|
|
name: str | list[str],
|
|
format: str | None = None,
|
|
no_letterhead: bool = False,
|
|
letterhead: str | None = None,
|
|
options: str | None = None,
|
|
):
|
|
"""
|
|
Calls _download_multi_pdf with the given parameters and returns the response
|
|
"""
|
|
return _download_multi_pdf(doctype, name, format, no_letterhead, letterhead, options)
|
|
|
|
|
|
@frappe.whitelist()
|
|
def download_multi_pdf_async(
|
|
doctype: str | dict[str, list[str]],
|
|
name: str | list[str],
|
|
format: str | None = None,
|
|
no_letterhead: bool = False,
|
|
letterhead: str | None = None,
|
|
options: str | None = None,
|
|
):
|
|
"""
|
|
Calls _download_multi_pdf with the given parameters in a background job, returns task ID
|
|
"""
|
|
task_id = str(uuid.uuid4())
|
|
if isinstance(doctype, dict):
|
|
doc_count = sum([len(doctype[dt]) for dt in doctype])
|
|
else:
|
|
doc_count = len(json.loads(name))
|
|
|
|
frappe.enqueue(
|
|
_download_multi_pdf,
|
|
doctype=doctype,
|
|
name=name,
|
|
task_id=task_id,
|
|
format=format,
|
|
no_letterhead=no_letterhead,
|
|
letterhead=letterhead,
|
|
options=options,
|
|
queue="long" if doc_count > 20 else "short",
|
|
at_front_when_starved=True,
|
|
)
|
|
frappe.local.response["http_status_code"] = http.HTTPStatus.CREATED
|
|
return {"task_id": task_id}
|
|
|
|
|
|
def _download_multi_pdf(
|
|
doctype: str | dict[str, list[str]],
|
|
name: str | list[str],
|
|
format: str | None = None,
|
|
no_letterhead: bool = False,
|
|
letterhead: str | None = None,
|
|
options: str | None = None,
|
|
task_id: str | None = None,
|
|
):
|
|
"""Return a PDF compiled by concatenating multiple documents.
|
|
|
|
The documents can be from a single DocType or multiple DocTypes.
|
|
|
|
Note: The design may seem a little weird, but it exists to ensure backward compatibility.
|
|
The correct way to use this function is to pass a dict to doctype as described below
|
|
|
|
NEW FUNCTIONALITY
|
|
=================
|
|
Parameters:
|
|
doctype (dict):
|
|
key (string): DocType name
|
|
value (list): of strings of doc names which need to be concatenated and printed
|
|
name (string):
|
|
name of the pdf which is generated
|
|
format:
|
|
Print Format to be used
|
|
|
|
OLD FUNCTIONALITY - soon to be deprecated
|
|
=========================================
|
|
Parameters:
|
|
doctype (string):
|
|
name of the DocType to which the docs belong which need to be printed
|
|
name (string or list):
|
|
If string the name of the doc which needs to be printed
|
|
If list the list of strings of doc names which needs to be printed
|
|
format:
|
|
Print Format to be used
|
|
|
|
Returns:
|
|
Publishes a link to the PDF to the given task ID
|
|
"""
|
|
filename = ""
|
|
|
|
pdf_writer = PdfWriter()
|
|
|
|
if isinstance(options, str):
|
|
options = json.loads(options)
|
|
|
|
if not isinstance(doctype, dict):
|
|
result = json.loads(name)
|
|
total_docs = len(result)
|
|
filename = f"{doctype}_"
|
|
|
|
# Concatenating pdf files
|
|
for idx, ss in enumerate(result):
|
|
try:
|
|
pdf_writer = frappe.get_print(
|
|
doctype,
|
|
ss,
|
|
format,
|
|
as_pdf=True,
|
|
output=pdf_writer,
|
|
no_letterhead=no_letterhead,
|
|
letterhead=letterhead,
|
|
pdf_options=options,
|
|
)
|
|
except Exception:
|
|
if task_id:
|
|
frappe.publish_realtime(task_id=task_id, message={"message": "Failed"})
|
|
|
|
# Publish progress
|
|
if task_id:
|
|
frappe.publish_progress(
|
|
percent=(idx + 1) / total_docs * 100,
|
|
title=_("PDF Generation in Progress"),
|
|
description=_("{0}/{1} complete | Please leave this tab open until completion.").format(
|
|
idx + 1, total_docs
|
|
),
|
|
task_id=task_id,
|
|
)
|
|
|
|
if task_id is None:
|
|
frappe.local.response.filename = "{doctype}.pdf".format(
|
|
doctype=doctype.replace(" ", "-").replace("/", "-")
|
|
)
|
|
|
|
else:
|
|
total_docs = sum([len(doctype[dt]) for dt in doctype])
|
|
count = 1
|
|
for doctype_name in doctype:
|
|
filename += f"{doctype_name}_"
|
|
for doc_name in doctype[doctype_name]:
|
|
try:
|
|
pdf_writer = frappe.get_print(
|
|
doctype_name,
|
|
doc_name,
|
|
format,
|
|
as_pdf=True,
|
|
output=pdf_writer,
|
|
no_letterhead=no_letterhead,
|
|
letterhead=letterhead,
|
|
pdf_options=options,
|
|
)
|
|
except Exception:
|
|
if task_id:
|
|
frappe.publish_realtime(task_id=task_id, message="Failed")
|
|
frappe.log_error(
|
|
title="Error in Multi PDF download",
|
|
message=f"Permission Error on doc {doc_name} of doctype {doctype_name}",
|
|
reference_doctype=doctype_name,
|
|
reference_name=doc_name,
|
|
)
|
|
|
|
count += 1
|
|
|
|
if task_id:
|
|
frappe.publish_progress(
|
|
percent=count / total_docs * 100,
|
|
title=_("PDF Generation in Progress"),
|
|
description=_(
|
|
"{0}/{1} complete | Please leave this tab open until completion."
|
|
).format(count, total_docs),
|
|
task_id=task_id,
|
|
)
|
|
if task_id is None:
|
|
frappe.local.response.filename = f"{name}.pdf"
|
|
|
|
with BytesIO() as merged_pdf:
|
|
pdf_writer.write(merged_pdf)
|
|
if task_id:
|
|
_file = frappe.get_doc(
|
|
{
|
|
"doctype": "File",
|
|
"file_name": f"{filename}{task_id}.pdf",
|
|
"content": merged_pdf.getvalue(),
|
|
"is_private": 1,
|
|
}
|
|
)
|
|
_file.save()
|
|
frappe.publish_realtime(f"task_complete:{task_id}", message={"file_url": _file.unique_url})
|
|
else:
|
|
frappe.local.response.filecontent = merged_pdf.getvalue()
|
|
frappe.local.response.type = "pdf"
|
|
|
|
|
|
from frappe.deprecation_dumpster import read_multi_pdf
|
|
|
|
|
|
@frappe.whitelist(allow_guest=True)
|
|
def download_pdf(
|
|
doctype: str,
|
|
name: str,
|
|
format: str | None = None,
|
|
doc: Document | None = None,
|
|
no_letterhead: bool | int = 0,
|
|
language: str | None = None,
|
|
letterhead: str | None = None,
|
|
pdf_generator: Literal["wkhtmltopdf", "chrome"] | None = None,
|
|
):
|
|
if pdf_generator is None:
|
|
pdf_generator = "wkhtmltopdf"
|
|
|
|
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,
|
|
pdf_generator=pdf_generator,
|
|
)
|
|
|
|
frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-"))
|
|
frappe.local.response.filecontent = pdf_file
|
|
frappe.local.response.type = "pdf"
|
|
|
|
|
|
@frappe.whitelist()
|
|
def report_to_pdf(html: str, orientation: str = "Landscape"):
|
|
make_access_log(file_type="PDF", method="PDF", page=html)
|
|
frappe.local.response.filename = "report.pdf"
|
|
frappe.local.response.filecontent = get_pdf(
|
|
html,
|
|
{
|
|
"orientation": orientation,
|
|
"proxy": "http://0.0.0.0:0",
|
|
"bypass-proxy-for": urlparse(frappe.utils.get_url(allow_header_override=False)).hostname,
|
|
"load-error-handling": "ignore",
|
|
},
|
|
)
|
|
frappe.local.response.type = "pdf"
|
|
|
|
|
|
@frappe.whitelist()
|
|
def render_letterhead_for_print(letterhead: str | None = None, doc: dict | str | None = None) -> dict:
|
|
"""Render letterhead HTML (header/footer) with Jinja for report printing."""
|
|
|
|
if not frappe.has_permission("Letter Head", "read"):
|
|
return {}
|
|
|
|
if isinstance(doc, str):
|
|
try:
|
|
doc = json.loads(doc)
|
|
except Exception:
|
|
doc = {}
|
|
|
|
letter_head = frappe._dict(
|
|
frappe.db.get_value(
|
|
"Letter Head",
|
|
letterhead or {"is_default": 1},
|
|
["content", "footer", "header_script", "footer_script"],
|
|
as_dict=True,
|
|
)
|
|
or {}
|
|
)
|
|
|
|
context_doc = frappe._dict(doc or {})
|
|
rendered = {}
|
|
|
|
if letter_head.content:
|
|
header = render_template(letter_head.content, {"doc": context_doc})
|
|
if letter_head.header_script:
|
|
header += f"\n<script>\n{letter_head.header_script}\n</script>\n"
|
|
rendered["header"] = header
|
|
|
|
if letter_head.footer:
|
|
footer = render_template(letter_head.footer, {"doc": context_doc})
|
|
if letter_head.footer_script:
|
|
footer += f"\n<script>\n{letter_head.footer_script}\n</script>\n"
|
|
rendered["footer"] = footer
|
|
|
|
return rendered
|
|
|
|
|
|
@frappe.whitelist()
|
|
def print_by_server(
|
|
doctype: str,
|
|
name: str | int,
|
|
printer_setting: str,
|
|
print_format: str | None = None,
|
|
doc: Document | None = None,
|
|
no_letterhead: bool | int = 0,
|
|
file_path: str | None = None,
|
|
):
|
|
print_settings = frappe.get_doc("Network Printer Settings", printer_setting)
|
|
try:
|
|
import cups
|
|
except ImportError:
|
|
frappe.throw(_("You need to install pycups to use this feature!"))
|
|
|
|
try:
|
|
cups.setServer(print_settings.server_ip)
|
|
cups.setPort(print_settings.port)
|
|
conn = cups.Connection()
|
|
output = PdfWriter()
|
|
output = frappe.get_print(
|
|
doctype, name, print_format, doc=doc, no_letterhead=no_letterhead, as_pdf=True, output=output
|
|
)
|
|
if not file_path:
|
|
file_path = os.path.join("/", "tmp", f"frappe-pdf-{frappe.generate_hash()}.pdf")
|
|
output.write(open(file_path, "wb"))
|
|
conn.printFile(print_settings.printer_name, file_path, name, {})
|
|
except OSError as e:
|
|
if (
|
|
"ContentNotFoundError" in e.message
|
|
or "ContentOperationNotPermittedError" in e.message
|
|
or "UnknownContentError" in e.message
|
|
or "RemoteHostClosedError" in e.message
|
|
):
|
|
frappe.throw(_("PDF generation failed"))
|
|
except cups.IPPError:
|
|
frappe.throw(_("Printing failed"))
|