frappe-impl-hooks について
Frappe 開発者向けに hooks.py の設定手順をまとめたガイドです。イベント、スケジュール、権限、アセット注入を扱い、バージョンごとの違いを踏まえて実装・デプロイ・テストを進められます。
主な機能
- 01ドキュメントイベントの設定
- 02定期タスクと Cron の設定
- 03DocType クラスと権限の拡張
- 04アセットと Web サイトフックの注入
- 05フック変更の移行とテスト
活用シーン
- 01別アプリの DocType イベント処理
- 02クリーンアップやレポートの定期実行
- 03Sales Invoice の動作拡張
- 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.mddecision-tree.mdexamples.mdworkflows.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 | — |
| 毎時 | hourly | hourly_long |
| 毎日 | daily | daily_long |
| 毎週 | weekly | weekly_long |
| 毎月 | monthly | monthly_long |
| カスタム | cron | cron(長時間キューを手動で使用) |
ステップ 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.py | 1. Python ハンドラーを作成する、2. doc_events を追加する、3. サーバースクリプトを無効化する、4. 移行する | |
| hooks.py → コントローラー | 1. ロジックを doctype .py に移動する、2. doc_events エントリーを削除する、3. 移行する | |
| コントローラー → hooks.py | 1. イベントモジュールを作成する、2. doc_events を追加する、3. コントローラーから削除する、4. 移行する |
必ず、hooks.py を変更した後は毎回移行してください:bench --site sitename migrate
ハンドラーシグネチャのクイックリファレンス
| フック | シグネチャ |
|---|---|
| doc_events | def handler(doc, method=None): |
| rename events | def handler(doc, method, old, new, merge): |
| scheduler_events | def handler():(引数なし) |
| extend_bootinfo | def handler(bootinfo): |
| permission_query | def handler(user): SQL 文字列を返します |
| has_permission | def handler(doc, user=None, permission_type=None): True/False/None を返します |
| on_login | def handler(login_manager): |
| on_logout | def handler(): |
バージョンの違い
| 機能 | V14 | V15 | V16 |
|---|---|---|---|
| doc_events | はい | はい | はい |
| scheduler_events | はい | はい | はい |
| override_doctype_class | はい | はい | はい |
| extend_doctype_class | いいえ | いいえ | はい |
| permission hooks | はい | はい | はい |
| スケジューラーのティック間隔 | 約4分 | 約4分 | 約60秒 |
| auth_hooks | いいえ | はい | はい |
参考ファイル
| ファイル | 内容 |
|---|---|
| decision-tree.md | フック選択の完全なフローチャート |
| workflows.md | 実装パターンの手順 |
| examples.md | すべてのフックタイプの動作するコード例 |