frappe-syntax-hooks-events について
Frappe で doc_events を使う開発者向けのリファレンスです。イベント順序、ハンドラー登録、イベントの選び方、複数アプリでの拡張とトランザクションを説明します。
主な機能
- 01挿入・保存・送信時のイベント順序
- 02hooks.py でのハンドラー登録
- 03用途に応じたイベントの選択
- 04doc_events とコントローラーイベントの比較
- 05複数アプリの実行順とトランザクション規則
活用シーン
- 01保存前のドキュメントデータ検証
- 02送信後の関連ドキュメント作成
- 03別アプリの DocType へのイベント処理追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-hooks-eventsインストール後、Claude Code で/frappe-syntax-hooks-eventsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/.gitkeepanti-patterns.mdevent-order.mdexamples.md詳細
ドキュメントのライフサイクルフック(doc_events)
クイックリファレンス:イベントの実行順序
挿入(新規ドキュメント)
| 順序 | イベント | 目的 | 例外を発生させられるか? |
|---|---|---|---|
| 1 | before_insert | 名前を付ける前にデフォルト値を設定する | はい |
| 2 | before_naming | 命名ロジックを変更する | はい |
| 3 | autoname | name プロパティを設定する | はい |
| 4 | before_validate | 未設定の値を自動設定する | はい |
| 5 | validate | 検証ロジック — 例外を発生させて中止する | はい |
| 6 | before_save | データベースへの書き込み前に最終変更を行う | はい |
| 7 | db_insert | 内部処理 — 行をデータベースに書き込む | — |
| 8 | after_insert | 挿入後の処理(初回のみ実行) | はい |
| 9 | on_update | 保存後の処理(保存のたびに実行) | はい |
| 10 | on_change | フィールド値に変更があった場合に実行される | はい |
保存(既存のドキュメント)
| 順序 | イベント | 目的 |
|---|---|---|
| 1 | before_validate | 未設定の値を自動設定する |
| 2 | validate | 検証ロジック — 例外を発生させて中止する |
| 3 | before_save | データベースへの書き込み前に最終変更を行う |
| 4 | db_update | 内部処理 — 行をデータベースで更新する |
| 5 | on_update | 保存後の処理 |
| 6 | on_change | フィールド値に変更があった場合に実行される |
送信
| 順序 | イベント | 目的 |
|---|---|---|
| 1 | before_validate | 未設定の値を自動設定する |
| 2 | validate | 検証ロジック |
| 3 | before_save | データベースへの書き込み前に最終変更を行う |
| 4 | before_submit | 送信前の処理 — 例外を発生させて中止する |
| 5 | db_update | 内部処理 — 行をデータベースで更新する |
| 6 | on_submit | 送信後の処理(GL エントリーなど) |
| 7 | on_update | 保存後の処理 |
| 8 | on_change | フィールド値に変更があった場合に実行される |
キャンセル
| 順序 | イベント | 目的 |
|---|---|---|
| 1 | before_cancel | キャンセル前の検証 |
| 2 | db_update | 内部処理 — 行をデータベースで更新する |
| 3 | on_cancel | キャンセル後の処理(GL の取り消しなど) |
| 4 | on_change | フィールド値に変更があった場合に実行される |
削除
| 順序 | イベント | 目的 |
|---|---|---|
| 1 | on_trash | 削除前のクリーンアップ |
| 2 | after_delete | 削除後の処理 |
その他の操作
| 操作 | イベント(実行順) |
|---|---|
| 名前の変更 | before_rename → after_rename |
| 修正 | 新しい修正版ドキュメントで before_insert の処理が実行される |
| 送信後の更新 | before_update_after_submit → db_update → on_update_after_submit → on_change |
hooks.py の doc_events:構文
基本構造
# hooks.py
doc_events = {
"Sales Invoice": {
"on_submit": "myapp.events.sales_invoice.on_submit",
"on_cancel": "myapp.events.sales_invoice.on_cancel",
},
"Purchase Order": {
"validate": "myapp.events.purchase_order.validate",
}
}
ワイルドカード:すべての DocType に適用
doc_events = {
"*": {
"after_insert": "myapp.events.global_handler.after_insert_all",
"on_update": "myapp.events.global_handler.track_changes",
}
}
キーには必ず "*"(アスタリスクを含む文字列)を使用してください。すべての DocType でハンドラーが実行されます。
イベントごとの複数のハンドラー
doc_events = {
"Sales Invoice": {
"on_submit": [
"myapp.events.accounting.create_gl_entries",
"myapp.events.notifications.send_invoice_email",
]
}
}
ハンドラー関数のシグネチャ
# myapp/events/sales_invoice.py
def on_submit(doc, method=None):
"""
doc — the Document instance (e.g., Sales Invoice)
method — string name of the event (e.g., "on_submit"), or None
"""
if doc.grand_total > 10000:
frappe.sendmail(...)
2 番目のパラメーターとして、デフォルト値を None にした method を必ず受け取るようにしてください。Frappe によって自動的に渡されます。
意思決定ツリー:使用するイベントの選び方
「保存する前にデータを検証したい」
→ validate を使用します。無効なデータの保存をブロックするため、必ずここで frappe.throw() を呼び出してください。
「デフォルト値を自動的に設定したい」
→ before_validate を使用します。これは validate の前に実行されるため、検証チェックの前にデフォルト値を設定できます。
「初回作成時だけロジックを実行したい」
→ after_insert を使用します。これは挿入時にのみ実行され、以降の保存では実行されません。
「毎回の保存時(挿入+更新)にロジックを実行したい」
→ on_update を使用します。これは挿入操作と保存操作の両方で実行されます。
「送信後にリンクされたドキュメントを作成したい」
→ on_submit を使用します。validate でリンクされたドキュメントを作成してはいけません。ドキュメントはまだコミットされていません。
「キャンセル時にリンクされたドキュメントを取り消したい」
→ on_cancel を使用します。GL エントリー、在庫台帳エントリー、リンクされたドキュメントは、必ずここでクリーンアップしてください。
「ドキュメント名を変更したい」
→ コントローラーの autoname を使用するか、条件付きロジックには before_naming を使用します。
「特定の条件下で削除を防止したい」
→ on_trash を使用します。削除をブロックするには frappe.throw() を呼び出してください。
「送信済みドキュメントのフィールドを更新したい」
→ 検証には before_update_after_submit を、副作用の処理には on_update_after_submit を使用します。
「値が実際に変更された場合にのみ実行されるロジックが必要」
→ on_change を使用します。少なくとも1つのフィールド値が DB の状態と異なる場合にのみ実行されます。
doc_events と Controller Events
どちらの仕組みも同じイベントをトリガーします。違いは登録場所です。
| 項目 | コントローラー(クラスメソッド) | doc_events(hooks.py) |
|---|---|---|
| 場所 | {doctype}.py コントローラーファイル | アプリ内の hooks.py |
| 使用する場面 | 対象の DocType を所有している場合 | 別のアプリの DocType を拡張する場合 |
| 実行順序 | 最初に実行(コントローラー) | コントローラーメソッドの後に実行 |
| 複数アプリ | DocType ごとにコントローラーは1つのみ | 複数のアプリでハンドラーを登録可能 |
自分が所有していない DocType にフックする場合は、必ず doc_events を使用してください。別のアプリのコントローラーファイルを直接変更してはいけません。
単一イベント内の実行順序
特定のイベント(例:validate)では、次の順序で実行されます。
- コントローラーメソッドが最初に実行されます(
def validate(self)) doc_eventsハンドラーがアプリのインストール順に実行されます- ワイルドカード
"*"ハンドラーが、特定の DocType のハンドラーの後に実行されます
extend_doctype_class [v16+]
Frappe v16+ では、既存の DocType にメソッドを追加する場合、extend_doctype_class は doc_events よりもすっきりとした方法です。
hooks.py
extend_doctype_class = {
"Sales Invoice": [
"myapp.overrides.sales_invoice.SalesInvoiceExtension"
]
}
拡張クラス(Mixin)
# myapp/overrides/sales_invoice.py
import frappe
class SalesInvoiceExtension:
def validate(self):
"""This is called as part of the controller chain."""
if self.grand_total < 0:
frappe.throw("Grand total cannot be negative")
def custom_method(self):
"""Custom methods are also available on the doc instance."""
return self.items
主なルール
- v16+ で複数のアプリが同じ DocType を拡張する可能性がある場合は、
override_doctype_classよりも必ずextend_doctype_classを使用してください。 - 複数のアプリで同じ DocType を拡張できます。拡張は MRO を通じて積み重ねられます。
- クラスの解決順序は hooks の優先順位に従います:
class Final(App2Mixin, App1Mixin, Original)。 - 拡張メソッド(
validateなど)はコントローラーの一部として実行され、個別の doc_events ハンドラーとしては実行されません。
override_doctype_class [v14+]
コントローラークラスを完全に置き換えます。使用には十分注意してください。
# hooks.py
override_doctype_class = {
"ToDo": "myapp.overrides.todo.CustomToDo"
}
# myapp/overrides/todo.py
from frappe.desk.doctype.todo.todo import ToDo
class CustomToDo(ToDo):
def validate(self):
super().validate() # ALWAYS call super() to preserve original logic
# Your additions here
extend_doctype_class が利用可能な場合(v16+)は、override_doctype_class を使用してはいけません。DocType をオーバーライドできるアプリは1つのみです。最後にインストールされたアプリが優先され、他のアプリが気付かないうちに動作しなくなるおそれがあります。
複数アプリ間のイベント実行順序
複数のアプリが同じ DocType とイベントに対して doc_events を登録している場合:
- ハンドラーはアプリのインストール順(
sites/{site}/site_config.jsonのinstalled_appsに記載された順)に実行されます。 - 順序は Setup > Installed Applications > Update Hooks Resolution Order から変更できます。
override_doctype_classでは、最後にインストールされたアプリが優先されます(適用されるオーバーライドは1つのみ)。extend_doctype_class(v16+)では、すべての拡張が累積的に積み重ねられます。
トランザクションの動作
before_validate から on_change までのすべてのドキュメントイベントは、単一のデータベーストランザクション内で実行されます。
- いずれかのイベントで例外が発生した場合、操作全体がロールバックされます(
db_insert/db_updateも含みます)。 after_insert、on_update、on_submit、on_cancelはすべて、トランザクションがコミットされる前に実行されます。- トランザクションがコミットされるのは、すべてのイベントが正常に完了した後だけです。
after_deleteは DELETE 文の実行後に呼び出されますが、リクエストのトランザクション内で実行されます。
イベントハンドラー内でデータがデータベースにコミットされていると決して想定しないでください。他の同時リクエストから変更が見えるようになるのは、リクエスト全体が完了した後です。
重要なルール
- 操作を中断するには、必ず
frappe.throw()を使用してください。raise Exceptionは決して使用しないでください。 autonameまたはbefore_namingの外でdoc.nameを変更しないでください。- サブクラスでコントローラーメソッドをオーバーライドする場合は、必ず
super().{event}()を呼び出してください。 validateまたはbefore_save内でdoc.save()を使用しないでください。無限再帰が発生します。- フックで権限を回避する必要がある場合は、必ず明示的に
doc.flags.ignore_permissions = Trueを設定してください。フックが Administrator として実行されると決して想定しないでください。 validateに時間のかかる処理(API 呼び出し、ファイル I/O)を入れないでください。代わりにafter_insertまたはon_updateでfrappe.enqueue()を使用してください。- 同じリクエスト内のイベント間で情報を伝えるには、必ず
doc.flagsを使用してください(例:doc.flags.skip_notification = True)。 - 重要なロジックを
on_changeに依存させないでください。データベースの状態と値が実際に異なる場合にのみ実行されます。
関連項目
- イベント実行順序 — 詳細な図
- よくあるパターンの実用例
- アンチパターンとよくある間違い
frappe-syntax-hooks-config— アプリレベルのフック(スケジューラー、フィクスチャ、権限)- 公式ドキュメント:https://docs.frappe.io/framework/user/en/basics/doctypes/controllers