Claude-skill-registry 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: create print format, email template, portal page, custom jinja filter, print format styling, pdf template, invoice template, report template.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/erpnext-impl-jinja" ~/.claude/skills/majiayu000-claude-skill-registry-erpnext-impl-jinja && rm -rf "$T"
manifest:
skills/data/erpnext-impl-jinja/SKILL.mdsource content
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 | , , |
| Email Template | , (limited) |
| Portal Page | , , custom context |
| Notification | , |
Quick Reference: Essential Methods
| Need | Method |
|---|---|
| Format currency/date | |
| Format child row | |
| Translate string | |
| Get linked doc | |
| Get single field | |
| Current date | |
| Format 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 |