Frappe Printing & Templates
Create print formats, email templates, and document templates using Jinja in Frappe.
When to use
-
Creating custom print formats for documents
-
Building email templates with dynamic content
-
Generating PDFs from documents
-
Using Jinja templating in web pages
-
Configuring letter heads for branding
-
Using the Print Format Builder
Inputs required
-
Target DocType for the print format
-
Layout requirements (fields, tables, headers)
-
Whether format is standard (version controlled) or custom (DB-stored)
-
Letter Head / branding requirements
-
PDF generation needs
Procedure
- Choose format type
Type How to Create Version Controlled Customizable by User
Standard Developer Mode, saved as JSON Yes No
Print Format Builder Drag-and-drop UI No (DB) Yes
Custom HTML (Jinja) Type "new print format" in awesomebar Optional Depends
- Create a Jinja print format
Create via awesomebar → "New Print Format":
-
Set a unique name
-
Link to the target DocType
-
Set "Standard" = "No" (or "Yes" for dev mode export)
-
Check "Custom Format"
-
Set Print Format Type = "Jinja"
-
Write your Jinja HTML
<div class="print-format"> <h1>{{ doc.name }}</h1> <p><strong>{{ _("Customer") }}:</strong> {{ doc.customer }}</p> <p><strong>{{ _("Date") }}:</strong> {{ frappe.format_date(doc.transaction_date) }}</p>
<table class="table table-bordered">
<thead>
<tr>
<th>{{ _("Item") }}</th>
<th>{{ _("Qty") }}</th>
<th class="text-right">{{ _("Rate") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
</thead>
<tbody>
{% for item in doc.items %}
<tr>
<td>{{ item.item_name }}</td>
<td>{{ item.qty }}</td>
<td class="text-right">{{ frappe.format(item.rate, {'fieldtype': 'Currency'}) }}</td>
<td class="text-right">{{ frappe.format(item.amount, {'fieldtype': 'Currency'}) }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right"><strong>{{ _("Total") }}</strong></td>
<td class="text-right"><strong>{{ frappe.format(doc.grand_total, {'fieldtype': 'Currency'}) }}</strong></td>
</tr>
</tfoot>
</table>
{% if doc.terms %}
<div class="terms">
<h4>{{ _("Terms & Conditions") }}</h4>
<p>{{ doc.terms }}</p>
</div>
{% endif %}
</div>
<style> .print-format { font-family: Arial, sans-serif; } .print-format h1 { color: #333; } .print-format table { width: 100%; margin-top: 20px; } </style>
- Use Frappe Jinja API
Data fetching in templates:
{# Fetch a document #} {% set customer = frappe.get_doc('Customer', doc.customer) %} {{ customer.customer_name }}
{# List query (ignores permissions) #} {% set open_orders = frappe.get_all('Sales Order', filters={'customer': doc.customer, 'status': 'To Deliver and Bill'}, fields=['name', 'grand_total'], order_by='creation desc', page_length=5) %}
{# Permission-aware list query #} {% set my_tasks = frappe.get_list('Task', filters={'owner': frappe.session.user}) %}
{# Single value lookup #} {% set company_abbr = frappe.db.get_value('Company', doc.company, 'abbr') %}
{# Settings value #} {% set timezone = frappe.db.get_single_value('System Settings', 'time_zone') %}
Formatting:
{{ frappe.format(50000, {'fieldtype': 'Currency'}) }} {{ frappe.format_date('2025-01-15') }} {{ frappe.format_date(doc.posting_date) }}
Session and context:
{{ frappe.session.user }} {{ frappe.get_fullname() }} {{ frappe.lang }} {{ _("Translatable string") }}
URLs:
<a href="{{ frappe.get_url() }}/app/sales-order/{{ doc.name }}">View Order</a>
- Build email templates
Dear {{ doc.customer_name }},
Your order {{ doc.name }} has been confirmed.
Items: {% for item in doc.items %}
- {{ item.item_name }} x {{ item.qty }} {% endfor %}
Total: {{ frappe.format(doc.grand_total, {'fieldtype': 'Currency'}) }}
Thank you, {{ frappe.get_fullname() }}
- Generate PDFs programmatically
import frappe
Generate PDF
pdf_content = frappe.get_print( doctype="Sales Invoice", name="SINV-001", print_format="Custom Invoice", as_pdf=True )
Attach PDF to document
frappe.attach_print( doctype="Sales Invoice", name="SINV-001", print_format="Custom Invoice", file_name="invoice.pdf" )
Send with email
frappe.sendmail( recipients=["customer@example.com"], subject="Your Invoice", message="Please find attached your invoice.", attachments=[{ "fname": "invoice.pdf", "fcontent": pdf_content }] )
- Configure Letter Head
-
Navigate to Letter Head list → New
-
Upload company logo and header image
-
Set as default for the company
-
Letter Head appears automatically on print formats
- Use Jinja filters
{{ doc.customer_name|upper }} {# UPPERCASE #} {{ doc.notes|truncate(100) }} {# Truncate text #} {{ doc.description|striptags }} {# Remove HTML #} {{ doc.html_content|safe }} {# Render raw HTML (trusted only!) #} {{ items|length }} {# Count items #} {{ items|first }} {# First item #} {{ names|join(', ') }} {# Join list #} {{ amount|round(2) }} {# Round number #} {{ value|default('N/A') }} {# Default if undefined #} {{ data|tojson }} {# Convert to JSON #}
- Template inheritance and macros
{# macros/fields.html #} {% macro field_row(label, value) %} <tr> <td class="label"><strong>{{ _(label) }}</strong></td> <td>{{ value }}</td> </tr> {% endmacro %}
{# In print format #} {% from "macros/fields.html" import field_row %} <table> {{ field_row("Customer", doc.customer_name) }} {{ field_row("Date", frappe.format_date(doc.posting_date)) }} {{ field_row("Total", frappe.format(doc.grand_total, {'fieldtype': 'Currency'})) }} </table>
Verification
-
Print format renders correctly in Print View
-
All fields display with proper formatting
-
PDF generation works without errors
-
Email templates render with correct data
-
Letter Head appears on printed documents
-
Translations work in templates (_() )
-
No XSS risks from unescaped content
Failure modes / debugging
-
Template syntax error: Check Jinja delimiters ({{ }} , {% %} ); look for unclosed blocks
-
Field not rendering: Verify field name matches DocType schema; check child table access pattern
-
PDF generation fails: Check wkhtmltopdf installation; verify print format Jinja is valid
-
Styling issues in PDF: Use inline styles; avoid complex CSS; test with Print View first
-
Permission error in template: Use frappe.get_all (no permission check) vs frappe.get_list
Escalation
-
For app-level hooks and structure → frappe-app-development
-
For DocType schema questions → frappe-doctype-development
References
-
references/jinja.md — Jinja templating and Frappe Jinja API
-
references/printing.md — Print formats and PDF generation
Guardrails
-
Test with actual data: Always preview with real documents; edge cases break templates
-
Handle missing fields gracefully: Use {{ doc.field or '' }} or {% if doc.field %}
-
Use get_url() for images: Never hardcode URLs; use {{ frappe.utils.get_url() }}/files/...
-
Escape user content: Use {{ value | e }} for user-generated content to prevent XSS
-
Keep styling inline: PDF generators don't support external CSS; use inline style attributes
Common Mistakes
Mistake Why It Fails Fix
Wrong Jinja syntax Template error, blank output Use {{ }} for output, {% %} for logic; check closing tags
Missing filters Raw data displayed Use frappe.format() or frappe.format_date() for formatting
Hardcoded URLs Images/links break across sites Use {{ frappe.utils.get_url() }} for absolute URLs
Accessing child table wrong Empty or error Use {% for item in doc.items %} not doc.child_table_name
Complex CSS in print format Styling lost in PDF Use inline styles, simple layouts, <table> for structure
Not handling None values 'None' string in output Use {{ value or '' }} or {% if value %}