mcpField
F
frappe-syntax-jinja

frappe-syntax-jinja

github.com/impertio-studio开发198123 次安装30 周前更新
代码仓库

关于 frappe-syntax-jinja

frappe-syntax-jinja 是用于编写 Frappe Jinja 模板的参考技能,涵盖打印格式、邮件、通知和门户页面,并提供语法、上下文及版本渲染说明。

主要功能

  1. 01Jinja 语法与控制结构
  2. 02各类模板的上下文
  3. 03Frappe 方法与过滤器
  4. 04通过 hooks.py 注册自定义方法和过滤器
  5. 05打印格式与门户页面模式
  6. 06v14–v16 渲染说明

适用场景

  1. 01构建 Frappe 打印格式
  2. 02创建动态邮件或通知模板
  3. 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.md
context-objects.md
examples.md
filters-reference.md
methods-reference.md
methods.md
patterns.md
structure.md
syntax.md
templates.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 %}
语言PythonJavaScript
上下文doc、frappedata、filters

报表打印格式中绝不要使用 Jinja 语法。标准打印格式中绝不要使用 {%= %}。


各模板类型的上下文对象

打印格式
对象描述
doc正在打印的文档(完整的 Document 对象)
frappeFrappe 模块(仅限已列入白名单的方法)
frappe.utils实用函数
_()翻译函数
doc.items、doc.taxes子表访问器(按字段名)
邮件模板
对象描述
doc关联的文档(从 DocType 触发时)
frappeFrappe 模块(受限)
_()翻译函数
通知模板
对象描述
doc触发通知的文档
frappeFrappe 模块
_()翻译函数
门户页面(www/*.html)
对象描述
frappeFrappe 模块
frappe.session.user当前经过身份验证的用户
frappe.form_dictURL 中的查询参数
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


关键规则

始终遵循
  1. 对所有面向用户的字符串使用 _()
  2. 对货币、日期和数值字段使用 get_formatted()
  3. 对可选/可空字段使用 default() 过滤器
  4. 将父级 doc 传递给子行的 get_formatted("field", doc)
  5. 只需要一个字段时使用 frappe.db.get_value()
  6. 将计算放在 Python 控制器中,而不是 Jinja 模板中
绝不
  1. 在循环中执行数据库查询(N+1 问题)
  2. 对用户提供的输入使用 | safe(存在 XSS 漏洞)
  3. 在报表打印格式中使用 Jinja 语法(它们需要 JS {%= %})
  4. 在 frappe.db.get_value() 足以满足需求时使用 frappe.get_doc()
  5. 硬编码字符串而不使用 _() 翻译包装器
  6. 未经安全审查就禁用 safe_render

反模式及修复方法:references/anti-patterns.md


参考文件

文件内容
references/syntax.mdJinja 语法参考(标签、过滤器、测试、循环)
references/methods.md通过 hooks 自定义 Jinja 方法/过滤器
references/context-objects.md各模板类型可用的对象
references/filters-reference.md所有标准和自定义 Frappe 过滤器
references/methods-reference.mdJinja 中可用的所有 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