erpnext-impl-jinja

ERPNext Jinja Templates - Implementation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-impl-jinja" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-impl-jinja

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' %}
    &#x3C;p>{{ _("Welcome") }}, {{ frappe.get_fullname() }}&#x3C;/p>
{% endif %}

&#x3C;div class="row">
    {% for project in projects %}
    &#x3C;div class="col-md-4">
        &#x3C;div class="card">
            &#x3C;h3>{{ project.title }}&#x3C;/h3>
            &#x3C;p>{{ project.description | truncate(100) }}&#x3C;/p>
            &#x3C;a href="/projects/{{ project.name }}">{{ _("View Details") }}&#x3C;/a>
        &#x3C;/div>
    &#x3C;/div>
    {% else %}
    &#x3C;p>{{ _("No projects found.") }}&#x3C;/p>
    {% endfor %}
&#x3C;/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") }}

  1. 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 %}

  1. ALWAYS use translation function for user text

{# ❌ Not translatable #} <h1>Invoice</h1>

{# ✅ Translatable #} <h1>{{ _("Invoice") }}</h1>

  1. 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> {% } %}

  1. 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 Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-customapp

No summary provided by upstream source.

Repository SourceNeeds Review