Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-impl-jinja && cp -r /tmp/frappe-impl-jinja/skills/source/impl/frappe-impl-jinja ~/.claude/skills/frappe-impl-jinja
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Frappe Jinja Templates Implementation Workflow

Step-by-step workflows for building Jinja templates. For syntax reference, see `frappe-syntax-jinja`.

**Version**: v14/v15/v16 (V16 Chrome PDF noted)

---

## Master Decision: What Are You Creating?

```
WHAT IS YOUR OUTPUT?
│
├─► Printable PDF (invoice, PO, report)?
│   ├─► Standard DocType → Print Format (Jinja)
│   └─► Query/Script Report → Report Print Format (JAVASCRIPT!)
│       ⚠️ Uses {%= %} NOT {{ }}
│
├─► Automated email with dynamic content?
│   └─► Email Template (Jinja, linked to DocType)
│
├─► System notification?
│   └─► Notification (Setup > Notification, uses Jinja)
│
├─► Customer-facing web page?
│   └─► Portal Page (myapp/www/*.html + *.py)
│
└─► Reusable template functions/filters?
    └─► Custom jenv methods in hooks.py
```

---

## Workflow 1: Create a Print Format

### Step 1: Create via UI

```
Setup > Printing > Print Format > New
- Name: My Invoice Format
- DocType: Sales Invoice
- Module: Accounts
- Standard: No (custom)
- Print Format Type: Jinja
```

### Step 2: Write the Template

```jinja
<style>
    .print-format { font-family: Arial, sans-serif; font-size: 11px; }
    .header { margin-bottom: 20px; }
    .table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    .table th, .table td { border: 1px solid #ddd; padding: 8px; }
    .table th { background: #f0f0f0; }
    .text-right { text-align: right; }
</style>

<div class="header">
    <h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
    <p><strong>{{ doc.name }}</strong> |
       {{ doc.get_formatted("posting_date") }}</p>
</div>

<p><strong>{{ doc.customer_name }}</strong></p>
{% if doc.address_display %}
    <p>{{ doc.address_display | safe }}</p>
{% endif %}

<table class="table">
    <thead>
        <tr>
            <th>#</th>
            <th>{{ _("Item") }}</th>
            <th class="text-right">{{ _("Qty") }}</th>
            <th class="text-right">{{ _("Rate") }}</th>
            <th class="text-right">{{ _("Amount") }}</th>
        </tr>
    </thead>
    <tbody>
        {% for row in doc.items %}
        <tr>
            <td>{{ row.idx }}</td>
            <td>{{ row.item_name }}</td>
            <td class="text-right">{{ row.qty }}</td>
            <td class="text-right">{{ row.get_formatted("rate", doc) }}</td>
            <td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

{% for tax in doc.taxes %}
<p class="text-right">{{ tax.description }}: {{ tax.get_formatted("tax_amount", doc) }}</p>
{% endfor %}

<p class="text-right">
    <strong>{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}</strong>
</p>

{% if doc.terms %}
<div style="margin-top: 30px; border-top: 1px solid #ddd; padding-top: 10px;">
    <strong>{{ _("Terms and Conditions") }}</strong>
    {{ doc.terms | safe }}
</div>
{% endif %}
```

### Step 3: Test

1. Open a Sales Invoice
2. Menu > Print > Select "My Invoice Format"
3. Verify layout and formatting
4. **ALWAYS** test PDF download — wkhtmltopdf renders differently from browser

### Critical Rules for Print Formats

- **ALWAYS** use `doc.get_formatted("field")` for currency, dates, numbers
- **ALWAYS** pass parent doc for child rows: `row.get_formatted("rate", doc)`
- **ALWAYS** wrap user-facing text with `_("text")` for translation
- **ALWAYS** put CSS in a `<style>` block at the top (not external files)
- **NEVER** use flexbox in v14/v15 (wkhtmltopdf does not support it) — V16 Chrome PDF does
- **NEVER** use `| safe` on user-supplied input — only on trusted system HTML

---

## Workflow 2: Create an Email Template

### Step 1: Create via UI

```
Setup > Email > Email Template > New
- Name: Payment Reminder
- Subject: Invoice {{ doc.name }} - Payment Reminder
- DocType: Sales Invoice
```

### Step 2: Write Email Content

**ALWAYS** use inline styles for emails — most clients strip `<style>` blocks.

```jinja
<div style="font-family: Arial, sans-serif; max-width: 600px;">
    <p>{{ _("Dear") }} {{ doc.customer_name }},</p>

    <p>{{ _("Invoice") }} <strong>{{ doc.name }}</strong>
    {{ _("for") }} {{ doc.get_formatted("grand_total") }}
    {{ _("is due for payment.") }}</p>

    <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
        <tr style="background: #f5f5f5;">
            <td style="padding: 10px; border: 1px solid #ddd;">
                <strong>{{ _("Due Date") }}</strong></td>
            <td style="padding: 10px; border: 1px solid #ddd;">
                {{ frappe.format_date(doc.due_date) }}</td>
        </tr>
        <tr>
            <td style="padding: 10px; border: 1px solid #ddd;">
                <strong>{{ _("Outstanding") }}</strong></td>
            <td style="padding: 10px; border: 1px solid #ddd; color: #c00;">
                {{ doc.get_formatted("outstanding_amount") }}</td>
        </tr>
    </table>

    {% if doc.items %}
    <p><strong>{{ _("Items") }}:</strong></p>
    <ul>
    {% for item in doc.items[:5] %}
        <li>{{ item.item_name }} ({{ item.qty }})</li>
    {% endfor %}
    {% if doc.items | length > 5 %}
        <li style="color: #666;">{{ _("and {0} more...").format(doc.items|length - 5) }}</li>
    {% endif %}
    </ul>
    {% endif %}

    <p>{{ _("Best regards") }},<br>
    {{ frappe.db.get_value("Company", doc.company, "company_name") }}</p>
</div>
```

### Step 3: Use in Notification or Code

**Option A: Auto-triggered Notification**

```
Setup > Notification > New
- Channel: Email
- Document Type: Sales Invoice
- Send Alert On: Days After (7 days after due_date)
- Condition: doc.outstanding_amount > 0
- Email Template: Payment Reminder
```

**Option B: Send from code**

```python
template = frappe.get_doc("Email Template", "Payment Reminder")
frappe.sendmail(
    recipients=[doc.contact_email],
    subject=frappe.render_template(template.subject, {"doc": doc}),
    message=frappe.render_template(template.response, {"doc": doc}),
    reference_doctype=do