erpnext-impl-jinja

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Jinja Templates - Implementation

ERPNext Jinja模板 - 实现指南

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)
本指南将帮助你确定如何实现Jinja模板。如需了解具体语法,请参考
erpnext-syntax-jinja
版本:兼容v14/v15/v16(标注了V16专属功能)

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

┌─────────────────────────────────────────────────────────────────────────┐
│ 你要创建什么?                                                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► 可打印文档(发票、采购订单、报表)?                                   │
│   ├── 标准文档类型 → 打印格式(Jinja)                                   │
│   └── 查询/脚本报表 → 报表打印格式(JavaScript!)                       │
│                                                                         │
│ ► 含动态内容的自动化邮件?                                             │
│   └── 邮件模板(Jinja)                                                │
│                                                                         │
│ ► 面向客户的网页?                                                     │
│   └── 门户页面(www/*.html + *.py)                                   │
│                                                                         │
│ ► 可复用的模板函数/过滤器?                                            │
│   └── hooks.py中的自定义jenv方法                                       │
│                                                                         │
│ ► 通知内容?                                                           │
│   └── 通知模板(使用Jinja语法)                                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

⚠️ 重要提示:报表打印格式使用JavaScript模板,而非Jinja!
   - Jinja: {{ variable }}
   - JS报表: {%= 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

你要打印什么?
├─► 标准文档类型(发票、采购订单、报价单)?
│   │
│   │ 在哪里创建?
│   ├─► 快速/简单格式 → 打印格式构建器(设置 > 打印)
│   │   - 拖拽界面
│   │   - 自定义能力有限
│   │
│   └─► 需要复杂布局 → 自定义HTML打印格式
│       - 完全控制Jinja
│       - 自定义CSS样式
│       - 动态逻辑
├─► 查询报表或脚本报表?
│   └─► 报表打印格式(JavaScript模板!)
│       ⚠️ 不是Jinja!使用{%= %}和{% %}
└─► 信函或独立文档?
    └─► 信头 + 打印格式组合

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

是一次性使用还是可复用?
├─► 站点专属,通过UI管理?
│   └─► 通过设置 > 打印格式 / 邮件模板创建
│       - 存储在数据库中
│       - 无需代码即可轻松编辑
├─► 属于自定义应用的一部分?
│   │
│   │ 类型是什么?
│   ├─► 打印格式 → myapp/fixtures或数据库记录
│   │
│   ├─► 门户页面 → myapp/www/pagename/
│   │   - index.html(模板)
│   │   - index.py(上下文)
│   │
│   └─► 自定义方法/过滤器 → myapp/jinja/
│       - 通过hooks.py的jenv注册
└─► 多站点共用的模板?
    └─► 包含在应用中,导出为fixture

Implementation Workflow: Print Format

实现工作流:打印格式

Step 1: Create via UI (Recommended Start)

步骤1:通过UI创建(推荐入门方式)

Setup > Printing > Print Format > New
- DocType: Sales Invoice
- Module: Accounts
- Standard: No (Custom)
- Print Format Type: Jinja
设置 > 打印 > 打印格式 > 新建
- 文档类型:销售发票
- 模块:会计
- 标准:否(自定义)
- 打印格式类型:Jinja

Step 2: Basic Template Structure

步骤2:基础模板结构

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

步骤3:测试与优化

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

1. 打开一个文档(如销售发票)
2. 菜单 > 打印 > 选择你的格式
3. 检查布局,按需调整CSS
4. 测试PDF生成

Implementation Workflow: Email Template

实现工作流:邮件模板

Step 1: Create via UI

步骤1:通过UI创建

Setup > Email > Email Template > New
- Name: Payment Reminder
- Subject: Invoice {{ doc.name }} - Payment Due
- DocType: Sales Invoice
设置 > 邮件 > 邮件模板 > 新建
- 名称:付款提醒
- 主题:发票 {{ doc.name }} - 付款到期
- 文档类型:销售发票

Step 2: Template Content

步骤2:模板内容

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

步骤3:在通知或代码中使用

python
undefined
python
undefined

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}) )

---
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

步骤1:创建目录结构

myapp/
└── www/
    └── projects/
        ├── index.html    # Jinja template
        └── index.py      # Python context
myapp/
└── www/
    └── projects/
        ├── index.html    # Jinja模板
        └── index.py      # Python上下文

Step 2: Create Template (index.html)

步骤2:创建模板(index.html)

jinja
{% 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 %}
jinja
{% 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)

步骤3:创建上下文(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
python
import frappe

def get_context(context):
    context.title = "Projects"
    context.no_cache = True  # 动态内容
    
    # 获取数据
    context.projects = frappe.get_all(
        "Project",
        filters={"is_public": 1},
        fields=["name", "title", "description"],
        order_by="creation desc"
    )
    
    return context

Step 4: Test

步骤4:测试

Visit: https://yoursite.com/projects

访问:https://yoursite.com/projects

Implementation Workflow: Custom Jinja Methods

实现工作流:自定义Jinja方法

Step 1: Register in hooks.py

步骤1:在hooks.py中注册

python
undefined
python
undefined

myapp/hooks.py

myapp/hooks.py

jenv = { "methods": ["myapp.jinja.methods"], "filters": ["myapp.jinja.filters"] }
undefined
jenv = { "methods": ["myapp.jinja.methods"], "filters": ["myapp.jinja.filters"] }
undefined

Step 2: Create Methods Module

步骤2:创建方法模块

python
undefined
python
undefined

myapp/jinja/methods.py

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
undefined
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
undefined

Step 3: Create Filters Module

步骤3:创建过滤器模块

python
undefined
python
undefined

myapp/jinja/filters.py

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}"
undefined
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}"
undefined

Step 4: Use in Templates

步骤4:在模板中使用

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

步骤5:部署

bash
bench --site sitename migrate

bash
bench --site sitename migrate

Quick Reference: Context Variables

快速参考:上下文变量

Template TypeAvailable Objects
Print Format
doc
,
frappe
,
_()
Email Template
doc
,
frappe
(limited)
Portal Page
frappe.session
,
frappe.form_dict
, custom context
Notification
doc
,
frappe

模板类型可用对象
打印格式
doc
,
frappe
,
_()
邮件模板
doc
,
frappe
(有限制)
门户页面
frappe.session
,
frappe.form_dict
, 自定义上下文
通知
doc
,
frappe

Quick Reference: Essential Methods

快速参考:核心方法

NeedMethod
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)

需求方法
格式化货币/日期
doc.get_formatted("fieldname")
格式化子行
row.get_formatted("field", doc)
翻译字符串
_("String")
获取关联文档
frappe.get_doc("DocType", name)
获取单个字段
frappe.db.get_value("DT", name, "field")
当前日期
frappe.utils.nowdate()
格式化日期
frappe.format_date(date)

Critical Rules

重要规则

1. ALWAYS use get_formatted for display values

1. 始终使用get_formatted显示值

jinja
{# ❌ Raw database value #}
{{ doc.grand_total }}

{# ✅ Properly formatted with currency #}
{{ doc.get_formatted("grand_total") }}
jinja
{# ❌ 原始数据库值 #}
{{ doc.grand_total }}

{# ✅ 带货币的正确格式 #}
{{ doc.get_formatted("grand_total") }}

2. ALWAYS pass parent doc for child table formatting

2. 格式化子表时始终传入父文档

jinja
{% for row in doc.items %}
    {# ❌ Missing currency context #}
    {{ row.get_formatted("rate") }}
    
    {# ✅ Has currency context from parent #}
    {{ row.get_formatted("rate", doc) }}
{% endfor %}
jinja
{% for row in doc.items %}
    {# ❌ 缺少货币上下文 #}
    {{ row.get_formatted("rate") }}
    
    {# ✅ 从父文档获取货币上下文 #}
    {{ row.get_formatted("rate", doc) }}
{% endfor %}

3. ALWAYS use translation function for user text

3. 用户文本始终使用翻译函数

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

{# ✅ Translatable #}
<h1>{{ _("Invoice") }}</h1>
jinja
{# ❌ 不可翻译 #}
<h1>Invoice</h1>

{# ✅ 可翻译 #}
<h1>{{ _("Invoice") }}</h1>

4. NEVER use Jinja in Report Print Formats

4. 永远不要在报表打印格式中使用Jinja

html
<!-- Query/Script Reports use JAVASCRIPT templating -->
{% for(var i=0; i<data.length; i++) { %}
<tr><td>{%= data[i].name %}</td></tr>
{% } %}
html
<!-- 查询/脚本报表使用JavaScript模板 -->
{% for(var i=0; i<data.length; i++) { %}
<tr><td>{%= data[i].name %}</td></tr>
{% } %}

5. NEVER execute queries in loops

5. 永远不要在循环中执行查询

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

jinja
{# ❌ N+1查询问题 #}
{% for item in doc.items %}
    {% set stock = frappe.db.get_value("Bin", ...) %}
{% endfor %}

{# ✅ 在控制器/上下文中预取数据 #}
{% for item in items_with_stock %}
    {{ item.stock_qty }}
{% endfor %}

Version Differences

版本差异

FeatureV14V15V16
Jinja templates
get_formatted()
jenv hooks
wkhtmltopdf PDF⚠️
Chrome PDF
功能V14V15V16
Jinja模板
get_formatted()
jenv钩子
wkhtmltopdf PDF⚠️
Chrome PDF

V16 Chrome PDF Considerations

V16 Chrome PDF注意事项

See
erpnext-syntax-jinja
for detailed Chrome PDF documentation.

如需详细的Chrome PDF文档,请参考
erpnext-syntax-jinja

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
文件内容
decision-tree.md完整的模板类型选择
workflows.md分步实现模式
examples.md完整的工作示例
anti-patterns.md需避免的常见错误