mcpField
F
frappe-impl-jinja

frappe-impl-jinja

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

关于 frappe-impl-jinja

面向 Frappe 开发者的 Jinja 模板实现指南,适用于打印格式、邮件与通知模板以及门户页面。它提供分步创建、调试和部署流程,并说明版本相关的 PDF 与 CSS 差异。

主要功能

  1. 01创建打印格式模板
  2. 02制作邮件和通知模板
  3. 03构建门户页面
  4. 04注册自定义 Jinja 方法与过滤器
  5. 05排查模板渲染问题

适用场景

  1. 01生成发票或采购订单 PDF
  2. 02发送带动态文档内容的邮件
  3. 03创建订单门户页面
  4. 04调试格式、翻译或空值问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-impl-jinja

安装后在 Claude Code 中输入/frappe-impl-jinja确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
decision-tree.md
examples.md
print-format-decision.md
workflows.md

详细介绍

Frappe Jinja 模板实现工作流

用于构建 Jinja 模板的分步工作流。语法参考请参阅 frappe-syntax-jinja。

版本:v14/v15/v16(注明 V16 Chrome PDF)


主要决策:你要创建什么?

WHAT IS YOUR OUTPUT?
│
├─► Printable PDF (invoice, PO, report)?
│   ├─► Standard DocType → Print Format (Jinja)
│   └─► Query/Script Report → Report Print Format (JAVASCRIPT!)
│       ⚠️ Uses {%= %} NOT {{ }}
│
├─► Automated email with dynamic content?
│   └─► Email Template (Jinja, linked to DocType)
│
├─► System notification?
│   └─► Notification (Setup > Notification, uses Jinja)
│
├─► Customer-facing web page?
│   └─► Portal Page (myapp/www/*.html + *.py)
│
└─► Reusable template functions/filters?
    └─► Custom jenv methods in hooks.py

工作流 1:创建打印格式

步骤 1:通过 UI 创建
Setup > Printing > Print Format > New
- Name: My Invoice Format
- DocType: Sales Invoice
- Module: Accounts
- Standard: No (custom)
- Print Format Type: Jinja
步骤 2:编写模板
<style>
    .print-format { font-family: Arial, sans-serif; font-size: 11px; }
    .header { margin-bottom: 20px; }
    .table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    .table th, .table td { border: 1px solid #ddd; padding: 8px; }
    .table th { background: #f0f0f0; }
    .text-right { text-align: right; }
</style>

<div class="header">
    <h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
    <p><strong>{{ doc.name }}</strong> |
       {{ doc.get_formatted("posting_date") }}</p>
</div>

<p><strong>{{ doc.customer_name }}</strong></p>
{% if doc.address_display %}
    <p>{{ doc.address_display | safe }}</p>
{% endif %}

<table class="table">
    <thead>
        <tr>
            <th>#</th>
            <th>{{ _("Item") }}</th>
            <th class="text-right">{{ _("Qty") }}</th>
            <th class="text-right">{{ _("Rate") }}</th>
            <th class="text-right">{{ _("Amount") }}</th>
        </tr>
    </thead>
    <tbody>
        {% for row in doc.items %}
        <tr>
            <td>{{ row.idx }}</td>
            <td>{{ row.item_name }}</td>
            <td class="text-right">{{ row.qty }}</td>
            <td class="text-right">{{ row.get_formatted("rate", doc) }}</td>
            <td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

{% for tax in doc.taxes %}
<p class="text-right">{{ tax.description }}: {{ tax.get_formatted("tax_amount", doc) }}</p>
{% endfor %}

<p class="text-right">
    <strong>{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}</strong>
</p>

{% if doc.terms %}
<div style="margin-top: 30px; border-top: 1px solid #ddd; padding-top: 10px;">
    <strong>{{ _("Terms and Conditions") }}</strong>
    {{ doc.terms | safe }}
</div>
{% endif %}
步骤 3:测试
  1. 打开一张销售发票
  2. 菜单 > 打印 > 选择“My Invoice Format”
  3. 检查布局和格式
  4. 务必测试 PDF 下载——wkhtmltopdf 的渲染效果与浏览器不同
打印格式的重要规则
  • 务必使用 doc.get_formatted("field") 格式化货币、日期和数字
  • 务必为子表行传入父文档:row.get_formatted("rate", doc)
  • 务必使用 _("text") 包裹面向用户的文本,以便翻译
  • 务必将 CSS 放在顶部的 <style> 块中(不要放在外部文件中)
  • v14/v15 中绝不要使用 flexbox(wkhtmltopdf 不支持)——V16 Chrome PDF 支持
  • 对用户提供的输入绝不要使用 | safe——仅对可信的系统 HTML 使用

工作流 2:创建电子邮件模板

步骤 1:通过 UI 创建
Setup > Email > Email Template > New
- Name: Payment Reminder
- Subject: Invoice {{ doc.name }} - Payment Reminder
- DocType: Sales Invoice
步骤 2:编写电子邮件内容

电子邮件中务必使用内联样式——大多数客户端会移除 <style> 块。

<div style="font-family: Arial, sans-serif; max-width: 600px;">
    <p>{{ _("Dear") }} {{ doc.customer_name }},</p>

    <p>{{ _("Invoice") }} <strong>{{ doc.name }}</strong>
    {{ _("for") }} {{ doc.get_formatted("grand_total") }}
    {{ _("is due for payment.") }}</p>

    <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
        <tr style="background: #f5f5f5;">
            <td style="padding: 10px; border: 1px solid #ddd;">
                <strong>{{ _("Due Date") }}</strong></td>
            <td style="padding: 10px; border: 1px solid #ddd;">
                {{ frappe.format_date(doc.due_date) }}</td>
        </tr>
        <tr>
            <td style="padding: 10px; border: 1px solid #ddd;">
                <strong>{{ _("Outstanding") }}</strong></td>
            <td style="padding: 10px; border: 1px solid #ddd; color: #c00;">
                {{ doc.get_formatted("outstanding_amount") }}</td>
        </tr>
    </table>

    {% if doc.items %}
    <p><strong>{{ _("Items") }}:</strong></p>
    <ul>
    {% for item in doc.items[:5] %}
        <li>{{ item.item_name }} ({{ item.qty }})</li>
    {% endfor %}
    {% if doc.items | length > 5 %}
        <li style="color: #666;">{{ _("and {0} more...").format(doc.items|length - 5) }}</li>
    {% endif %}
    </ul>
    {% endif %}

    <p>{{ _("Best regards") }},<br>
    {{ frappe.db.get_value("Company", doc.company, "company_name") }}</p>
</div>
步骤 3:在通知或代码中使用

选项 A:自动触发的通知

Setup > Notification > New
- Channel: Email
- Document Type: Sales Invoice
- Send Alert On: Days After (7 days after due_date)
- Condition: doc.outstanding_amount > 0
- Email Template: Payment Reminder

选项 B:从代码发送

template = frappe.get_doc("Email Template", "Payment Reminder")
frappe.sendmail(
    recipients=[doc.contact_email],
    subject=frappe.render_template(template.subject, {"doc": doc}),
    message=frappe.render_template(template.response, {"doc": doc}),
    reference_doctype=doc.doctype,
    reference_name=doc.name
)

工作流 3:创建通知模板

步骤 1:通过 UI 创建
Setup > Notification > New
- Name: Low Stock Alert
- Channel: Email (or Slack, System Notification)
- Document Type: Stock Ledger Entry
- Send Alert On: Method (on change)
- Condition: doc.actual_qty < 10
步骤 2:编写消息(Jinja)
<h3>{{ _("Low Stock Alert") }}</h3>
<p>{{ _("Item") }}: <strong>{{ doc.item_code }}</strong></p>
<p>{{ _("Warehouse") }}: {{ doc.warehouse }}</p>
<p>{{ _("Current Stock") }}: {{ doc.actual_qty }}</p>
<p>{{ _("Please reorder.") }}</p>

工作流 4:创建门户页面

步骤 1:创建目录结构
myapp/
└── www/
    └── my-orders/
        ├── index.html    # Jinja template
        └── index.py      # Python context
步骤 2:创建上下文(index.py)
import frappe

def get_context(context):
    if frappe.session.user == "Guest":
        frappe.local.flags.redirect_location = "/login"
        raise frappe.Redirect

    context.title = "My Orders"
    context.no_cache = True

    customer = frappe.db.get_value("Contact",
        {"user": frappe.session.user}, "link_name")

    context.orders = frappe.get_all("Sales Order",
        filters={"customer": customer, "docstatus": ["!=", 2]},
        fields=["name", "transaction_date", "grand_total", "status"],
        order_by="transaction_date desc",
        limit=50
    ) if customer else []

    return context
步骤 3:创建模板(index.html)
{% extends "templates/web.html" %}

{% block title %}{{ _("My Orders") }}{% endblock %}

{% block page_content %}
<div class="container my-4">
    <h1>{{ _("My Orders") }}</h1>

    {% if orders %}
    <table class="table table-hover">
        <thead>
            <tr>
                <th>{{ _("Order") }}</th>
                <th>{{ _("Date") }}</th>
                <th>{{ _("Status") }}</th>
                <th class="text-right">{{ _("Total") }}</th>
            </tr>
        </thead>
        <tbody>
            {% for order in orders %}
            <tr>
                <td><a href="/orders/{{ order.name }}">{{ order.name }}</a></td>
                <td>{{ frappe.format_date(order.transaction_date) }}</td>
                <td>{{ order.status }}</td>
                <td class="text-right">
                    {{ frappe.format(order.grand_total, {"fieldtype": "Currency"}) }}
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    {% else %}
    <p class="text-muted">{{ _("No orders found.") }}</p>
    {% endif %}
</div>
{% endblock %}
步骤 4:在 https://yoursite.com/my-orders 进行测试

工作流 5:注册自定义 Jinja 方法

步骤 1:添加到 hooks.py
jenv = {
    "methods": ["myapp.jinja_utils.methods"],
    "filters": ["myapp.jinja_utils.filters"]
}
步骤 2:创建方法模块
# myapp/jinja_utils/methods.py
import frappe

def get_company_logo(company):
    """Usage: {{ get_company_logo(doc.company) }}"""
    return frappe.db.get_value("Company", company, "company_logo") or ""

def format_address(address_name):
    """Usage: {{ format_address(doc.customer_address) | safe }}"""
    if not address_name:
        return ""
    return frappe.get_doc("Address", address_name).get_display()
步骤 3:创建过滤器模块
# myapp/jinja_utils/filters.py
def phone_format(value):
    """Usage: {{ doc.phone | phone_format }}"""
    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
步骤 4:部署
bench --site sitename migrate
bench --site sitename clear-cache
自定义 Jinja 方法的重要规则
  • 自定义方法应为只读——绝不要写入数据库或提交事务
  • 务必妥善处理 None/空输入(返回空字符串)
  • 绝不要调用耗时的外部 API——模板必须快速渲染

工作流 6:调试模板

模板未能渲染?








常见调试步骤
  1. 在错误日志(设置 > 错误日志)中检查模板异常
  2. 在 bench 控制台中使用 frappe.render_template(template_string, {"doc": doc})
  3. 对于打印格式:菜单 > 打印 > 检查浏览器控制台中的错误
  4. 对于门户页面:检查 Python 上下文——在 get_context 中添加 frappe.logger().info(context)
常见问题
症状原因修复方法
输出为空模板类型错误(在 Report 中使用 Jinja)报表使用 JS:{%= %}
显示“None”字段为 null使用 | default('')
货币格式错误缺少父文档上下文使用 row.get_formatted("rate", doc)
HTML 显示为文本自动转义添加 | safe(仅限可信内容)
翻译无效缺少 _() 包装包装所有字符串:{{ _("text") }}

快捷模式:子表、有条件内容、翻译

{# Child tables — ALWAYS pass parent doc for formatting context #}
{% for row in doc.items %}
  {{ row.get_formatted("rate", doc) }}  {# Correct: has currency context #}
{% endfor %}

{# Conditional sections #}
{% if doc.shipping_address_name %}
  {{ doc.shipping_address | safe }}
{% endif %}

{# Translation — ALWAYS wrap user-facing text #}
{{ _("Invoice") }}
{{ _("Page {0} of {1}").format(page, total_pages) }}
{{ doc.get_formatted("grand_total") }}  {# Auto-formats per locale #}

打印格式中的样式/CSS

@page { margin: 1.5cm; }
.avoid-break { page-break-inside: avoid; }
thead { display: table-header-group; }   /* Repeat header on pages */
.page-break { page-break-before: always; }
/* V14/V15: NO flexbox (wkhtmltopdf). V16 Chrome PDF: flexbox OK */
.layout { display: table; width: 100%; }
.col { display: table-cell; vertical-align: top; }

上下文变量快速参考

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

版本差异

功能V14V15V16
Jinja 模板是是是
get_formatted()是是是
jenv 钩子是是是
wkhtmltopdf PDF是是已弃用
Chrome PDF否否是

V16 Chrome PDF 支持现代 CSS(flexbox、grid、CSS variables)。详情请参阅 frappe-syntax-jinja。


参考文件

文件内容
decision-tree.md完整的模板类型选择流程图
print-format-decision.mdJinja、Print Designer 与 JS Microtemplate 的决策树
workflows.md所有模板类型的分步模式
examples.md可用于生产环境的模板(发票、电子邮件、门户)

评论

支持 Markdown