--- 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 ### 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 ```jinja {# ALWAYS include styles at top #} {# Document header #}

{{ doc.select_print_heading or _("Invoice") }}

{{ doc.name }}

{{ _("Date") }}: {{ doc.get_formatted("posting_date") }}

{# Items table #} {% for row in doc.items %} {% endfor %}
{{ _("Item") }} {{ _("Qty") }} {{ _("Amount") }}
{{ row.item_name }} {{ row.qty }} {{ row.get_formatted("amount", doc) }}
{# Totals #}

{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}

``` ### 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 ```jinja

{{ _("Dear") }} {{ doc.customer_name }},

{{ _("This is a reminder that invoice") }} {{ doc.name }} {{ _("for") }} {{ doc.get_formatted("grand_total") }} {{ _("is due.") }}

{{ _("Due Date") }} {{ frappe.format_date(doc.due_date) }}
{{ _("Outstanding") }} {{ doc.get_formatted("outstanding_amount") }}
{% if doc.items %}

{{ _("Items") }}:

{% endif %}

{{ _("Best regards") }},
{{ frappe.db.get_value("Company", doc.company, "company_name") }}

``` ### Step 3: Use in Notifications or Code ```python # 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) ```jinja {% extends "templates/web.html" %} {% block title %}{{ _("Projects") }}{% endblock %} {% block page_content %}

{{ title }}

{% if frappe.session.user != 'Guest' %}

{{ _("Welcome") }}, {{ frappe.get_fullname() }}

{% endif %}
{% for project in projects %}

{{ project.title }}

{{ project.description | truncate(100) }}

{{ _("View Details") }}
{% else %}

{{ _("No projects found.") }}

{% endfor %}
{% endblock %} ``` ### Step 3: Create Context (index.py) ```python 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 ```python # myapp/hooks.py jenv = { "methods": ["myapp.jinja.methods"], "filters": ["myapp.jinja.filters"] } ``` ### Step 2: Create Methods Module ```python # 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 ```python # 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 ```jinja {# Methods - called as functions #} Logo

{{ get_address_display(doc.customer_address) }}

Outstanding: {{ get_outstanding_amount(doc.customer) }}

{# Filters - piped after values #}

Phone: {{ doc.phone | format_phone }}

Amount: {{ doc.grand_total | currency_words }}

``` ### Step 5: Deploy ```bash 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 ```jinja {# ❌ Raw database value #} {{ doc.grand_total }} {# ✅ Properly formatted with currency #} {{ doc.get_formatted("grand_total") }} ``` ### 2. ALWAYS pass parent doc for child table formatting ```jinja {% 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 ```jinja {# ❌ Not translatable #}

Invoice

{# ✅ Translatable #}

{{ _("Invoice") }}

``` ### 4. NEVER use Jinja in Report Print Formats ```html {% for(var i=0; i{%= data[i].name %} {% } %} ``` ### 5. NEVER execute queries in loops ```jinja {# ❌ 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](references/decision-tree.md) | Complete template type selection | | [workflows.md](references/workflows.md) | Step-by-step implementation patterns | | [examples.md](references/examples.md) | Complete working examples | | [anti-patterns.md](references/anti-patterns.md) | Common mistakes to avoid |