← Back to list

erpnext-impl-jinja
by OpenAEC-Foundation
28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.
⭐ 1🍴 1📅 Jan 23, 2026
SKILL.md
name: erpnext-impl-jinja description: "Implementation workflows and decision trees for Jinja templates in ERPNext/Frappe. Use when determining HOW to implement Print Formats, Email Templates, Portal Pages, or custom Jinja methods. Covers template type selection, context variables, styling, and V16 Chrome PDF rendering. Triggers: create print format, email template, portal page, custom jinja filter, print format styling, pdf template, invoice template, report template."
ERPNext 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 |
Score
Total Score
75/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
Reviews
💬
Reviews coming soon

