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書式・翻訳・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.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. Sales Invoice を開きます
  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. テンプレートの例外がないか、Error Log(Setup > Error Log)を確認します
  2. bench コンソールで frappe.render_template(template_string, {"doc": doc}) を使用します
  3. 印刷フォーマットの場合:メニュー > 印刷 > ブラウザーのコンソールでエラーを確認します
  4. ポータルページの場合: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、カスタムコンテキスト

バージョンごとの差異

機能V14V15V16
Jinja テンプレートはいはいはい
get_formatted()はいはいはい
jenv フックはいはいはい
wkhtmltopdf PDFはいはい非推奨
Chrome PDFいいえいいえはい

V16 の Chrome PDF は最新の CSS(flexbox、grid、CSS 変数)に対応しています。詳細については frappe-syntax-jinja を参照してください。


参照ファイル

ファイル内容
decision-tree.mdテンプレートの種類を選択するための完全なフローチャート
print-format-decision.mdJinja、Print Designer、JS Microtemplate の選択ツリー
workflows.mdすべてのテンプレートの種類に対応した、手順ごとのパターン
examples.md本番環境ですぐに使えるテンプレート(請求書、メール、ポータル)

コメント