frappe-syntax-jinja について
frappe-syntax-jinjaは、Frappeの印刷フォーマット、メール、通知、ポータルページ向けJinjaテンプレートのリファレンススキルです。構文やコンテキスト、バージョン別のレンダリング方法を扱います。
主な機能
- 01Jinja構文と制御構造
- 02テンプレート種別ごとのコンテキスト
- 03Frappeのメソッドとフィルター
- 04hooks.pyによる独自メソッドとフィルターの登録
- 05印刷フォーマットとポータルページのパターン
- 06v14~v16のレンダリング指針
活用シーン
- 01Frappeの印刷フォーマットの作成
- 02動的なメールまたは通知テンプレートの作成
- 03Pythonコントローラーを使ったポータルページの作成
インストール
コマンドをコピーしてターミナルで実行します。
$
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()を使用します - オプションまたは null 許容のフィールドには
default()フィルターを使用します - 子行の
get_formatted("field", doc)には親のdocを渡します - 1つのフィールドのみ必要な場合は
frappe.db.get_value()を使用します - 計算は Jinja テンプレートではなく、Python コントローラーで行います
絶対に行わないこと
- ループ内でデータベースクエリを実行します(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 対応