The invoice your customer receives is rarely the Standard layout. Somebody wants the bank details in the footer, the HSN or tax code next to each item, and the logo a little smaller. All of that lives in a Print Format.
Three kinds you’ll run into
Standard. Every DocType gets one automatically, built from the form layout. You can’t edit it. Hiding a field from it is done in Customize Form with the Print Hide checkbox on that field.
Built in the Print Format Builder. A drag-and-drop editor where you move fields onto the page, arrange sections and columns, and drop in Custom HTML blocks for text the form doesn’t have. It’s good for rearranging, and it gets awkward once you need conditions or loops.
Custom Format. Tick Custom Format on a Print Format record and you write the template yourself. The type is usually Jinja, rendered on the server in a sandboxed Jinja environment with the document available as doc and a limited frappe namespace for helpers. There’s also a JS type, and a Raw Printing option where the template produces raw printer commands instead of HTML, for printers that expect them.
A Print Format can target a Report instead of a DocType, and it can carry its own CSS, margins and default language. On v16 the PDF Generator field offers chrome next to wkhtmltopdf.
What a Jinja format looks like
<h2>{{ doc.select_print_heading or _("Tax Invoice") }}</h2>
<p>{{ doc.customer_name }}<br>{{ doc.get_formatted("posting_date") }}</p>
<table class="table table-bordered">
{% for row in doc.items %}
<tr>
<td>{{ row.idx }}</td>
<td>{{ row.item_name }}</td>
<td class="text-right">{{ row.get_formatted("qty") }}</td>
<td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
</tr>
{% endfor %}
</table>
<p class="text-right"><strong>{{ doc.get_formatted("grand_total") }}</strong></p>
Use get_formatted rather than the raw value, so dates follow system settings and currency shows symbols and precision. For currency fields on child rows, pass the parent doc as the second argument so the right currency is used.
Problems we see in real templates
- Queries inside loops. Calling
frappe.db.get_valuefor every item row works on a ten-line invoice and drags on a five-hundred-line delivery note. Fetch what you need once, or set it on the document before printing with a Before Print Server Script or abefore_printcontroller hook. - Fields that were renamed. An upgrade or a renamed Custom Field doesn’t always raise an error in a template. Print a real document on staging after every upgrade.
- Formats that only exist on one site. A Print Format made in the browser lives in that site’s database. With developer mode on, setting Standard to Yes and choosing a Module writes it into the app’s module folder instead; exporting it as a fixture by name also works.
erpfly writes customer-facing formats as Jinja inside the app, with the letterhead and tax breakdown the way your current invoices look, so a layout change shows up as a diff in the pull request. You can see how print output fits the billing flow on the invoice management module page, and the broader ERPNext customization page covers where formats sit next to fields and workflows.