249 lines
7.6 KiB
Python
249 lines
7.6 KiB
Python
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# MIT License. See LICENSE
|
|
|
|
import click
|
|
|
|
import frappe
|
|
from frappe import _
|
|
|
|
|
|
@frappe.whitelist()
|
|
def download_pdf(doctype, name, print_format, letterhead=None):
|
|
doc = frappe.get_doc(doctype, name)
|
|
doc.check_permission("print")
|
|
generator = PrintFormatGenerator(print_format, doc, letterhead)
|
|
pdf = generator.render_pdf()
|
|
|
|
frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-"))
|
|
frappe.local.response.filecontent = pdf
|
|
frappe.local.response.type = "pdf"
|
|
|
|
|
|
def get_html(doctype, name, print_format, letterhead=None):
|
|
doc = frappe.get_doc(doctype, name)
|
|
doc.check_permission("print")
|
|
generator = PrintFormatGenerator(print_format, doc, letterhead)
|
|
return generator.get_html_preview()
|
|
|
|
|
|
class PrintFormatGenerator:
|
|
"""
|
|
Generate a PDF of a Document, with repeatable header and footer if letterhead is provided.
|
|
|
|
This generator draws its inspiration and, also a bit of its implementation, from this
|
|
discussion in the library github issues: https://github.com/Kozea/WeasyPrint/issues/92
|
|
"""
|
|
|
|
def __init__(self, print_format, doc, letterhead=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
print_format: str
|
|
Name of the Print Format
|
|
doc: str
|
|
Document to print
|
|
letterhead: str
|
|
Letter Head to apply (optional)
|
|
"""
|
|
self.base_url = frappe.utils.get_url()
|
|
self.print_format = frappe.get_doc("Print Format", print_format)
|
|
self.doc = doc
|
|
|
|
if letterhead == _("No Letterhead"):
|
|
letterhead = None
|
|
self.letterhead = frappe.get_doc("Letter Head", letterhead) if letterhead else None
|
|
|
|
self.build_context()
|
|
self.layout = self.get_layout(self.print_format)
|
|
self.context.layout = self.layout
|
|
|
|
def build_context(self):
|
|
self.print_settings = frappe.get_doc("Print Settings")
|
|
page_width_map = {"A4": 210, "Letter": 216}
|
|
page_width = page_width_map.get(self.print_settings.pdf_page_size) or 210
|
|
body_width = page_width - self.print_format.margin_left - self.print_format.margin_right
|
|
print_style = (
|
|
frappe.get_doc("Print Style", self.print_settings.print_style)
|
|
if self.print_settings.print_style
|
|
else None
|
|
)
|
|
context = frappe._dict(
|
|
{
|
|
"doc": self.doc,
|
|
"print_format": self.print_format,
|
|
"print_settings": self.print_settings,
|
|
"print_style": print_style,
|
|
"letterhead": self.letterhead,
|
|
"page_width": page_width,
|
|
"body_width": body_width,
|
|
}
|
|
)
|
|
self.context = context
|
|
|
|
def get_html_preview(self):
|
|
header_html, footer_html = self.get_header_footer_html()
|
|
self.context.header = header_html
|
|
self.context.footer = footer_html
|
|
return self.get_main_html()
|
|
|
|
def get_main_html(self):
|
|
self.context.css = frappe.render_template("templates/print_format/print_format.css", self.context)
|
|
return frappe.render_template("templates/print_format/print_format.html", self.context)
|
|
|
|
def get_header_footer_html(self):
|
|
header_html = footer_html = None
|
|
if self.letterhead:
|
|
header_html = frappe.render_template("templates/print_format/print_header.html", self.context)
|
|
if self.letterhead:
|
|
footer_html = frappe.render_template("templates/print_format/print_footer.html", self.context)
|
|
return header_html, footer_html
|
|
|
|
def render_pdf(self):
|
|
"""Return a bytes sequence of the rendered PDF."""
|
|
HTML, _CSS = import_weasyprint()
|
|
|
|
self._make_header_footer()
|
|
|
|
self.context.update({"header_height": self.header_height, "footer_height": self.footer_height})
|
|
main_html = self.get_main_html()
|
|
|
|
html = HTML(string=main_html, base_url=self.base_url)
|
|
main_doc = html.render()
|
|
|
|
if self.header_html or self.footer_html:
|
|
self._apply_overlay_on_main(main_doc, self.header_body, self.footer_body)
|
|
return main_doc.write_pdf()
|
|
|
|
def _compute_overlay_element(self, element: str):
|
|
"""
|
|
Parameters
|
|
----------
|
|
element: str
|
|
Either 'header' or 'footer'
|
|
|
|
Returns
|
|
-------
|
|
element_body: BlockBox
|
|
A Weasyprint pre-rendered representation of an html element
|
|
element_height: float
|
|
The height of this element, which will be then translated in a html height
|
|
"""
|
|
HTML, CSS = import_weasyprint()
|
|
|
|
html = HTML(
|
|
string=getattr(self, f"{element}_html"),
|
|
base_url=self.base_url,
|
|
)
|
|
element_doc = html.render(stylesheets=[CSS(string="@page {size: A4 portrait; margin: 0;}")])
|
|
element_page = element_doc.pages[0]
|
|
element_body = PrintFormatGenerator.get_element(element_page._page_box.all_children(), "body")
|
|
element_body = element_body.copy_with_children(element_body.all_children())
|
|
element_html = PrintFormatGenerator.get_element(element_page._page_box.all_children(), element)
|
|
|
|
if element == "header":
|
|
element_height = element_html.height
|
|
if element == "footer":
|
|
element_height = element_page.height - element_html.position_y
|
|
|
|
return element_body, element_height
|
|
|
|
def _apply_overlay_on_main(self, main_doc, header_body=None, footer_body=None):
|
|
"""
|
|
Insert the header and the footer in the main document.
|
|
|
|
Parameters
|
|
----------
|
|
main_doc: Document
|
|
The top level representation for a PDF page in Weasyprint.
|
|
header_body: BlockBox
|
|
A representation for an html element in Weasyprint.
|
|
footer_body: BlockBox
|
|
A representation for an html element in Weasyprint.
|
|
"""
|
|
for page in main_doc.pages:
|
|
page_body = PrintFormatGenerator.get_element(page._page_box.all_children(), "body")
|
|
|
|
if header_body:
|
|
page_body.children += header_body.all_children()
|
|
if footer_body:
|
|
page_body.children += footer_body.all_children()
|
|
|
|
def _make_header_footer(self):
|
|
self.header_html, self.footer_html = self.get_header_footer_html()
|
|
|
|
if self.header_html:
|
|
header_body, header_height = self._compute_overlay_element("header")
|
|
else:
|
|
header_body, header_height = None, 0
|
|
if self.footer_html:
|
|
footer_body, footer_height = self._compute_overlay_element("footer")
|
|
else:
|
|
footer_body, footer_height = None, 0
|
|
|
|
self.header_body = header_body
|
|
self.header_height = header_height
|
|
self.footer_body = footer_body
|
|
self.footer_height = footer_height
|
|
|
|
def get_layout(self, print_format):
|
|
layout = frappe.parse_json(print_format.format_data)
|
|
layout = self.set_field_renderers(layout)
|
|
layout = self.process_margin_texts(layout)
|
|
return layout
|
|
|
|
def set_field_renderers(self, layout):
|
|
renderers = {"HTML Editor": "HTML", "Markdown Editor": "Markdown"}
|
|
for section in layout["sections"]:
|
|
for column in section["columns"]:
|
|
for df in column["fields"]:
|
|
fieldtype = df["fieldtype"]
|
|
renderer_name = fieldtype.replace(" ", "")
|
|
df["renderer"] = renderers.get(fieldtype) or renderer_name
|
|
df["section"] = section
|
|
return layout
|
|
|
|
def process_margin_texts(self, layout):
|
|
margin_texts = [
|
|
"top_left",
|
|
"top_center",
|
|
"top_right",
|
|
"bottom_left",
|
|
"bottom_center",
|
|
"bottom_right",
|
|
]
|
|
for key in margin_texts:
|
|
text = layout.get("text_" + key)
|
|
if text and "{{" in text:
|
|
layout["text_" + key] = frappe.render_template(text, self.context)
|
|
|
|
return layout
|
|
|
|
@staticmethod
|
|
def get_element(boxes, element):
|
|
"""
|
|
Given a set of boxes representing the elements of a PDF page in a DOM-like way, find the
|
|
box which is named `element`.
|
|
|
|
Look at the notes of the class for more details on Weasyprint insides.
|
|
"""
|
|
for box in boxes:
|
|
if box.element_tag == element:
|
|
return box
|
|
return PrintFormatGenerator.get_element(box.all_children(), element)
|
|
|
|
|
|
def import_weasyprint():
|
|
try:
|
|
from weasyprint import CSS, HTML
|
|
|
|
return HTML, CSS
|
|
except OSError:
|
|
message = "\n".join(
|
|
[
|
|
"WeasyPrint depdends on additional system dependencies.",
|
|
"Follow instructions specific to your operating system:",
|
|
"https://doc.courtbouillon.org/weasyprint/stable/first_steps.html",
|
|
]
|
|
)
|
|
click.secho(message, fg="yellow")
|
|
frappe.throw(message)
|