frappe-syntax-controllers について
Frappe で DocType コントローラーを実装する開発者向けの構文リファレンスです。ライフサイクル、命名、拡張方法と実装上の注意点を扱います。
主な機能
- 01コントローラーファイルとクラスの命名
- 02ドキュメントライフサイクルフックの順序
- 03ドキュメント命名と autoname メソッド
- 04コントローラー拡張方式の比較
- 05ホワイトリストメソッドと文書状態
活用シーン
- 01DocType のサーバー側ロジックの実装
- 02適切なライフサイクルフックの選択
- 03既存 DocType への独自動作の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-controllersインストール後、Claude Code で/frappe-syntax-controllersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mddocument-api-complete.mdevents.mdexamples.mdflags.mdhooks.mdlifecycle-methods.mdmethods.mdpatterns.mdsyntax.md詳細
Frappe の構文:ドキュメントコントローラー
ドキュメントコントローラーは、DocType のサーバー側ロジックをすべて定義する Python クラスです。
すべての DocType には、コントローラーファイルが必ず 1 つだけあります。コントローラークラスは frappe.model.document.Document を継承します。
クイックリファレンス
import frappe
from frappe import _
from frappe.model.document import Document
class SalesOrder(Document):
def autoname(self):
"""Custom naming logic. Sets self.name."""
self.name = f"SO-{self.customer_code}-{frappe.utils.now_datetime().year}"
def validate(self):
"""MAIN validation — runs on EVERY save (insert and update).
Changes to self ARE saved to database."""
if not self.items:
frappe.throw(_("Items are required"))
self.total = sum(item.amount for item in self.items)
def on_update(self):
"""After save — changes to self are NOT saved.
Use frappe.db.set_value() for post-save field changes."""
self.notify_linked_docs()
def on_submit(self):
"""After submit (docstatus 0 -> 1). Create ledger entries here."""
self.create_gl_entries()
def on_cancel(self):
"""After cancel (docstatus 1 -> 2). Reverse ledger entries here."""
self.reverse_gl_entries()
@frappe.whitelist()
def recalculate(self):
"""Exposed to client JS via frm.call('recalculate')."""
self.total = sum(item.amount for item in self.items)
return {"total": self.total}
ファイルの場所と命名規則
| DocType 名 | クラス名 | ファイルパス |
|---|---|---|
| Sales Order | SalesOrder | selling/doctype/sales_order/sales_order.py |
| My Custom Doc | MyCustomDoc | module/doctype/my_custom_doc/my_custom_doc.py |
ルール:DocType 名 -> PascalCase のクラス -> snake_case のファイル名。必ず正確に一致させてください。
ライフサイクルフックの実行順序
挿入(新しいドキュメント)
before_insert -> before_naming -> autoname -> before_validate -> validate
-> before_save -> [db_insert] -> after_insert -> on_update -> on_change
保存(既存のドキュメント)
before_validate -> validate -> before_save -> [db_update]
-> on_update -> on_change
送信(docstatus 0 -> 1)
before_validate -> validate -> before_submit -> [db_update]
-> on_submit -> on_update -> on_change
キャンセル(docstatus 1 -> 2)
before_cancel -> [db_update] -> on_cancel -> on_change
送信後の更新
before_update_after_submit -> [db_update]
-> on_update_after_submit -> on_change
削除
on_trash -> [db_delete] -> after_delete
破棄 [v15+]
before_discard -> [db_set docstatus=2] -> on_discard
パラメーターを含むフックの完全なリファレンス:lifecycle-methods.md を参照してください。
フック選択の判断ツリー
What do you need to do?
|
+-- Validate data or calculate fields?
| +-- validate (changes to self ARE saved)
|
+-- Action AFTER save (emails, sync, linked docs)?
| +-- on_update (changes to self are NOT saved)
|
+-- Only for NEW documents?
| +-- after_insert (runs once on first save only)
|
+-- Custom document name?
| +-- autoname (set self.name)
|
+-- Before/after SUBMIT?
| +-- Validate before submit? -> before_submit
| +-- Create entries after submit? -> on_submit
|
+-- Before/after CANCEL?
| +-- Check linked docs? -> before_cancel
| +-- Reverse entries? -> on_cancel
|
+-- Cleanup before delete?
| +-- on_trash
|
+-- React to ANY value change (including db_set)?
| +-- on_change (MUST be idempotent)
重要なルール
1. on_update の後の変更は保存されません
# WRONG - change is lost after on_update
def on_update(self):
self.status = "Completed" # NOT saved to database
# CORRECT - use db_set or frappe.db.set_value
def on_update(self):
self.db_set("status", "Completed")
2. コントローラー内で frappe.db.commit() を呼び出さないでください
# WRONG - breaks Frappe transaction management
def validate(self):
frappe.db.commit() # Can cause partial updates on error
# CORRECT - Frappe commits automatically at end of request
def validate(self):
self.update_related() # No commit needed
3. オーバーライドするときは必ず super() を呼び出してください
# WRONG - parent validation is skipped entirely
def validate(self):
self.custom_check()
# CORRECT - parent logic preserved
def validate(self):
super().validate()
self.custom_check()
4. 再帰を防止するにはフラグを使用してください
def on_update(self):
if self.flags.get("from_linked_doc"):
return
linked = frappe.get_doc("Linked Doc", self.linked_doc)
linked.flags.from_linked_doc = True
linked.save()
5. 検証ロジックを on_update に記述しないでください
# WRONG - document is already saved when this throws
def on_update(self):
if self.total < 0:
frappe.throw("Invalid total") # Too late!
# CORRECT - validate BEFORE save
def validate(self):
if self.total < 0:
frappe.throw("Invalid total") # Blocks save
ドキュメントの命名(autoname)
| メソッド | 例 | 結果 | バージョン |
|---|---|---|---|
field:fieldname | field:customer_name | ABC Company | すべて |
naming_series: | naming_series: | SO-2024-00001 | すべて |
| 式 | PRE-.##### | PRE-00001 | すべて |
| 旧形式 | INV-{YYYY}-{####} | INV-2024-0001 | v16 で非推奨 |
hash / random | hash | a1b2c3d4e5 | すべて |
Prompt | Prompt | ユーザーが名前を入力 | すべて |
autoincrement | autoincrement | 1、2、3 | すべて |
UUID | UUID | 550e8400-e29b-... | v16+ |
| カスタムメソッド | コントローラー内の autoname() | 任意のパターン | すべて |
カスタム autoname メソッド
from frappe.model.naming import getseries
class Project(Document):
def autoname(self):
prefix = f"P-{self.customer[:3].upper()}-"
self.name = getseries(prefix, 3)
# Result: P-ACM-001, P-ACM-002, etc.
UUID による命名 [v16+]
DocType 定義で autoname = "UUID" を設定します。Frappe が UUID v4 を生成します。
When to use UUID: When to use traditional naming:
- Cross-system sync - User-facing references (SO-00001)
- Bulk record creation - Sequential numbering required
- Global uniqueness needed - Auditing requires readable names
コントローラーの拡張方法
1. override_doctype_class(完全な置き換え)[すべてのバージョン]
# hooks.py
override_doctype_class = {
"Sales Order": "custom_app.overrides.CustomSalesOrder"
}
# custom_app/overrides.py
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate() # ALWAYS call super()
self.custom_validation()
警告:DocType クラスをオーバーライドできるアプリは 1 つだけです。複数のオーバーライドは競合します。
2. extend_doctype_class(ミックスイン、非破壊的)[v16+]
# hooks.py
extend_doctype_class = {
"Address": ["custom_app.extensions.address.GeocodingMixin"],
"Contact": [
"custom_app.extensions.common.ValidationMixin",
"custom_app.extensions.contact.PhoneMixin"
]
}
# custom_app/extensions/address.py
from frappe.model.document import Document
class GeocodingMixin(Document):
@property
def full_address(self):
return f"{self.address_line1}, {self.city}, {self.country}"
def validate(self):
super().validate()
self.geocode_address()
v16 以降では、override_doctype_class よりも extend_doctype_class を必ず優先してください。
複数のアプリで同じ DocType を安全に拡張できます。
3. doc_events(個々のイベントにフック)[すべてのバージョン]
# hooks.py
doc_events = {
"Sales Order": {
"validate": "custom_app.events.validate_sales_order",
"on_submit": "custom_app.events.on_submit_sales_order"
},
"*": { # ALL DocTypes
"after_insert": "custom_app.events.log_creation"
}
}
# custom_app/events.py
def validate_sales_order(doc, method=None):
if doc.total > 100000:
doc.requires_approval = 1
使い分け
Need full class replacement? -> override_doctype_class [all versions]
Need to add methods/properties? -> extend_doctype_class [v16+]
Need to hook one or two events? -> doc_events [all versions]
Need to extend in v14/v15? -> override_doctype_class or doc_events
ホワイトリスト登録されたメソッド
@frappe.whitelist() を使用して、コントローラーメソッドをクライアント側 JavaScript から利用できるようにします。
class SalesOrder(Document):
@frappe.whitelist()
def send_email(self, recipient):
"""Callable from JS: frm.call('send_email', {recipient: '...'})"""
frappe.sendmail(recipients=[recipient], message="Order confirmed")
return {"status": "sent"}
// Client-side call
frm.call('send_email', { recipient: '[email protected]' })
.then(r => frappe.msgprint(r.message.status));
ルール:
- 必ず
@frappe.whitelist()デコレーターを追加してください。これがないと、メソッドをクライアントから呼び出せません。 - メソッドはコントローラークラス上に定義する必要があります(単独で定義しないでください)。
- 権限チェックは自動的に行われます(ユーザーにはドキュメントへの読み取りアクセス権が必要です)。
送信可能なドキュメント
is_submittable = 1 のドキュメントは、docstatus のライフサイクルに従います。
| docstatus | 状態 | 編集可能 | 遷移 |
|---|---|---|---|
| 0 | 下書き | はい | -> 1(送信) |
| 1 | 送信済み | 「送信時に許可」フィールドのみ | -> 2(キャンセル) |
| 2 | キャンセル済み | いいえ | なし(修正により新しい下書きを作成) |
on_submit と on_cancel は必ずセットで実装してください。
on_submit で作成したものは、必ず on_cancel で元に戻してください。
継承パターン
# Standard controller
from frappe.model.document import Document
class MyDoc(Document): pass
# Tree DocType (hierarchical)
from frappe.utils.nestedset import NestedSet
class Department(NestedSet):
nsm_parent_field = "parent_department"
# Virtual DocType (no database table)
class ExternalData(Document):
def load_from_db(self): ...
def db_insert(self, *args, **kwargs): ...
def db_update(self, *args, **kwargs): ...
@staticmethod
def get_list(args): ...
@staticmethod
def get_count(args): ...
型アノテーション [v15+]
class Person(Document):
if TYPE_CHECKING:
from frappe.types import DF
first_name: DF.Data
last_name: DF.Data
birth_date: DF.Date
company: DF.Link
hooks.py で自動生成を有効にします:export_python_type_annotations = True
バージョンごとの差異
| 機能 | v14 | v15 | v16 |
|---|---|---|---|
| 型アノテーション | なし | 自動生成 | あり |
before_discard / on_discard | なし | あり | あり |
flags.notify_update | なし | あり | あり |
extend_doctype_class | なし | なし | あり |
| UUID による自動命名 | なし | なし | あり |
| 旧形式の命名 | あり | あり | 非推奨 |
参考ファイル
| ファイル | 内容 |
|---|---|
| lifecycle-methods.md | 実行順序の図を含む、すべてのフック |
| document-api-complete.md | Document API 完全版:カテゴリ別(CRUD、フィールド、DB、権限、フラグ、子テーブル、命名)のすべてのメソッド |
| methods.md | Document クラスのメソッドシグネチャ |
| events.md | 順序どおりに並んだすべてのドキュメントイベント |
| examples.md | 動作するコントローラーの完全な例 |
| anti-patterns.md | よくある間違いと修正方法 |
| flags.md | フラグシステム(doc.flags、frappe.flags) |
| hooks.md | hooks.py とコントローラーの連携 |
| patterns.md | よく使われるコントローラーパターン |
| syntax.md | コントローラークラスの構文リファレンス |
関連スキル
frappe-syntax-serverscripts-- Server Scripts(サンドボックスを使う代替手段)frappe-syntax-hooks-- hooks.py の設定frappe-impl-controllers-- 実装ワークフローfrappe-core-permissions-- 権限システム