关于 frappe-syntax-jinja
frappe-syntax-jinja 是用于编写 Frappe Jinja 模板的参考技能,涵盖打印格式、邮件、通知和门户页面,并提供语法、上下文及版本渲染说明。
主要功能
- 01Jinja 语法与控制结构
- 02各类模板的上下文
- 03Frappe 方法与过滤器
- 04通过 hooks.py 注册自定义方法和过滤器
- 05打印格式与门户页面模式
- 06v14–v16 渲染说明
适用场景
- 01构建 Frappe 打印格式
- 02创建动态邮件或通知模板
- 03使用 Python 控制器编写门户页面
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-jinja安装后在 Claude Code 中输入/frappe-syntax-jinja确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mdcontext-objects.mdexamples.mdfilters-reference.mdmethods-reference.mdmethods.mdpatterns.mdstructure.mdsyntax.mdtemplates.md详细介绍
Frappe Jinja 模板语法
适用于 Frappe v14/v15/v16 中打印格式、邮件模板、通知模板和门户页面的确定性 Jinja 参考。
何时使用此技能
适用于:
- 创建或修改打印格式(基于 Jinja)
- 编写包含动态字段的邮件模板
- 使用 Python 控制器构建门户页面(
www/*.html) - 编写通知模板(系统/邮件/SMS)
- 通过
hooks.py注册自定义 Jinja 方法或过滤器
不适用于:
- 报表打印格式——它们使用 JavaScript 模板语法(
{%= %}),而非 Jinja - 客户端脚本——请参阅
frappe-syntax-clientscripts - 服务端脚本——请参阅
frappe-syntax-serverscripts
决策树:选择哪种模板类型?
Need a printable document?
├─ YES → Is it for a Query/Script Report?
│ ├─ YES → Use JS Template ({%= %}), NOT Jinja
│ └─ NO → Use Jinja Print Format
└─ NO → Is it for email?
├─ YES → Is it triggered by workflow/notification?
│ ├─ YES → Notification Template (Jinja)
│ └─ NO → Email Template (Jinja)
└─ NO → Is it a web page?
├─ YES → Portal Page (www/*.html + .py controller)
└─ NO → frappe.render_template() for ad-hoc rendering
快速参考:Jinja 语法
| 语法 | 用途 | 示例 |
|---|---|---|
{{ }} | 输出表达式 | {{ doc.name }} |
{% %} | 控制语句 | {% if doc.status == "Paid" %} |
{# #} | 注释 | {# This is a comment #} |
{{ _("text") }} | 翻译 | {{ _("Invoice") }} |
{{ val | filter }} | 过滤器 | {{ name | default("N/A") }} |
关键:Jinja 与 JS 模板语法的区别
| 方面 | Jinja(打印格式) | JS 模板(报表打印格式) |
|---|---|---|
| 输出 | {{ expression }} | {%= expression %} |
| 代码块 | {% statement %} | {% js_code %} |
| 语言 | Python | JavaScript |
| 上下文 | doc、frappe | data、filters |
报表打印格式中绝不要使用 Jinja 语法。标准打印格式中绝不要使用 {%= %}。
各模板类型的上下文对象
打印格式
| 对象 | 描述 |
|---|---|
doc | 正在打印的文档(完整的 Document 对象) |
frappe | Frappe 模块(仅限已列入白名单的方法) |
frappe.utils | 实用函数 |
_() | 翻译函数 |
doc.items、doc.taxes | 子表访问器(按字段名) |
邮件模板
| 对象 | 描述 |
|---|---|
doc | 关联的文档(从 DocType 触发时) |
frappe | Frappe 模块(受限) |
_() | 翻译函数 |
通知模板
| 对象 | 描述 |
|---|---|
doc | 触发通知的文档 |
frappe | Frappe 模块 |
_() | 翻译函数 |
门户页面(www/*.html)
| 对象 | 描述 |
|---|---|
frappe | Frappe 模块 |
frappe.session.user | 当前经过身份验证的用户 |
frappe.form_dict | URL 中的查询参数 |
frappe.lang | 当前语言代码 |
| 自定义上下文 | 在 .py 控制器中的 get_context(context) 里设置 |
完整详情:
references/context-objects.md
核心方法(在 Jinja 中已列入白名单)
格式化:显示时始终使用
{# ALWAYS use get_formatted() for fields in Print Formats #}
{{ doc.get_formatted("posting_date") }}
{{ doc.get_formatted("grand_total") }}
{# Child table rows — ALWAYS pass parent doc for currency context #}
{% for row in doc.items %}
{{ row.get_formatted("rate", doc) }}
{{ row.get_formatted("amount", doc) }}
{% endfor %}
{# General formatting with explicit fieldtype #}
{{ frappe.format(value, {'fieldtype': 'Currency'}) }}
{{ frappe.format_date(doc.posting_date) }}
文档检索
{# Full document — use only when multiple fields needed #}
{% set customer = frappe.get_doc("Customer", doc.customer) %}
{# Single field — ALWAYS prefer over get_doc for one field #}
{% set abbr = frappe.db.get_value("Company", doc.company, "abbr") %}
{# List of records (no permission check) #}
{% set tasks = frappe.get_all("Task",
filters={"status": "Open"},
fields=["title", "due_date"],
order_by="due_date asc",
page_length=10) %}
{# List with permission check (portal pages) #}
{% set orders = frappe.get_list("Sales Order",
filters={"customer": doc.customer},
fields=["name", "grand_total"]) %}
翻译:所有面向用户的字符串都必须翻译
<h1>{{ _("Invoice") }}</h1>
<p>{{ _("Total: {0}").format(doc.get_formatted("grand_total")) }}</p>
系统与会话
{{ frappe.get_url() }}
{{ frappe.get_fullname() }}
{{ frappe.get_fullname(doc.owner) }}
{{ frappe.db.get_single_value("System Settings", "time_zone") }}
{% if frappe.session.user != "Guest" %}...{% endif %}
完整方法参考:
references/methods-reference.md
控制结构
条件语句
{% if doc.status == "Paid" %}
<span class="paid">{{ _("Paid") }}</span>
{% elif doc.status == "Overdue" %}
<span class="overdue">{{ _("Overdue") }}</span>
{% else %}
<span>{{ doc.status }}</span>
{% endif %}
使用子表的循环
{% for item in doc.items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.item_name }}</td>
<td>{{ item.get_formatted("amount", doc) }}</td>
</tr>
{% else %}
<tr><td colspan="3">{{ _("No items") }}</td></tr>
{% endfor %}
循环变量
| 变量 | 描述 |
|---|---|
loop.index | 从 1 开始的索引位置 |
loop.index0 | 从 0 开始的索引位置 |
loop.first | 首次迭代时为 True |
loop.last | 最后一次迭代时为 True |
loop.length | 项目总数 |
变量
{% set total = 0 %}
{% set name = doc.customer_name | default("Unknown") %}
过滤器
| 过滤器 | 示例 | 说明 |
|---|---|---|
default | {{ val | default("N/A") }} | 可选字段始终使用 |
length | {{ items | length }} | 统计项目数量 |
join | {{ names | join(", ") }} | 将列表合并为字符串 |
truncate | {{ text | truncate(100) }} | 截断并添加省略号 |
escape | {{ input | escape }} | 转义 HTML(默认行为) |
safe | {{ html | safe }} | 渲染原始 HTML——绝不可用于用户输入 |
round | {{ num | round(2) }} | 四舍五入 |
lower / upper | {{ text | upper }} | 转换大小写 |
完整的过滤器参考:
references/filters-reference.md
通过 hooks.py 自定义 Jinja 方法和过滤器
hooks.py 注册
# hooks.py
jenv = {
"methods": [
"myapp.jinja.methods" # Module with callable functions
],
"filters": [
"myapp.jinja.filters" # Module with filter functions
]
}
自定义方法
# myapp/jinja/methods.py
import frappe
def get_company_logo(company):
"""Returns company logo URL. Called as get_company_logo() in Jinja."""
return frappe.db.get_value("Company", company, "company_logo") or ""
<img src="{{ get_company_logo(doc.company) }}" alt="Logo">
自定义过滤器
# myapp/jinja/filters.py
def nl2br(text):
"""Convert newlines to <br> tags. Used as {{ text | nl2br }}."""
return (text or "").replace("\n", "<br>")
{{ doc.notes | nl2br | safe }}
详情:
references/methods.md
打印格式模式
最简打印格式模板
<style>
.print-header { background: #f5f5f5; padding: 15px; }
.item-table { width: 100%; border-collapse: collapse; }
.item-table th, .item-table td { border: 1px solid #ddd; padding: 8px; }
.text-right { text-align: right; }
</style>
<div class="print-header">
<h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
<p>{{ doc.name }} — {{ doc.get_formatted("posting_date") }}</p>
</div>
<table class="item-table">
<thead>
<tr>
<th>#</th>
<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>{{ loop.index }}</td>
<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>
<p><strong>{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}</strong></p>
分页符
/* v14/v15 (wkhtmltopdf) */
.page-break { page-break-before: always; }
/* v16 (Chrome PDF) — ALWAYS prefer break-* in v16 */
.page-break { break-before: page; }
完整示例:
references/examples.md| 模式:references/patterns.md
V16:Chrome PDF 渲染
| 方面 | v14/v15(wkhtmltopdf) | v16(Chrome) |
|---|---|---|
| CSS 支持 | CSS3 支持有限 | 完整支持现代 CSS |
| Flexbox/Grid | 部分支持 | 完全支持 |
| 分页符 | page-break-* | 优先使用 break-* |
| 字体 | 仅系统字体 | 支持 Web 字体 |
V16 配置
// site_config.json
{
"pdf_engine": "chrome",
"chrome_path": "/usr/bin/chromium"
}
门户页面模式
www/projects/index.html
{% extends "templates/web.html" %}
{% block title %}{{ _("Projects") }}{% endblock %}
{% block page_content %}
<h1>{{ _("Projects") }}</h1>
{% for project in projects %}
<h3>{{ project.title }}</h3>
<p>{{ project.description | default("") | truncate(150) }}</p>
{% else %}
<p>{{ _("No projects found.") }}</p>
{% endfor %}
{% endblock %}
www/projects/index.py
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
完整结构:
references/structure.md| 模板:references/templates.md
关键规则
始终遵循
- 对所有面向用户的字符串使用
_() - 对货币、日期和数值字段使用
get_formatted() - 对可选/可空字段使用
default()过滤器 - 将父级
doc传递给子行的get_formatted("field", doc) - 只需要一个字段时使用
frappe.db.get_value() - 将计算放在 Python 控制器中,而不是 Jinja 模板中
绝不
- 在循环中执行数据库查询(N+1 问题)
- 对用户提供的输入使用
| safe(存在 XSS 漏洞) - 在报表打印格式中使用 Jinja 语法(它们需要 JS
{%= %}) - 在
frappe.db.get_value()足以满足需求时使用frappe.get_doc() - 硬编码字符串而不使用
_()翻译包装器 - 未经安全审查就禁用
safe_render
反模式及修复方法:
references/anti-patterns.md
参考文件
| 文件 | 内容 |
|---|---|
references/syntax.md | Jinja 语法参考(标签、过滤器、测试、循环) |
references/methods.md | 通过 hooks 自定义 Jinja 方法/过滤器 |
references/context-objects.md | 各模板类型可用的对象 |
references/filters-reference.md | 所有标准和自定义 Frappe 过滤器 |
references/methods-reference.md | Jinja 中可用的所有 frappe.* 方法 |
references/examples.md | 完整的打印格式、邮件、门户示例 |
references/anti-patterns.md | 常见错误和正确替代方案 |
references/templates.md | 模板结构模式 |
references/patterns.md | 条件渲染、循环、子表 |
references/structure.md | 各模板类型的文件结构 |
另请参阅
frappe-syntax-hooks— hooks.py 中的 jenv 配置frappe-impl-printformat— 打印格式实现模式frappe-errors-serverscripts— 服务端错误处理
评论
支持 Markdown