mcpField
F
frappe-impl-hooks

frappe-impl-hooks

github.com/impertio-studio開発198118 回インストール30 週間前に更新
リポジトリ

frappe-impl-hooks について

Frappe 開発者向けに hooks.py の設定手順をまとめたガイドです。イベント、スケジュール、権限、アセット注入を扱い、バージョンごとの違いを踏まえて実装・デプロイ・テストを進められます。

主な機能

  1. 01ドキュメントイベントの設定
  2. 02定期タスクと Cron の設定
  3. 03DocType クラスと権限の拡張
  4. 04アセットと Web サイトフックの注入
  5. 05フック変更の移行とテスト

活用シーン

  1. 01別アプリの DocType イベント処理
  2. 02クリーンアップやレポートの定期実行
  3. 03Sales Invoice の動作拡張
  4. 04権限フィルターやページアセットの設定

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/frappe-impl-hooksを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

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

詳細

Frappe フックの実装ワークフロー

hooks.py の設定を実装するためのステップごとのワークフローです。API 構文のリファレンスについては、frappe-syntax-hooks を参照してください。

バージョン:v14/v15/v16(V16 固有の機能には注記があります)


全体の判断:何を実装しますか?

WHAT DO YOU WANT TO ACHIEVE?
│
├─► React to document lifecycle events?
│   ├─► On OTHER app's DocTypes → doc_events in hooks.py
│   ├─► On YOUR OWN DocTypes → controller methods (preferred)
│   └─► On ALL DocTypes → doc_events with "*" wildcard
│
├─► Run code on a schedule?
│   └─► scheduler_events (daily, hourly, cron, etc.)
│
├─► Modify an existing DocType's behavior?
│   ├─► V16+: extend_doctype_class (RECOMMENDED)
│   └─► V14/V15: override_doctype_class (last app wins!)
│
├─► Override an existing API endpoint?
│   └─► override_whitelisted_methods
│
├─► Add custom permission logic?
│   ├─► List filtering → permission_query_conditions
│   └─► Document-level → has_permission
│
├─► Send config data to client on page load?
│   └─► extend_bootinfo
│
├─► Export/import configuration?
│   └─► fixtures
│
├─► Add JS/CSS to desk or portal?
│   ├─► Desk-wide → app_include_js / app_include_css
│   ├─► Portal-wide → web_include_js / web_include_css
│   └─► Specific form → doctype_js
│
├─► Customize website/portal behavior?
│   └─► website_context, portal_menu_items, website_route_rules
│
└─► Hook into session/auth lifecycle?
    └─► on_login, on_session_creation, on_logout

ワークフロー 1:doc_events の実装

使用する場面

他のアプリ(ERPNext、Frappe コア)が所有する DocType のドキュメントライフサイクルイベントに対応する必要がある場合は、doc_events を使用します。独自の DocType では、常にコントローラーメソッドを優先してください。

ステップごとの手順

ステップ 1:適切なイベントを選択する(references/decision-tree.md を参照)

BEFORE save: validate (every save), before_insert (new only)
AFTER save:  after_insert (new only), on_update (every save), on_change (any change)
SUBMIT flow: before_submit → on_submit → on_change
CANCEL flow: before_cancel → on_cancel → on_change
DELETE:      on_trash (before), after_delete (after)
RENAME:      before_rename, after_rename

ステップ 2:hooks.py に追加する

# myapp/hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales_invoice.validate",
        "on_submit": "myapp.events.sales_invoice.on_submit"
    }
}

ステップ 3:ハンドラーモジュールを作成する

# myapp/events/sales_invoice.py
import frappe

def validate(doc, method=None):
    """Changes to doc ARE saved (before-save event)."""
    if doc.grand_total < 0:
        frappe.throw("Total cannot be negative")

def on_submit(doc, method=None):
    """Document already saved. Use db_set_value for changes."""
    frappe.db.set_value("Sales Invoice", doc.name,
                        "custom_external_id", create_external(doc))

ステップ 4:デプロイする

bench --site sitename migrate

ステップ 5:テストする

bench --site sitename execute myapp.events.sales_invoice.validate --kwargs '{"doc_name": "INV-001"}'
# Or in bench console:
# doc = frappe.get_doc("Sales Invoice", "INV-001"); doc.save()
doc_events に関する重要なルール
  • 絶対に doc_event ハンドラー内で frappe.db.commit() を呼び出さないでください。トランザクションは Frappe が管理します
  • 絶対に on_update 内で doc のフィールドを変更しないでください。変更は失われます。代わりに frappe.db.set_value() を使用してください
  • ハンドラーのシグネチャでは、2 番目のパラメーターとして 必ず method=None を受け取ってください
  • rename のシグネチャは 必ず def handler(doc, method, old, new, merge) を使用してください
  • hooks.py を変更した後は、必ず bench --site sitename migrate を実行してください

ワークフロー 2:scheduler_events の実装

ステップごとの手順

ステップ 1:頻度を選択する

頻度短時間(5 分未満)長時間(5〜25 分)
毎ティックall—
毎時hourlyhourly_long
毎日dailydaily_long
毎週weeklyweekly_long
毎月monthlymonthly_long
カスタムcroncron(長時間キューを手動で使用)

ステップ 2:hooks.py に追加する

scheduler_events = {
    "daily": ["myapp.tasks.daily_cleanup"],
    "daily_long": ["myapp.tasks.heavy_sync"],
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.weekday_report"]
    }
}

ステップ 3:タスクを実装する(引数なし)

# myapp/tasks.py
import frappe

def daily_cleanup():
    """Scheduler calls with NO arguments."""
    frappe.db.delete("Error Log", {
        "creation": ["<", frappe.utils.add_days(None, -30)]
    })
    frappe.db.commit()

def heavy_sync():
    """Long task — commit periodically."""
    records = get_records_to_sync()
    for i, record in enumerate(records):
        process(record)
        if i % 100 == 0:
            frappe.db.commit()
    frappe.db.commit()

ステップ 4:デプロイして検証する

bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status
# Test manually:
bench --site sitename execute myapp.tasks.daily_cleanup
スケジューラーに関する重要なルール
  • スケジューラータスク関数にパラメーターを 絶対に 追加しないでください。スケジューラーは引数を渡しません
  • 5 分を超えるタスクには、必ず _long バリアントを使用してください(デフォルトのキュータイムアウトは 5 分です)
  • 長時間タスクでは、進捗を保存するために 必ず 定期的にコミットしてください
  • 25 分を超えるタスクは、チャンクに分割するか、frappe.enqueue() を使用してください

ワークフロー 3:extend_doctype_class の実装(V16 以降)

ステップごとの手順

ステップ 1:hooks.py に追加する

extend_doctype_class = {
    "Sales Invoice": ["myapp.extensions.sales_invoice.SalesInvoiceMixin"]
}

ステップ 2:ミックスインクラスを作成する

# myapp/extensions/sales_invoice.py
import frappe
from frappe.model.document import Document

class SalesInvoiceMixin(Document):
    def validate(self):
        super().validate()  # ALWAYS call super() FIRST
        self.custom_validation()

    def custom_validation(self):
        if self.grand_total > 1000000:
            frappe.msgprint("High-value invoice", indicator="orange")

ステップ 3:デプロイする — bench --site sitename migrate

extend と override の使い分け
  • V16 以降では、必ず extend_doctype_class を優先してください。複数のアプリから安全に拡張できます
  • コントローラーロジックを完全に置き換える必要がある場合に 限り、override_doctype_class を使用してください
  • V14/V15 では override_doctype_class だけが利用可能です。最後にインストールされたアプリが優先されます

ワークフロー 4:権限フックの実装

ステップごとの手順

ステップ 1:hooks.py に追加する

permission_query_conditions = {
    "Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
    "Sales Invoice": "myapp.permissions.si_permission"
}

ステップ 2:ハンドラーを実装する

# myapp/permissions.py
import frappe

def si_query(user):
    """Returns SQL WHERE clause for list filtering."""
    if not user:
        user = frappe.session.user
    if "Sales Manager" in frappe.get_roles(user):
        return ""  # See all
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"

def si_permission(doc, user=None, permission_type=None):
    """Returns True (allow), False (deny), or None (use default)."""
    if not user:
        user = frappe.session.user
    if permission_type == "write" and doc.status == "Closed":
        return False
    return None
権限フックに関する重要なルール
  • permission_query_conditions は get_list で のみ 動作し、get_all では 動作しません
  • has_permission でできるのはアクセスの拒否 のみ です。True を返しても、追加の権限は付与されません
  • user=None の場合は、デフォルトで frappe.session.user を使用するよう 必ず 処理してください

ワークフロー 5:アセットの挿入と doctype_js

グローバル JS/CSS の追加
# hooks.py
app_include_js = "/assets/myapp/js/myapp.min.js"       # Desk
app_include_css = "/assets/myapp/css/myapp.min.css"     # Desk
web_include_js = "/assets/myapp/js/portal.min.js"       # Portal
web_include_css = "/assets/myapp/css/portal.min.css"    # Portal
特定のフォームの拡張
# hooks.py
doctype_js = {
    "Sales Invoice": "public/js/sales_invoice.js"
}
// myapp/public/js/sales_invoice.js
frappe.ui.form.on("Sales Invoice", {
    refresh(frm) {
        if (frm.doc.docstatus === 1) {
            frm.add_custom_button(__("Custom Action"), () => {
                frappe.call({
                    method: "myapp.api.custom_action",
                    args: { invoice: frm.doc.name },
                    freeze: true
                });
            }, __("Actions"));
        }
    }
});

JS/CSS ファイルを変更した後は、必ず bench build --app myapp を実行してください。


ワークフロー 6:Fixtures、Boot Info、Website フック

Fixtures
fixtures = [
    {"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
    {"dt": "Property Setter", "filters": [["module", "=", "My App"]]}
]

フィルターを指定せずに fixtures をエクスポートしては 絶対に いけません。すべてのアプリのカスタマイズが取り込まれます。

extend_bootinfo
extend_bootinfo = "myapp.boot.extend_with_config"
def extend_with_config(bootinfo):
    bootinfo.my_app = {"feature_enabled": True}
    # NEVER send secrets — bootinfo is visible in browser DevTools
Website フック
website_route_rules = [
    {"from_route": "/shop/<category>", "to_route": "shop"}
]
portal_menu_items = [
    {"title": "My Orders", "route": "/my-orders", "role": "Customer"}
]
on_login = "myapp.handlers.on_login"
on_logout = "myapp.handlers.on_logout"

移行:フック、コントローラー、サーバースクリプト間でロジックを移動する

移行元移行先手順
サーバースクリプト → hooks.py1. Python ハンドラーを作成する、2. doc_events を追加する、3. サーバースクリプトを無効化する、4. 移行する
hooks.py → コントローラー1. ロジックを doctype .py に移動する、2. doc_events エントリーを削除する、3. 移行する
コントローラー → hooks.py1. イベントモジュールを作成する、2. doc_events を追加する、3. コントローラーから削除する、4. 移行する

必ず、hooks.py を変更した後は毎回移行してください:bench --site sitename migrate


ハンドラーシグネチャのクイックリファレンス

フックシグネチャ
doc_eventsdef handler(doc, method=None):
rename eventsdef handler(doc, method, old, new, merge):
scheduler_eventsdef handler():(引数なし)
extend_bootinfodef handler(bootinfo):
permission_querydef handler(user): SQL 文字列を返します
has_permissiondef handler(doc, user=None, permission_type=None): True/False/None を返します
on_logindef handler(login_manager):
on_logoutdef handler():

バージョンの違い

機能V14V15V16
doc_eventsはいはいはい
scheduler_eventsはいはいはい
override_doctype_classはいはいはい
extend_doctype_classいいえいいえはい
permission hooksはいはいはい
スケジューラーのティック間隔約4分約4分約60秒
auth_hooksいいえはいはい

参考ファイル

ファイル内容
decision-tree.mdフック選択の完全なフローチャート
workflows.md実装パターンの手順
examples.mdすべてのフックタイプの動作するコード例

コメント