erpnext-impl-jinja
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Jinja Templates - Implementation
ERPNext Jinja模板 - 实现指南
This skill helps you determine HOW to implement Jinja templates. For exact syntax, see .
erpnext-syntax-jinjaVersion: 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注册
│
└─► 多站点共用的模板?
└─► 包含在应用中,导出为fixtureImplementation 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设置 > 打印 > 打印格式 > 新建
- 文档类型:销售发票
- 模块:会计
- 标准:否(自定义)
- 打印格式类型:JinjaStep 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 generation1. 打开一个文档(如销售发票)
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
undefinedpython
undefinedIn 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 contextmyapp/
└── 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 contextpython
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 contextStep 4: Test
步骤4:测试
Visit: https://yoursite.com/projects访问:https://yoursite.com/projectsImplementation Workflow: Custom Jinja Methods
实现工作流:自定义Jinja方法
Step 1: Register in hooks.py
步骤1:在hooks.py中注册
python
undefinedpython
undefinedmyapp/hooks.py
myapp/hooks.py
jenv = {
"methods": ["myapp.jinja.methods"],
"filters": ["myapp.jinja.filters"]
}
undefinedjenv = {
"methods": ["myapp.jinja.methods"],
"filters": ["myapp.jinja.filters"]
}
undefinedStep 2: Create Methods Module
步骤2:创建方法模块
python
undefinedpython
undefinedmyapp/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
WHERE customer = %s AND docstatus = 1
""", customer)
return result[0][0] if result else 0
tabSales Invoiceundefinedimport 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
WHERE customer = %s AND docstatus = 1
""", customer)
return result[0][0] if result else 0
tabSales InvoiceundefinedStep 3: Create Filters Module
步骤3:创建过滤器模块
python
undefinedpython
undefinedmyapp/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}"
undefineddef 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}"
undefinedStep 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 migratebash
bench --site sitename migrateQuick Reference: Context Variables
快速参考:上下文变量
| Template Type | Available Objects |
|---|---|
| Print Format | |
| Email Template | |
| Portal Page | |
| 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
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
版本差异
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| Jinja templates | ✅ | ✅ | ✅ |
| get_formatted() | ✅ | ✅ | ✅ |
| jenv hooks | ✅ | ✅ | ✅ |
| wkhtmltopdf PDF | ✅ | ✅ | ⚠️ |
| Chrome PDF | ❌ | ❌ | ✅ |
| 功能 | V14 | V15 | V16 |
|---|---|---|---|
| Jinja模板 | ✅ | ✅ | ✅ |
| get_formatted() | ✅ | ✅ | ✅ |
| jenv钩子 | ✅ | ✅ | ✅ |
| wkhtmltopdf PDF | ✅ | ✅ | ⚠️ |
| Chrome PDF | ❌ | ❌ | ✅ |
V16 Chrome PDF Considerations
V16 Chrome PDF注意事项
See for detailed Chrome PDF documentation.
erpnext-syntax-jinja如需详细的Chrome PDF文档,请参考。
erpnext-syntax-jinjaReference 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 |
| 文件 | 内容 |
|---|---|
| decision-tree.md | 完整的模板类型选择 |
| workflows.md | 分步实现模式 |
| examples.md | 完整的工作示例 |
| anti-patterns.md | 需避免的常见错误 |