瀏覽代碼

feat: More features

- Letterhead editing
- Edit Header and Footer
- Margin Text
- PrintFormatGenerator class handles generation of HTML and PDF and repeating of Header/Footer
- Simplify /printpreview
- Separate renderer files for each fieldtype
version-14
Faris Ansari 3 年之前
父節點
當前提交
f4bd62c010
共有 22 個文件被更改,包括 642 次插入221 次删除
  1. +6
    -0
      frappe/client.py
  2. +7
    -1
      frappe/printing/doctype/print_format/print_format.py
  3. +61
    -0
      frappe/public/js/print_format_builder/HTMLEditor.vue
  4. +108
    -0
      frappe/public/js/print_format_builder/LetterHeadEditor.vue
  5. +114
    -0
      frappe/public/js/print_format_builder/MarginText.vue
  6. +3
    -0
      frappe/public/js/print_format_builder/Preview.vue
  7. +28
    -1
      frappe/public/js/print_format_builder/PrintFormat.vue
  8. +1
    -0
      frappe/public/js/print_format_builder/PrintFormatControls.vue
  9. +21
    -1
      frappe/public/js/print_format_builder/store.js
  10. +1
    -56
      frappe/templates/print_format/macros.html
  11. +10
    -0
      frappe/templates/print_format/macros/Data.html
  12. +3
    -0
      frappe/templates/print_format/macros/HTML.html
  13. +3
    -0
      frappe/templates/print_format/macros/Markdown.html
  14. +30
    -0
      frappe/templates/print_format/macros/Table.html
  15. +24
    -0
      frappe/templates/print_format/print_footer.html
  16. +21
    -6
      frappe/templates/print_format/print_format.css
  17. +2
    -0
      frappe/templates/print_format/print_format.html
  18. +9
    -0
      frappe/templates/print_format/print_format_font.css
  19. +24
    -0
      frappe/templates/print_format/print_header.html
  20. +157
    -139
      frappe/utils/weasyprint.py
  21. +9
    -2
      frappe/www/printpreview.html
  22. +0
    -15
      frappe/www/printpreview.py

+ 6
- 0
frappe/client.py 查看文件

@@ -258,6 +258,12 @@ def set_default(key, value, parent=None):
frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user)

@frappe.whitelist()
def get_default(key, parent=None):
"""set a user default value"""
return frappe.db.get_default(key, parent)


@frappe.whitelist(methods=['POST', 'PUT'])
def make_width_property_setter(doc):
'''Set width Property Setter


+ 7
- 1
frappe/printing/doctype/print_format/print_format.py 查看文件

@@ -7,10 +7,16 @@ import frappe.utils
import json
from frappe import _
from frappe.utils.jinja import validate_template
from frappe.utils.weasyprint import get_html, download_pdf
from frappe.model.document import Document

class PrintFormat(Document):
def get_html(self, docname, letterhead=None):
return get_html(self.doc_type, docname, self.name, letterhead)

def download_pdf(self, docname, letterhead=None):
return download_pdf(self.doc_type, docname, self.name, letterhead)

def validate(self):
if (self.standard=="Yes"
and not frappe.local.conf.get("developer_mode")


+ 61
- 0
frappe/public/js/print_format_builder/HTMLEditor.vue 查看文件

@@ -0,0 +1,61 @@
<template>
<div class="html-editor">
<div class="d-flex justify-content-end">
<button class="btn btn-default btn-xs btn-edit" @click="toggle_edit">
{{ !editing ? buttonLabel : __("Done") }}
</button>
</div>
<div v-if="!editing" v-html="value"></div>
<div v-show="editing" ref="editor"></div>
</div>
</template>
<script>
export default {
name: "HTMLEditor",
props: ["value", "button-label"],
data() {
return {
editing: false
};
},
methods: {
toggle_edit() {
if (this.editing) {
this.$emit("change", this.get_value());
this.editing = false;
return;
}

this.editing = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "editor",
fieldtype: "HTML Editor",
min_lines: 10,
max_lines: 30,
change: () => {
this.$emit("change", this.get_value());
}
},
render_input: true
});
}
this.control.set_value(this.value);
},
get_value() {
return frappe.dom.remove_script_and_style(this.control.get_value());
}
}
};
</script>
<style>
.html-editor {
position: relative;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
</style>

+ 108
- 0
frappe/public/js/print_format_builder/LetterHeadEditor.vue 查看文件

@@ -0,0 +1,108 @@
<template>
<div class="letterhead">
<div class="mb-2 d-flex justify-content-end">
<button
class="btn btn-default btn-xs btn-edit"
@click="toggle_edit_letterhead"
>
{{ !$store.edit_letterhead ? __("Edit") : __("Done") }}
</button>
<button
v-if="type == 'Header'"
class="ml-2 btn btn-default btn-xs btn-change-letterhead"
@click="change_letterhead"
>
{{ __("Change Letter Head") }}
</button>
</div>
<div
v-if="letterhead && !$store.edit_letterhead"
v-html="letterhead[field]"
></div>
<div v-show="letterhead && $store.edit_letterhead" ref="editor"></div>
</div>
</template>
<script>
import { storeMixin } from "./store";
export default {
name: "LetterHeadEditor",
props: ["type"],
mixins: [storeMixin],
mounted() {
if (!this.letterhead) {
frappe
.call("frappe.client.get_default", { key: "letter_head" })
.then(r => {
if (r.message) {
this.$store.change_letterhead(r.message);
}
});
}
},
methods: {
toggle_edit_letterhead() {
if (this.$store.edit_letterhead) {
this.$store.edit_letterhead = false;
return;
}
this.$store.edit_letterhead = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "letterhead",
fieldtype: "Comment",
change: () => {
this.letterhead._dirty = true;
this.letterhead[
this.field
] = this.control.get_value();
}
},
render_input: true,
only_input: true,
no_wrapper: true
});
}
this.control.set_value(this.letterhead[this.field]);
},
change_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Change Letter Head"),
fields: [
{
label: __("Letter Head"),
fieldname: "letterhead",
fieldtype: "Link",
options: "Letter Head"
}
],
primary_action: ({ letterhead }) => {
if (letterhead) {
this.$store.change_letterhead(letterhead);
}
d.hide();
}
});
d.show();
}
},
computed: {
field() {
return {
Header: "content",
Footer: "footer"
}[this.type];
}
}
};
</script>
<style>
.letterhead {
position: relative;
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
</style>

+ 114
- 0
frappe/public/js/print_format_builder/MarginText.vue 查看文件

@@ -0,0 +1,114 @@
<template>
<button
class="btn btn-xs btn-default margin-text"
:class="{ 'text-extra-muted': !value }"
:style="styles"
@click="edit"
:title="__('Edit {0}', [label])"
>
{{ value || label }}
</button>
</template>
<script>
import { storeMixin } from "./store";

export default {
name: "MarginText",
props: ["position"],
mixins: [storeMixin],
methods: {
edit() {
let d = new frappe.ui.Dialog({
title: __("Edit {0}", [this.label]),
fields: [
{
label: __("Select Template"),
fieldname: "helper",
fieldtype: "Select",
options: Object.keys(this.helpers),
change: () => {
this.set_helper(d.get_value("helper"));
}
},
{
label: this.label,
fieldname: "text",
fieldtype: "Data",
description:
"Use jinja blocks for dynamic content. For e.g., {{ doc.name }}"
}
],
primary_action: ({ text }) => {
this.$set(this.layout, "text_" + this.position, text);
d.hide();
},
secondary_action_label: __("Clear"),
secondary_action: () => {
d.set_value("text", "");
}
});
d.show();
d.set_value("text", this.value);
this.dialog = d;
},
set_helper(helper) {
let value = this.helpers[helper];
if (value) {
this.dialog.set_value("text", value);
}
}
},
computed: {
value() {
let text = this.layout["text_" + this.position];
return text;
},
label() {
return {
top_left: __("Top Left Text"),
top_center: __("Top Center Text"),
top_right: __("Top Right Text"),
bottom_left: __("Bottom Left Text"),
bottom_center: __("Bottom Center Text"),
bottom_right: __("Bottom Right Text")
}[this.position];
},
helpers() {
return {
"Page number (x of y)": 'counter(page) " of " counter(pages)',
"Document Name": '"{{ doc.name }}"'
};
},
styles() {
let styles = {};
if (this.position.includes("top")) {
styles.top = "0.5rem";
}
if (this.position.includes("bottom")) {
styles.bottom = "0.5rem";
}
if (this.position.includes("left")) {
styles.left = this.print_format.margin_left + "mm";
}
if (this.position.includes("right")) {
styles.right = this.print_format.margin_right + "mm";
}
if (this.position.includes("center")) {
styles.left = "50%";
styles.transform = "translateX(-50%)";
}
return styles;
}
}
};
</script>
<style scoped>
.margin-text {
position: absolute;
z-index: 1;
max-width: 10rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>

+ 3
- 0
frappe/public/js/print_format_builder/Preview.vue 查看文件

@@ -97,6 +97,9 @@ export default {
params.append("doctype", this.doctype);
params.append("name", this.docname);
params.append("print_format", this.print_format.name);
if (this.$store.letterhead) {
params.append("letterhead", this.$store.letterhead.name);
}
let url =
this.type == "PDF"
? `/api/method/frappe.utils.weasyprint.download_pdf`


+ 28
- 1
frappe/public/js/print_format_builder/PrintFormat.vue 查看文件

@@ -1,6 +1,20 @@
<template>
<div class="print-format-main" :style="rootStyles">
<MarginText position="top_left" />
<MarginText position="top_center" />
<MarginText position="top_right" />
<MarginText position="bottom_left" />
<MarginText position="bottom_center" />
<MarginText position="bottom_right" />

<LetterHeadEditor type="Header" />
<HTMLEditor
:value="layout.header"
@change="$set(layout, 'header', $event)"
:button-label="__('Edit Header')"
/>
<draggable
class="mb-4"
v-model="layout.sections"
group="sections"
filter=".section-columns, .column, .field"
@@ -13,11 +27,20 @@
@add_section_above="add_section_above(section)"
/>
</draggable>
<HTMLEditor
:value="layout.footer"
@change="$set(layout, 'footer', $event)"
:button-label="__('Edit Footer')"
/>
<LetterHeadEditor type="Footer" />
</div>
</template>

<script>
import draggable from "vuedraggable";
import HTMLEditor from "./HTMLEditor.vue";
import LetterHeadEditor from "./LetterHeadEditor.vue";
import MarginText from "./MarginText.vue";
import PrintFormatSection from "./PrintFormatSection.vue";
import { storeMixin } from "./store";

@@ -26,7 +49,10 @@ export default {
mixins: [storeMixin],
components: {
draggable,
PrintFormatSection
PrintFormatSection,
LetterHeadEditor,
HTMLEditor,
MarginText
},
computed: {
rootStyles() {
@@ -66,6 +92,7 @@ export default {

<style scoped>
.print-format-main {
position: relative;
margin-right: auto;
margin-left: auto;
background-color: white;


+ 1
- 0
frappe/public/js/print_format_builder/PrintFormatControls.vue 查看文件

@@ -20,6 +20,7 @@
type="number"
class="form-control form-control-sm"
:value="print_format[df.fieldname]"
min="0"
@change="
e =>
update_margin(


+ 21
- 1
frappe/public/js/print_format_builder/store.js 查看文件

@@ -11,11 +11,14 @@ export function getStore(print_format_name) {
data() {
return {
print_format_name,
letterhead_name: null,
print_format: null,
letterhead: null,
doctype: null,
meta: null,
layout: null,
dirty: false
dirty: false,
edit_letterhead: false
};
},
watch: {
@@ -56,6 +59,7 @@ export function getStore(print_format_name) {
this.print_format = print_format;
this.layout = this.get_layout();
this.$nextTick(() => (this.dirty = false));
this.edit_letterhead = false;
resolve();
}
);
@@ -109,11 +113,19 @@ export function getStore(print_format_name) {
.call("frappe.client.save", {
doc: this.print_format
})
.then(() => {
if (this.letterhead && this.letterhead._dirty) {
return frappe.call("frappe.client.save", {
doc: this.letterhead
});
}
})
.then(() => this.fetch())
.always(() => frappe.dom.unfreeze());
},
reset_changes() {
this.fetch();

},
get_layout() {
if (this.print_format) {
@@ -129,6 +141,11 @@ export function getStore(print_format_name) {
},
get_default_layout() {
return create_default_layout(this.meta);
},
change_letterhead(letterhead) {
frappe.db.get_doc("Letter Head", letterhead).then(doc => {
this.letterhead = doc;
});
}
}
};
@@ -145,6 +162,9 @@ export let storeMixin = {
layout() {
return this.$store.layout;
},
letterhead() {
return this.$store.letterhead;
},
meta() {
return this.$store.meta;
}


+ 1
- 56
frappe/templates/print_format/macros.html 查看文件

@@ -1,20 +1,5 @@
{% macro render_field(df, doc) %}
{% if df.fieldtype == 'Table' %}
{{ render_table(df, doc) }}
{% elif df.fieldtype == 'HTML' %}
{{ render_custom_html(df, doc) }}
{% else %}
{% if doc.get(fieldname) %}
<div class="field">
<div class="label" {{ field_attributes(df) }}>
{{ df.label }}
</div>
<div class="value" {{ field_attributes(df) }}>
{{ doc.get_formatted(df.fieldname) }}
</div>
</div>
{% endif %}
{% endif %}
{% include ['templates/print_format/macros/' + df.renderer + '.html', 'templates/print_format/macros/Data.html'] ignore missing %}
{% endmacro %}

{% macro field_attributes(df) %}
@@ -25,43 +10,3 @@ data-fieldname="{{ df.fieldname }}"
data-fieldtype="{{ df.fieldtype }}"
{%- endif -%}
{% endmacro %}

{% macro render_table(df, doc) %}
{% if doc.get(df.fieldname) %}
<div class="child-table" {{ field_attributes(df) }}>
<div class="label">
{{ df.label }}
</div>
<table class="table">
{% set columns = df.table_columns %}
<thead>
<tr class="table-row">
{% for column in columns %}
<th class="column-header" width="{{ column.width }}%" {{ field_attributes(column) }}>
{{ column.label }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in doc.get(df.fieldname) %}
<tr class="table-row {{ loop.cycle('odd', 'even') }}" data-idx="{{ row.idx }}">
{% for column in columns %}
<td class="column-value" width="{{ column.width }}%" {{ field_attributes(column) }}>
{{ row.get_formatted(column.fieldname) }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endmacro %}

{% macro render_custom_html(df, doc) %}
<div class="custom-html">
{{ frappe.render_template(df.html, {'doc': doc}) }}
</div>
{% endmacro %}


+ 10
- 0
frappe/templates/print_format/macros/Data.html 查看文件

@@ -0,0 +1,10 @@
{% if doc.get(fieldname) %}
<div class="field">
<div class="label" {{ field_attributes(df) }}>
{{ df.label }}
</div>
<div class="value" {{ field_attributes(df) }}>
{{ doc.get_formatted(df.fieldname) }}
</div>
</div>
{% endif %}

+ 3
- 0
frappe/templates/print_format/macros/HTML.html 查看文件

@@ -0,0 +1,3 @@
<div class="custom-html">
{{ frappe.render_template(df.html, {'doc': doc}) }}
</div>

+ 3
- 0
frappe/templates/print_format/macros/Markdown.html 查看文件

@@ -0,0 +1,3 @@
<div class="value" {{ field_attributes(df) }}>
{{ frappe.utils.md_to_html(doc.get(df.fieldname)) }}
</div>

+ 30
- 0
frappe/templates/print_format/macros/Table.html 查看文件

@@ -0,0 +1,30 @@
{% if doc.get(df.fieldname) %}
<div class="child-table" {{ field_attributes(df) }}>
<div class="label">
{{ df.label }}
</div>
<table class="table">
{% set columns = df.table_columns %}
<thead>
<tr class="table-row">
{% for column in columns %}
<th class="column-header" width="{{ column.width }}%" {{ field_attributes(column) }}>
{{ column.label }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in doc.get(df.fieldname) %}
<tr class="table-row {{ loop.cycle('odd', 'even') }}" data-idx="{{ row.idx }}">
{% for column in columns %}
<td class="column-value" width="{{ column.width }}%" {{ field_attributes(column) }}>
{{ row.get_formatted(column.fieldname) }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

+ 24
- 0
frappe/templates/print_format/print_footer.html 查看文件

@@ -0,0 +1,24 @@
<style>
{% include "templates/print_format/print_format_font.css" %}

@media print {
footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding-bottom: {{ print_format.margin_bottom | int }}mm;
padding-left: {{ print_format.margin_left | int }}mm;
padding-right: {{ print_format.margin_right | int }}mm;
}
}
</style>
<footer>
{%- if layout.footer -%}
{{ frappe.render_template(layout.footer, {'doc': doc}) }}
{%- endif -%}

{%- if letterhead -%}
{{ frappe.render_template(letterhead.footer, {'doc': doc}) }}
{%- endif -%}
</footer>

+ 21
- 6
frappe/templates/print_format/print_format.css 查看文件

@@ -1,7 +1,13 @@
@charset "UTF-8";
{% if font_family %}
@import url("https://fonts.googleapis.com/css?family={{ font_family }}:400,500,600,700");
{% include "templates/print_format/print_format_font.css" %}

{% macro render_margin_text(position) %}
{% set text = layout['text_' + position] %}
{% if text %}
@{{ position.replace('_', '-') }} {
content: {{ text }}
}
{% endif %}
{% endmacro %}

@page {
size: {{ print_settings.pdf_page_size or 'A4' }} portrait;
@@ -9,10 +15,15 @@
margin-bottom: {{ print_format.margin_bottom | int }}mm;
margin-left: {{ print_format.margin_left | int }}mm;
margin-right: {{ print_format.margin_right | int }}mm;
}
padding-top: {{ header_height }}px;
padding-bottom: {{ footer_height }}px;

html, body {
font-family: {{ print_format.font or 'Inter' }}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
{{ render_margin_text('top_left') }}
{{ render_margin_text('top_center') }}
{{ render_margin_text('top_right') }}
{{ render_margin_text('bottom_left') }}
{{ render_margin_text('bottom_center') }}
{{ render_margin_text('bottom_right') }}
}

body {
@@ -60,6 +71,10 @@ body {
text-align: right;
}

.table-row {
page-break-inside: avoid;
}

.table-row td, .table-row th {
border-bottom-width: 1px;
border-bottom-style: solid;


+ 2
- 0
frappe/templates/print_format/print_format.html 查看文件

@@ -13,6 +13,7 @@
</style>
</head>
<body>
{{ header }}
{% for section in layout.sections %}
<div class="section {{ resolve_class({'page-break': section.page_break}) }}">
{% if section.label %}
@@ -30,5 +31,6 @@
</div>
</div>
{% endfor %}
{{ footer }}
</body>
</html>

+ 9
- 0
frappe/templates/print_format/print_format_font.css 查看文件

@@ -0,0 +1,9 @@
@charset "UTF-8";
{% if print_format.font %}
{% set font_family = print_format.font.replace(' ', '+') %}
@import url("https://fonts.googleapis.com/css?family={{ font_family }}:400,500,600,700");
{% endif %}

html, body {
font-family: {{ print_format.font or 'Inter' }}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

+ 24
- 0
frappe/templates/print_format/print_header.html 查看文件

@@ -0,0 +1,24 @@
<style>
{% include "templates/print_format/print_format_font.css" %}

@media print {
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
padding-top: {{ print_format.margin_top | int }}mm;
padding-left: {{ print_format.margin_left | int }}mm;
padding-right: {{ print_format.margin_right | int }}mm;
}
}
</style>
<header>
{%- if letterhead -%}
{{ frappe.render_template(letterhead.content, {'doc': doc}) }}
{%- endif -%}

{%- if layout.header -%}
{{ frappe.render_template(layout.header, {'doc': doc}) }}
{%- endif -%}
</header>

+ 157
- 139
frappe/utils/weasyprint.py 查看文件

@@ -4,146 +4,151 @@
import frappe
from weasyprint import HTML, CSS


@frappe.whitelist()
def download_pdf(doctype, name, print_format, letterhead=None):
html = get_html(doctype, name, print_format, letterhead)
pdf = get_pdf(html.main, html.header, html.footer)
doc = frappe.get_doc(doctype, name)
generator = PrintFormatGenerator(print_format, doc, letterhead)
pdf = generator.render_pdf()

frappe.local.response.filename = "{name}.pdf".format(name=name.replace(" ", "-").replace("/", "-"))
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):
print_format = frappe.get_doc('Print Format', print_format)
letterhead = frappe.get_doc('Letter Head', letterhead) if letterhead else None
layout = frappe.parse_json(print_format.format_data)
doc = frappe.get_doc(doctype, name)
generator = PrintFormatGenerator(print_format, doc, letterhead)
return generator.get_html_preview()

print_settings = frappe.get_doc('Print Settings')

page_width_map = {
'A4': 210,
'Letter': 216
}
page_width = page_width_map.get(print_settings.pdf_page_size) or 210
body_width = page_width - print_format.margin_left - print_format.margin_right

context = frappe._dict({
'doc': doc,
'print_format': print_format,
'print_settings': print_settings,
'layout': layout,
'letterhead': letterhead,
'page_width': page_width,
'body_width': body_width,
'font_family': print_format.font.replace(' ', '+') if print_format.font else None
})
context.css = frappe.render_template('templates/print_format/print_format.css', context)
html = frappe.render_template('templates/print_format/print_format.html', context)

if layout.header:
header = frappe.render_template(layout.header['html'], context)
else:
header = None

if layout.footer:
footer = frappe.render_template(layout.footer['html'], context)
else:
footer = None

return frappe._dict({
'main': html,
'header': header,
'footer': footer,
})


def get_pdf(html, header, footer):
return PdfGenerator(
base_url=frappe.utils.get_url(),
main_html=html,
header_html=header,
footer_html=footer
).render_pdf()


class PdfGenerator:

class PrintFormatGenerator:
"""
Generate a PDF out of a rendered template, with the possibility to integrate nicely
a header and a footer if provided.

Notes:
------
- When Weasyprint renders an html into a PDF, it goes though several intermediate steps.
Here, in this class, we deal mostly with a box representation: 1 `Document` have 1 `Page`
or more, each `Page` 1 `Box` or more. Each box can contain other box. Hence the recursive
method `get_element` for example.
For more, see:
https://weasyprint.readthedocs.io/en/stable/hacking.html#dive-into-the-source
https://weasyprint.readthedocs.io/en/stable/hacking.html#formatting-structure
- Warning: the logic of this class relies heavily on the internal Weasyprint API. This
snippet was written at the time of the release 47, it might break in the future.
- 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
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
"""
OVERLAY_LAYOUT = '@page {size: A4 portrait; margin: 0;}'

def __init__(self, main_html, header_html=None, footer_html=None,
base_url=None, side_margin=2, extra_vertical_margin=30):
def __init__(self, print_format, doc, letterhead=None):
"""
Parameters
----------
main_html: str
An HTML file (most of the time a template rendered into a string) which represents
the core of the PDF to generate.
header_html: str
An optional header html.
footer_html: str
An optional footer html.
base_url: str
An absolute url to the page which serves as a reference to Weasyprint to fetch assets,
required to get our media.
side_margin: int, interpreted in cm, by default 2cm
The margin to apply on the core of the rendered PDF (i.e. main_html).
extra_vertical_margin: int, interpreted in pixel, by default 30 pixels
An extra margin to apply between the main content and header and the footer.
The goal is to avoid having the content of `main_html` touching the header or the
footer.
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
self.letterhead = frappe.get_doc("Letter Head", letterhead) if letterhead else None
self.print_settings = frappe.get_doc("Print Settings")
self.build_context()
self.layout = self.get_layout(self.print_format)
self.context.layout = self.layout

def build_context(self):
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
)
context = frappe._dict(
{
"doc": self.doc,
"print_format": self.print_format,
"print_settings": self.print_settings,
"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):
"""
self.main_html = main_html
self.header_html = header_html
self.footer_html = footer_html
self.base_url = base_url
self.side_margin = side_margin
self.extra_vertical_margin = extra_vertical_margin
Returns
-------
pdf: a bytes sequence
The rendered PDF.
"""
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)
pdf = main_doc.write_pdf()

return pdf

def _compute_overlay_element(self, element: str):
"""
Parameters
----------
element: str
Either 'header' or 'footer'
Either 'header' or 'footer'

Returns
-------
element_body: BlockBox
A Weasyprint pre-rendered representation of an html element
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
The height of this element, which will be then translated in a html height
"""
html = HTML(
string=getattr(self, f'{element}_html'),
base_url=self.base_url,
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_doc = html.render(stylesheets=[CSS(string=self.OVERLAY_LAYOUT)])
element_page = element_doc.pages[0]
element_body = PdfGenerator.get_element(element_page._page_box.all_children(), 'body')
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 = PdfGenerator.get_element(element_page._page_box.all_children(), element)
element_html = PrintFormatGenerator.get_element(
element_page._page_box.all_children(), element
)

if element == 'header':
if element == "header":
element_height = element_html.height
if element == 'footer':
if element == "footer":
element_height = element_page.height - element_html.position_y

return element_body, element_height
@@ -155,54 +160,67 @@ class PdfGenerator:
Parameters
----------
main_doc: Document
The top level representation for a PDF page in Weasyprint.
The top level representation for a PDF page in Weasyprint.
header_body: BlockBox
A representation for an html element in Weasyprint.
A representation for an html element in Weasyprint.
footer_body: BlockBox
A representation for an html element in Weasyprint.
A representation for an html element in Weasyprint.
"""
for page in main_doc.pages:
page_body = PdfGenerator.get_element(page._page_box.all_children(), 'body')
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 render_pdf(self):
"""
Returns
-------
pdf: a bytes sequence
The rendered PDF.
"""
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')
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')
footer_body, footer_height = self._compute_overlay_element("footer")
else:
footer_body, footer_height = None, 0

margins = '{header_size}px {side_margin} {footer_size}px {side_margin}'.format(
header_size=header_height + self.extra_vertical_margin,
footer_size=footer_height + self.extra_vertical_margin,
side_margin=f'{self.side_margin}cm',
)
content_print_layout = '@page {size: A4 portrait; margin: %s;}' % margins

html = HTML(
string=self.main_html,
base_url=self.base_url,
)
main_doc = html.render(stylesheets=[CSS(string=content_print_layout)])

if self.header_html or self.footer_html:
self._apply_overlay_on_main(main_doc, header_body, footer_body)
pdf = main_doc.write_pdf()

return pdf
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"]
df["renderer"] = renderers.get(fieldtype) or fieldtype
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):
@@ -215,4 +233,4 @@ class PdfGenerator:
for box in boxes:
if box.element_tag == element:
return box
return PdfGenerator.get_element(box.all_children(), element)
return PrintFormatGenerator.get_element(box.all_children(), element)

+ 9
- 2
frappe/www/printpreview.html 查看文件

@@ -1,3 +1,10 @@
<!-- </body> -->
---
no_cache: 1
---

{{ html.main }}
<!-- </body> -->
{{
frappe
.get_doc('Print Format', frappe.form_dict.print_format)
.get_html(frappe.form_dict.name, frappe.form_dict.letterhead)
}}

+ 0
- 15
frappe/www/printpreview.py 查看文件

@@ -1,15 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

import frappe
from frappe.utils.weasyprint import get_html

no_cache = 1

def get_context(context):
doctype = frappe.form_dict.doctype
name = frappe.form_dict.name
print_format = frappe.form_dict.print_format
letterhead = frappe.form_dict.letterhead
context.no_cache = 1
context.html = get_html(doctype, name, print_format, letterhead)

Loading…
取消
儲存