Get the FREE Ultimate OpenClaw Setup Guide →

erpnext-impl-jinja

npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-impl-jinja --openclaw
Files (1)
SKILL.md
13.8 KB

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

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

  1. Step 1: Decide what you are creating (Print Format, Email Template, Portal Page, or custom Jinja components) and choose the appropriate template type.
  2. 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.
  3. 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.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers