--- name: erpnext-syntax-jinja version: 1.0.0 description: Deterministic Jinja template syntax for ERPNext/Frappe Print Formats, Email Templates, and Portal Pages author: OpenAEC Foundation tags: [erpnext, frappe, jinja, templates, print-formats, email-templates, portal-pages] languages: [en] frappe_versions: [v14, v15, v16] --- # ERPNext Jinja Templates Syntax Skill > Correct Jinja syntax for Print Formats, Email Templates, and Portal Pages in ERPNext/Frappe v14/v15/v16. --- ## When to Use This Skill USE this skill when: - Creating or modifying Print Formats - Developing Email Templates - Building Portal Pages (www/*.html) - Adding custom Jinja filters/methods via hooks DO NOT USE for: - Report Print Formats (they use JavaScript templating, not Jinja) - Client Scripts (use erpnext-syntax-clientscripts) - Server Scripts (use erpnext-syntax-serverscripts) --- ## Context Objects per Template Type ### Print Formats | Object | Description | |--------|-------------| | `doc` | The document being printed | | `frappe` | Frappe module with utility methods | | `_()` | Translation function | ### Email Templates | Object | Description | |--------|-------------| | `doc` | The linked document | | `frappe` | Frappe module (limited) | ### Portal Pages | Object | Description | |--------|-------------| | `frappe.session.user` | Current user | | `frappe.form_dict` | Query parameters | | `frappe.lang` | Current language | | Custom context | Via Python controller | > **See**: `references/context-objects.md` for complete details. --- ## Essential Methods ### Formatting (ALWAYS use) ```jinja {# RECOMMENDED for fields in print formats #} {{ doc.get_formatted("posting_date") }} {{ doc.get_formatted("grand_total") }} {# For child table rows - pass parent doc #} {% for row in doc.items %} {{ row.get_formatted("rate", doc) }} {{ row.get_formatted("amount", doc) }} {% endfor %} {# General formatting #} {{ frappe.format(value, {'fieldtype': 'Currency'}) }} {{ frappe.format_date(doc.posting_date) }} ``` ### Document Retrieval ```jinja {# Full document #} {% set customer = frappe.get_doc("Customer", doc.customer) %} {# Specific field value (more efficient) #} {% set abbr = frappe.db.get_value("Company", doc.company, "abbr") %} {# List of records #} {% set tasks = frappe.get_all('Task', filters={'status': 'Open'}, fields=['title', 'due_date']) %} ``` ### Translation (REQUIRED for user-facing strings) ```jinja

{{ _("Invoice") }}

{{ _("Total: {0}").format(doc.grand_total) }}

``` > **See**: `references/methods-reference.md` for all methods. --- ## Control Structures ### Conditionals ```jinja {% if doc.status == "Paid" %} {{ _("Paid") }} {% elif doc.status == "Overdue" %} {{ _("Overdue") }} {% else %} {{ doc.status }} {% endif %} ``` ### Loops ```jinja {% for item in doc.items %} {{ loop.index }} {{ item.item_name }} {{ item.get_formatted("amount", doc) }} {% else %} {{ _("No items") }} {% endfor %} ``` ### Loop Variables | Variable | Description | |----------|-------------| | `loop.index` | 1-indexed position | | `loop.first` | True on first | | `loop.last` | True on last | | `loop.length` | Total items | ### Variables ```jinja {% set total = 0 %} {% set customer_name = doc.customer_name | default('Unknown') %} ``` --- ## Filters ### Commonly Used | Filter | Example | |--------|---------| | `default` | `{{ value \| default('N/A') }}` | | `length` | `{{ items \| length }}` | | `join` | `{{ names \| join(', ') }}` | | `truncate` | `{{ text \| truncate(100) }}` | | `safe` | `{{ html \| safe }}` (trusted content only!) | > **See**: `references/filters-reference.md` for all filters. --- ## Print Format Template ```jinja

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

{{ doc.name }}

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

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

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

``` --- ## Email Template ```jinja

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

{{ _("Invoice") }} {{ doc.name }} {{ _("for") }} {{ doc.get_formatted("grand_total") }} {{ _("is due.") }}

{{ _("Due Date") }}: {{ frappe.format_date(doc.due_date) }}

{% if doc.items %} {% endif %}

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

``` --- ## Portal Page with Controller ### www/projects/index.html ```jinja {% extends "templates/web.html" %} {% block title %}{{ _("Projects") }}{% endblock %} {% block page_content %}

{{ _("Projects") }}

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

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

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

{{ project.title }}

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

{% else %}

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

{% endfor %} {% endblock %} ``` ### www/projects/index.py ```python import frappe def get_context(context): context.title = "Projects" context.projects = frappe.get_all( "Project", filters={"is_public": 1}, fields=["name", "title", "description"], order_by="creation desc" ) return context ``` --- ## Custom Filters/Methods via jenv Hook ### hooks.py ```python jenv = { "methods": ["myapp.jinja.methods"], "filters": ["myapp.jinja.filters"] } ``` ### myapp/jinja/methods.py ```python import frappe def get_company_logo(company): """Get company logo URL""" return frappe.db.get_value("Company", company, "company_logo") or "" ``` ### Usage ```jinja ``` --- ## Critical Rules ### ✅ ALWAYS 1. Use `_()` for all user-facing strings 2. Use `get_formatted()` for currency/date fields 3. Use default values: `{{ value | default('') }}` 4. Child table rows: `row.get_formatted("field", doc)` ### ❌ NEVER 1. Execute queries in loops (N+1 problem) 2. Use `| safe` for user input (XSS risk) 3. Heavy calculations in templates (do in Python) 4. Jinja syntax in Report Print Formats (they use JS) --- ## Report Print Formats (NOT Jinja!) **WARNING**: Report Print Formats for Query/Script Reports use JavaScript templating. | Aspect | Jinja (Print Formats) | JS (Report Print Formats) | |--------|----------------------|---------------------------| | Output | `{{ }}` | `{%= %}` | | Code | `{% %}` | `{% %}` | | Language | Python | JavaScript | ```html {% for(var i=0; i{%= data[i].name %} {% } %} ``` --- ## Version Compatibility | Feature | v14 | v15 | |---------|:---:|:---:| | Basic Jinja API | ✅ | ✅ | | get_formatted() | ✅ | ✅ | | jenv hook | ✅ | ✅ | | Portal pages | ✅ | ✅ | | frappe.utils.format_date with format | ✅ | ✅+ | --- ## V16: Chrome PDF Rendering **Version 16 introduced Chrome-based PDF rendering** replacing wkhtmltopdf. ### Key Differences | Aspect | v14/v15 (wkhtmltopdf) | v16 (Chrome) | |--------|----------------------|---------------| | CSS Support | Limited CSS3 | Full modern CSS | | Flexbox/Grid | Partial | Full support | | Page breaks | `page-break-*` | `break-*` preferred | | Fonts | System fonts | Web fonts supported | | Performance | Faster | Slightly slower | ### CSS Updates for V16 ```css /* v14/v15 */ .page-break { page-break-before: always; } /* v16 - both work, but break-* is preferred */ .page-break { break-before: page; } ``` ### Configuration (V16) ```python # In site_config.json { "pdf_engine": "chrome", # or "wkhtmltopdf" for legacy "chrome_path": "/usr/bin/chromium" } ``` ### Print Format Compatibility Most print formats work unchanged. Update if using: - Complex CSS layouts (flexbox/grid now fully supported) - Custom fonts (web fonts now work) - Advanced page break control --- ## Reference Files | File | Contents | |------|----------| | `references/context-objects.md` | Available objects per template type | | `references/methods-reference.md` | All frappe.* methods | | `references/filters-reference.md` | Standard and custom filters | | `references/examples.md` | Complete working examples | | `references/anti-patterns.md` | Mistakes to avoid | --- ## See Also - `erpnext-syntax-hooks` - For jenv configuration in hooks.py - `erpnext-impl-jinja` - For implementation patterns - `erpnext-errors-jinja` - For error handling