Claude
Skills
Sign in
Back

print-format

Included with Lifetime
$97 forever

Frappe Print Format design — Jinja templates for receipts, certificates, invoices; PDF generation; page handling; multi-currency; wkhtmltopdf rendering. Use when designing or modifying a Print Format, troubleshooting PDF rendering, or formatting documents for print. Follows the team convention of self-contained formats — no `letter_head`/`footer` injection.

Design

What this skill does


# Frappe Print Format Reference

Reference for designing Print Formats in Frappe v14+, aligned with the team's edu_quality conventions: **self-contained formats** (no `letter_head` / `footer` injection), `wkhtmltopdf` pinned as the PDF engine, `dd-MM-yyyy` date format, and `Rs.` instead of `₹` to dodge font issues.

## Team conventions

These are the rules, not suggestions. New formats follow them; reviews flag deviations.

1. **Do NOT use `{{ letter_head }}` or `{{ footer }}`.** Every print format is self-contained: header (with logo/QR if needed), body, signature block — all inline in the template. The Letter Head DocType is not part of our print pipeline.
2. **Pin `pdf_generator: "wkhtmltopdf"`** in the Print Format JSON. Don't rely on the system default; v15 introduced Chromium and the team has not adopted it.
3. **Date format: `formatdate(doc.field, "dd-MM-yyyy")`** everywhere. No raw date rendering.
4. **Currency: `fmt_money(amt, currency="INR") | replace("₹", "Rs.")`** or hardcode `Rs.` directly. The wkhtml fonts we ship don't reliably render `₹`; the team standard is `Rs.`.
5. **Margins `15.0` all around**, `page_number: "Hide"`, `standard: "Yes"`, `print_format_type: "Jinja"`.
6. **`module` set to the module name**. Files live at `{app}/{module}/print_format/{slug}/{slug}.json`.

## When to use which type

Frappe supports four ways to define a print format:

| Type | Where it lives | When to use |
|------|----------------|-------------|
| **Standard** | Auto-generated from DocType fields | Quick default for new DocTypes; never edit |
| **Custom (Print Format Builder)** | DB row with `format_data` JSON | Non-developers customizing layout via the UI; commit the JSON via fixtures |
| **Jinja (file-based)** | DB row with raw `html`, exported as JSON | Developers; full control; goes through git |
| **Server-side** | Controller method `get_print_html` | Programmatic generation; e.g. dynamic per-customer layouts |

For anything code-reviewed and version-controlled, use **Jinja** (`custom_format: 1` with hand-written `html`). The Print Format Builder (`custom_format: 0` with `format_data`) is fine for layout-only formats but its JSON isn't readable in diffs.

## File layout

```
my_app/
└── my_module/
    └── print_format/
        └── fee_receipt/
            ├── __init__.py
            └── fee_receipt.json     # Print Format DocType row, exported via bench export-fixtures
```

The HTML lives **inside the JSON** in the `html` field. We don't ship separate `.html` files — keep everything in one fixture row so it round-trips through `bench export-fixtures`.

Register in `hooks.py`:
```python
fixtures = [
    {"dt": "Print Format", "filters": [["module", "=", "Edu Quality"]]}
]
```

## Manifest field reference

A typical hand-written Jinja format JSON:

```json
{
    "doctype": "Print Format",
    "name": "Fee Receipt",
    "doc_type": "Payment Entry",
    "module": "Fees",
    "print_format_type": "Jinja",
    "custom_format": 1,
    "standard": "Yes",
    "pdf_generator": "wkhtmltopdf",
    "default_print_language": "en",
    "page_number": "Hide",
    "font_size": 14,
    "margin_top": 15.0,
    "margin_bottom": 15.0,
    "margin_left": 15.0,
    "margin_right": 15.0,
    "html": "<style>...</style><table>...</table>"
}
```

| Field | Convention |
|-------|------------|
| `doc_type` | The DocType this prints |
| `module` | Sentence Case module name (e.g. `"Fees"`, `"Edu Quality"`) — must match an installed module |
| `print_format_type` | `"Jinja"` always |
| `custom_format` | `1` for hand-written `html`; `0` if using `format_data` (Builder) |
| `standard` | `"Yes"` for shipped formats |
| `pdf_generator` | `"wkhtmltopdf"` pinned |
| `font_size` | `14` is the team default; `16` for receipts |
| `margin_*` | `15.0` all four sides |
| `page_number` | `"Hide"` |

## Jinja context

Inside the template, these variables are available:

| Variable | What it is |
|----------|------------|
| `doc` | The full document being printed (with all child tables) |
| `frappe` | The full `frappe` namespace — `frappe.utils.*`, `frappe.db.*` |
| `_` | Translation function — `_("Total")` |
| `print_settings` | The `Print Settings` Single doc |
| `lang` | Language code (`"en"`, `"hi"`, ...) |

Note `letter_head` and `footer` are populated by Frappe but the team convention is **don't reference them** — every format is self-contained.

## Inline DB lookups

Templates pulling from related DocTypes use `frappe.db.get_value` directly in Jinja. This is the team idiom for receipts that need data the Payment Entry doesn't carry:

```jinja
{% set reference_number = frappe.db.get_value('Student', doc.party, 'reference_number') %}
{% set program = frappe.db.get_value(doc.reference_doctype, doc.reference_name, 'program') %}
{% set class = frappe.db.get_value('Program', program, 'program_name') %}

<p><b>Receipt No:</b> {{ doc.name }}</p>
<p><b>Reference:</b> {{ reference_number }}</p>
<p><b>Class:</b> {{ class }}</p>
```

Each `frappe.db.get_value` is one query. For more than 5–6 lookups in a template, do them in a controller `get_context` instead — Jinja loops with DB lookups inside become N+1 problems on bulk print runs.

## Currency formatting

The team standard is `Rs.` (not `₹`) for INR. Two ways:

```jinja
{# Option 1 — hardcode prefix #}
<td>Rs. {{ "{:,.2f}".format(item.amount) }}</td>

{# Option 2 — fmt_money with replace #}
<td>{{ frappe.utils.fmt_money(item.amount, currency="INR") | replace("₹", "Rs.") }}</td>
```

Option 2 picks up the precision configured on the Currency doctype; Option 1 is simpler when precision is fixed at 2.

For multi-currency (rare in our stack — almost everything is INR), pass the currency from the doc:
```jinja
{{ frappe.utils.fmt_money(doc.grand_total, currency=doc.currency) | replace("₹", "Rs.") }}
```

In-words (common on receipts):
```jinja
{{ frappe.utils.money_in_words(doc.grand_total, "INR") }}
```

## Date and time formatting

The team convention is `dd-MM-yyyy`:

```jinja
{{ frappe.utils.formatdate(doc.posting_date, "dd-MM-yyyy") }}
{# → "15-01-2026" #}
```

Other formats when needed:
```jinja
{{ frappe.utils.formatdate(doc.posting_date, "dd MMM yyyy") }}      {# 15 Jan 2026 #}
{{ frappe.utils.format_time(doc.posting_time, "HH:mm") }}            {# 14:30 #}
{{ frappe.utils.format_datetime(doc.creation, "dd-MM-yyyy HH:mm") }} {# 15-01-2026 14:30 #}

{# Quick "today" stamp inside the template #}
{{ frappe.utils.nowdate() }}
```

Never render `doc.posting_date` raw — it's a `datetime.date` object and prints inconsistently across PDF engines.

## Page breaks and table flow

Force a page break:
```html
<div class="page-break"></div>
```

For tables that flow across pages, repeat the header by using a proper `<thead>`:

```html
<table>
  <thead>
    <tr><th>Item</th><th>Qty</th><th>Rate</th><th>Amount</th></tr>
  </thead>
  <tbody>
    {% for row in doc.items %}
    <tr style="page-break-inside: avoid;">
      <td>{{ row.item_code }}</td>
      <td class="text-right">{{ row.qty }}</td>
      <td class="text-right">Rs. {{ "{:,.2f}".format(row.rate) }}</td>
      <td class="text-right">Rs. {{ "{:,.2f}".format(row.amount) }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>
```

For a signature/footer block that should never split:
```html
<div style="page-break-inside: avoid; margin-top: 36pt;">
  <p>For {{ frappe.db.get_value("Company", doc.company, "company_name") }}</p>
  <div style="height: 36pt;"></div>
  <hr>
  <small>Authorized Signatory</small>
</div>
```

## Custom CSS

Inline at the top of the template's `html`. Frappe wraps the body so a `<head>` block is ignored — `<style>` must be in the body.

```html
<style>
  table, th, td, tr {
    font-size: 16px;
    border: 2px solid black;
  }
  th {
    font-weight: bold;
    color: #000;
    background-color: #D3D3D3;
  }
  .description { text-align: center; }
  .text-right { text-align: right; }
  p { font-size: 16px; }
  @media print {
    .no-print { display: none; }
  }
</style>
``

Related in Design