erpnext-impl-jinja
npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-impl-jinja --openclawERPNext Jinja Templates - Implementation
This skill helps you determine HOW to implement Jinja templates. For exact syntax, see erpnext-syntax-jinja.
Version: v14/v15/v16 compatible (with V16-specific features noted)
Main Decision: What Are You Trying to Create?
┌─────────────────────────────────────────────────────────────────────────┐
│ WHAT DO YOU WANT TO CREATE? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► Printable document (invoice, PO, report)? │
│ ├── Standard DocType → Print Format (Jinja) │
│ └── Query/Script Report → Report Print Format (JavaScript!) │
│ │
│ ► Automated email with dynamic content? │
│ └── Email Template (Jinja) │
│ │
│ ► Customer-facing web page? │
│ └── Portal Page (www/*.html + *.py) │
│ │
│ ► Reusable template functions/filters? │
│ └── Custom jenv methods in hooks.py │
│ │
│ ► Notification content? │
│ └── Notification Template (uses Jinja syntax) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
⚠️ CRITICAL: Report Print Formats use JAVASCRIPT templating, NOT Jinja!
- Jinja: {{ variable }}
- JS Report: {%= variable %}
Decision Tree: Print Format Type
WHAT ARE YOU PRINTING?
│
├─► Standard DocType (Invoice, PO, Quotation)?
│ │
│ │ WHERE TO CREATE?
│ ├─► Quick/simple format → Print Format Builder (Setup > Print)
│ │ - Drag-drop interface
│ │ - Limited customization
│ │
│ └─► Complex layout needed → Custom HTML Print Format
│ - Full Jinja control
│ - Custom CSS styling
│ - Dynamic logic
│
├─► Query Report or Script Report?
│ └─► Report Print Format (JAVASCRIPT template!)
│ ⚠️ NOT Jinja! Uses {%= %} and {% %}
│
└─► Letter or standalone document?
└─► Letter Head + Print Format combination
Decision Tree: Where to Store Template
IS THIS A ONE-OFF OR REUSABLE?
│
├─► Site-specific, managed via UI?
│ └─► Create via Setup > Print Format / Email Template
│ - Stored in database
│ - Easy to edit without code
│
├─► Part of your custom app?
│ │
│ │ WHAT TYPE?
│ ├─► Print Format → myapp/fixtures or db records
│ │
│ ├─► Portal Page → myapp/www/pagename/
│ │ - index.html (template)
│ │ - index.py (context)
│ │
│ └─► Custom methods/filters → myapp/jinja/
│ - Registered via hooks.py jenv
│
└─► Template for multiple sites?
└─► Include in app, export as fixture
Implementation Workflow: Print Format
Step 1: Create via UI (Recommended Start)
Setup > Printing > Print Format > New
- DocType: Sales Invoice
- Module: Accounts
- Standard: No (Custom)
- Print Format Type: Jinja
Step 2: Basic Template Structure
{# ALWAYS include styles at top #}
<style>
.print-format { font-family: Arial, sans-serif; }
.header { background: #f5f5f5; padding: 15px; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; }
.text-right { text-align: right; }
.footer { margin-top: 30px; border-top: 1px solid #ddd; }
</style>
{# Document header #}
<div class="header">
<h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
<p><strong>{{ doc.name }}</strong></p>
<p>{{ _("Date") }}: {{ doc.get_formatted("posting_date") }}</p>
</div>
{# Items table #}
<table class="table">
<thead>
<tr>
<th>{{ _("Item") }}</th>
<th class="text-right">{{ _("Qty") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
</thead>
<tbody>
{% for row in doc.items %}
<tr>
<td>{{ row.item_name }}</td>
<td class="text-right">{{ row.qty }}</td>
<td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Totals #}
<div class="text-right">
<p><strong>{{ _("Grand Total") }}:</strong> {{ doc.get_formatted("grand_total") }}</p>
</div>
Step 3: Test and Refine
1. Open a document (e.g., Sales Invoice)
2. Menu > Print > Select your format
3. Check layout, adjust CSS as needed
4. Test PDF generation
Implementation Workflow: Email Template
Step 1: Create via UI
Setup > Email > Email Template > New
- Name: Payment Reminder
- Subject: Invoice {{ doc.name }} - Payment Due
- DocType: Sales Invoice
Step 2: Template Content
<p>{{ _("Dear") }} {{ doc.customer_name }},</p>
<p>{{ _("This is a reminder that invoice") }} <strong>{{ doc.name }}</strong>
{{ _("for") }} {{ doc.get_formatted("grand_total") }} {{ _("is due.") }}</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">
<strong>{{ _("Due Date") }}</strong>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
{{ frappe.format_date(doc.due_date) }}
</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">
<strong>{{ _("Outstanding") }}</strong>
</td>
<td style="padding: 8px; border: 1px solid #ddd;">
{{ doc.get_formatted("outstanding_amount") }}
</td>
</tr>
</table>
{% if doc.items %}
<p><strong>{{ _("Items") }}:</strong></p>
<ul>
{% for item in doc.items %}
<li>{{ item.item_name }} ({{ item.qty }})</li>
{% endfor %}
</ul>
{% endif %}
<p>{{ _("Best regards") }},<br>
{{ frappe.db.get_value("Company", doc.company, "company_name") }}</p>
Step 3: Use in Notifications or Code
# In Server Script or Controller
frappe.sendmail(
recipients=[doc.email],
subject=frappe.render_template(
frappe.db.get_value("Email Template", "Payment Reminder", "subject"),
{"doc": doc}
),
message=frappe.get_template("Payment Reminder").render({"doc": doc})
)
Implementation Workflow: Portal Page
Step 1: Create Directory Structure
myapp/
└── www/
└── projects/
├── index.html # Jinja template
└── index.py # Python context
Step 2: Create Template (index.html)
{% extends "templates/web.html" %}
{% block title %}{{ _("Projects") }}{% endblock %}
{% block page_content %}
<div class="container">
<h1>{{ title }}</h1>
{% if frappe.session.user != 'Guest' %}
<p>{{ _("Welcome") }}, {{ frappe.get_fullname() }}</p>
{% endif %}
<div class="row">
{% for project in projects %}
<div class="col-md-4">
<div class="card">
<h3>{{ project.title }}</h3>
<p>{{ project.description | truncate(100) }}</p>
<a href="/projects/{{ project.name }}">{{ _("View Details") }}</a>
</div>
</div>
{% else %}
<p>{{ _("No projects found.") }}</p>
{% endfor %}
</div>
</div>
{% endblock %}
Step 3: Create Context (index.py)
import frappe
def get_context(context):
context.title = "Projects"
context.no_cache = True # Dynamic content
# Fetch data
context.projects = frappe.get_all(
"Project",
filters={"is_public": 1},
fields=["name", "title", "description"],
order_by="creation desc"
)
return context
Step 4: Test
Visit: https://yoursite.com/projects
Implementation Workflow: Custom Jinja Methods
Step 1: Register in hooks.py
# myapp/hooks.py
jenv = {
"methods": ["myapp.jinja.methods"],
"filters": ["myapp.jinja.filters"]
}
Step 2: Create Methods Module
# myapp/jinja/methods.py
import frappe
def get_company_logo(company):
"""Returns company logo URL - usable in any template"""
return frappe.db.get_value("Company", company, "company_logo") or ""
def get_address_display(address_name):
"""Format address for display"""
if not address_name:
return ""
return frappe.get_doc("Address", address_name).get_display()
def get_outstanding_amount(customer):
"""Get total outstanding for customer"""
result = frappe.db.sql("""
SELECT COALESCE(SUM(outstanding_amount), 0)
FROM `tabSales Invoice`
WHERE customer = %s AND docstatus = 1
""", customer)
return result[0][0] if result else 0
Step 3: Create Filters Module
# myapp/jinja/filters.py
def format_phone(value):
"""Format phone number: 1234567890 → (123) 456-7890"""
if not value:
return ""
digits = ''.join(c for c in str(value) if c.isdigit())
if len(digits) == 10:
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
return value
def currency_words(amount, currency="EUR"):
"""Convert number to words (simplified)"""
return f"{currency} {amount:,.2f}"
Step 4: Use in Templates
{# Methods - called as functions #}
<img src="{{ get_company_logo(doc.company) }}" alt="Logo">
<p>{{ get_address_display(doc.customer_address) }}</p>
<p>Outstanding: {{ get_outstanding_amount(doc.customer) }}</p>
{# Filters - piped after values #}
<p>Phone: {{ doc.phone | format_phone }}</p>
<p>Amount: {{ doc.grand_total | currency_words }}</p>
Step 5: Deploy
bench --site sitename migrate
Quick Reference: Context Variables
| Template Type | Available Objects |
|---|---|
| Print Format | doc, frappe, _() |
| Email Template | doc, frappe (limited) |
| Portal Page | frappe.session, frappe.form_dict, custom context |
| Notification | doc, frappe |
Quick Reference: Essential Methods
| Need | Method |
|---|---|
| Format currency/date | doc.get_formatted("fieldname") |
| Format child row | row.get_formatted("field", doc) |
| Translate string | _("String") |
| Get linked doc | frappe.get_doc("DocType", name) |
| Get single field | frappe.db.get_value("DT", name, "field") |
| Current date | frappe.utils.nowdate() |
| Format date | frappe.format_date(date) |
Critical Rules
1. ALWAYS use get_formatted for display values
{# ❌ Raw database value #}
{{ doc.grand_total }}
{# ✅ Properly formatted with currency #}
{{ doc.get_formatted("grand_total") }}
2. ALWAYS pass parent doc for child table formatting
{% for row in doc.items %}
{# ❌ Missing currency context #}
{{ row.get_formatted("rate") }}
{# ✅ Has currency context from parent #}
{{ row.get_formatted("rate", doc) }}
{% endfor %}
3. ALWAYS use translation function for user text
{# ❌ Not translatable #}
<h1>Invoice</h1>
{# ✅ Translatable #}
<h1>{{ _("Invoice") }}</h1>
4. NEVER use Jinja in Report Print Formats
<!-- Query/Script Reports use JAVASCRIPT templating -->
{% for(var i=0; i<data.length; i++) { %}
<tr><td>{%= data[i].name %}</td></tr>
{% } %}
5. NEVER execute queries in loops
{# ❌ N+1 query problem #}
{% for item in doc.items %}
{% set stock = frappe.db.get_value("Bin", ...) %}
{% endfor %}
{# ✅ Prefetch data in controller/context #}
{% for item in items_with_stock %}
{{ item.stock_qty }}
{% endfor %}
Version Differences
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| Jinja templates | ✅ | ✅ | ✅ |
| get_formatted() | ✅ | ✅ | ✅ |
| jenv hooks | ✅ | ✅ | ✅ |
| wkhtmltopdf PDF | ✅ | ✅ | ⚠️ |
| Chrome PDF | ❌ | ❌ | ✅ |
V16 Chrome PDF Considerations
See erpnext-syntax-jinja for detailed Chrome PDF documentation.
Reference Files
| File | Contents |
|---|---|
| decision-tree.md | Complete template type selection |
| workflows.md | Step-by-step implementation patterns |
| examples.md | Complete working examples |
| anti-patterns.md | Common mistakes to avoid |
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/impl/erpnext-impl-jinja/SKILL.mdView on GitHub Overview
This skill guides how to implement Jinja templates in ERPNext/Frappe, covering Print Formats, Email Templates, Portal Pages, and custom Jinja methods. It explains template type selection, context variables, styling, and V16 Chrome PDF rendering, with triggers like creating templates or applying styling. Practical decision trees help you choose the right approach for each use case.
How This Skill Works
Start with a high-level decision of what you want to create (printable document, automated email, portal page, or reusable Jinja components), then follow storage and template-type guidance. Use Jinja for standard Print Formats and avoid Jinja in report print formats (which use JavaScript templating). The skill also covers where to store templates (database vs app fixtures) and how to register custom methods/filters via hooks.py jenv, with attention to V16 Chrome PDF rendering nuances.
When to Use It
- Designing a Jinja-based Print Format for a standard DocType (e.g., Sales Invoice).
- Building an automated Email Template that uses dynamic content and customer data.
- Creating a Portal Page (web page) to present data to customers with Jinja-driven context.
- Implementing reusable Jinja methods/filters via custom app code (hooks.py jenv).
- Crafting a Notification Template that leverages Jinja syntax for alert content.
Quick Start
- Step 1: Decide what you are creating (Print Format, Email Template, Portal Page, or custom Jinja components) and choose the appropriate template type.
- Step 2: Create or locate the template via Setup > Print Format / Email Template, or add Portal Page files under myapp/www and myapp/ (index.html, index.py). Register any custom Jinja methods/filters in hooks.py jenv if needed.
- Step 3: Define context variables, write your Jinja blocks, apply styling, and test rendering in ERPNext (including Chrome PDF rendering for V16).
Best Practices
- Choose the correct template type up front: Print Format for standard Doctype; Report Print Format uses JavaScript, not Jinja.
- Store templates where you need them most: in the database via Setup > Print Format / Email Template or in your app fixtures for portability.
- Keep styling separate from logic: use a dedicated CSS block or file in a Custom HTML Print Format to maintain consistent rendering.
- Test across environments and versions, paying special attention to V16 Chrome PDF rendering quirks.
- Explicitly define and document the required context variables to simplify debugging and future maintenance.
Example Use Cases
- A Sales Invoice Print Format that uses Jinja to calculate and display a custom discount and tax line.
- An Email Template that personalizes greetings and dynamically shows due dates and preferred contact methods.
- A Portal Page with index.html and index.py providing a list of open invoices for the logged-in customer.
- A custom Jinja filter (e.g., format_currency) registered via hooks.py to standardize currency formatting across templates.
- An Invoice PDF Print Format optimized for Chrome PDF rendering in V16 with CSS adjustments for fonts and margins.