Back to list
OpenAEC-Foundation

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

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 TypeAvailable Objects
Print Formatdoc, frappe, _()
Email Templatedoc, frappe (limited)
Portal Pagefrappe.session, frappe.form_dict, custom context
Notificationdoc, frappe

Quick Reference: Essential Methods

NeedMethod
Format currency/datedoc.get_formatted("fieldname")
Format child rowrow.get_formatted("field", doc)
Translate string_("String")
Get linked docfrappe.get_doc("DocType", name)
Get single fieldfrappe.db.get_value("DT", name, "field")
Current datefrappe.utils.nowdate()
Format datefrappe.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

FeatureV14V15V16
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

FileContents
decision-tree.mdComplete template type selection
workflows.mdStep-by-step implementation patterns
examples.mdComplete working examples
anti-patterns.mdCommon 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