Skip to content
erpnext-impl-jinja logo

ERPNext Jinja Templates - Implementation

erpnext-impl-jinja

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: creat...

SKILL.md

Full skill instructions

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 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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View