frappe-impl-jinja について
Frappe 開発者向けの Jinja テンプレート実装ガイドです。印刷フォーマット、メールや通知、ポータルページの作成手順に加え、PDF 描画や CSS のバージョン差、デバッグ方法を説明します。
主な機能
- 01印刷フォーマットテンプレートの作成
- 02メール・通知テンプレートの作成
- 03ポータルページの構築
- 04カスタム Jinja メソッドとフィルターの登録
- 05テンプレート描画エラーの診断
活用シーン
- 01請求書や発注書の PDF 生成
- 02文書の動的内容を含むメール送信
- 03注文ポータルページの作成
- 04書式・翻訳・null 値の問題のデバッグ
インストール
コマンドをコピーしてターミナルで実行します。
$
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.mddecision-tree.mdexamples.mdprint-format-decision.mdworkflows.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:テストする
- Sales Invoice を開きます
- メニュー > 印刷 >「My Invoice Format」を選択します
- レイアウトと書式を確認します
- 必ず 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:テンプレートをデバッグする
テンプレートがレンダリングされない場合
一般的なデバッグ手順
- テンプレートの例外がないか、Error Log(Setup > Error Log)を確認します
- bench コンソールで
frappe.render_template(template_string, {"doc": doc})を使用します - 印刷フォーマットの場合:メニュー > 印刷 > ブラウザーのコンソールでエラーを確認します
- ポータルページの場合:Python コンテキストを確認します —
get_contextにfrappe.logger().info(context)を追加します
よくある問題
| 症状 | 原因 | 対処方法 |
|---|---|---|
| 出力が空白 | テンプレートの種類が違う(レポートで 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、カスタムコンテキスト |
バージョンごとの差異
| 機能 | V14 | V15 | V16 |
|---|---|---|---|
| Jinja テンプレート | はい | はい | はい |
| get_formatted() | はい | はい | はい |
| jenv フック | はい | はい | はい |
| wkhtmltopdf PDF | はい | はい | 非推奨 |
| Chrome PDF | いいえ | いいえ | はい |
V16 の Chrome PDF は最新の CSS(flexbox、grid、CSS 変数)に対応しています。詳細については
frappe-syntax-jinjaを参照してください。
参照ファイル
| ファイル | 内容 |
|---|---|
| decision-tree.md | テンプレートの種類を選択するための完全なフローチャート |
| print-format-decision.md | Jinja、Print Designer、JS Microtemplate の選択ツリー |
| workflows.md | すべてのテンプレートの種類に対応した、手順ごとのパターン |
| examples.md | 本番環境ですぐに使えるテンプレート(請求書、メール、ポータル) |
コメント
Markdown 対応