frappe-impl-controllers について
カスタムアプリで DocType のサーバー側ロジックを実装する Frappe 開発者向けのワークフローガイドです。フックの実行順序や注意点を示し、検証、ERPNext の拡張、テストまで扱います。
主な機能
- 01DocType コントローラーの作成
- 02ライフサイクルフックの選択
- 03データ検証と変更検知
- 04命名とドキュメント送信のカスタマイズ
- 05ERPNext コントローラーの拡張
- 06テストの作成と実行
活用シーン
- 01DocType にサーバー側の検証や計算を追加
- 02保存後の通知や関連ドキュメントを処理
- 03標準 ERPNext DocType の動作をカスタマイズ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-controllersインストール後、Claude Code で/frappe-impl-controllersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mddecision-tree.mdexamples.mdworkflows.md詳細
ドキュメントコントローラー — 実装ワークフロー
Python の機能をフルに活用して、サーバー側の DocType ロジックを構築するための手順を説明します。正確な構文については、frappe-syntax-controllers を参照してください。
バージョン:v14/v15/v16 | v15+:自動生成される型注釈をサポート
クイック判断:コントローラーと Server Script のどちらを使うべきですか?
NEED full Python (imports, classes, generators)? → Controller
NEED external libraries (requests, pandas)? → Controller
NEED try/except with rollback? → Controller
NEED frappe.enqueue() for background jobs? → Controller
NEED to extend standard ERPNext DocType? → Controller
Quick validation without custom app? → Server Script
Simple auto-fill or notification? → Server Script
ルール:カスタムアプリが必要な場合は、必ずコントローラーを使用してください。ノーコードでプロトタイプを作成する場合は、必ず Server Script を使用してください。
ワークフロー 1:新しいコントローラーを作成する
ステップ 1:Frappe UI または bench new-doctype で DocType を作成します
ステップ 2:ファイルは次の場所に自動生成されます:
apps/myapp/myapp/{module}/doctype/{doctype_name}/{doctype_name}.py
ステップ 3:コントローラークラスを実装します:
import frappe
from frappe import _
from frappe.model.document import Document
class MyDocType(Document):
def validate(self):
self.validate_dates()
self.calculate_totals()
def validate_dates(self):
if self.from_date and self.to_date and self.from_date > self.to_date:
frappe.throw(_("From Date cannot be after To Date"))
def calculate_totals(self):
self.total = sum(item.amount for item in self.items)
ステップ 4:bench restart を実行します(または開発時のホットリロードには bench watch を使用します)
命名規則:DocType「Sales Order」→ クラス SalesOrder、ファイル sales_order.py
ワークフロー 2:適切なフックを選択する
WHAT DO YOU WANT?
├── Validate data / calculate fields before save?
│ └── validate — changes to self ARE saved
│
├── Action AFTER save (emails, linked docs, logs)?
│ └── on_update — changes to self NOT saved (use db_set)
│
├── Only for NEW documents?
│ └── after_insert
│
├── Before/after SUBMIT?
│ ├── Check before submit → before_submit
│ └── Ledger entries after → on_submit
│
├── Before/after CANCEL?
│ ├── Prevent cancel → before_cancel
│ └── Reverse entries → on_cancel
│
├── Before DELETE?
│ └── on_trash (throw to prevent)
│
├── Custom document naming?
│ └── autoname
│
└── Detect ANY change (including db_set)?
└── on_change
実行順序を含むすべてのフックについては、references/decision-tree.md を参照してください。
重要:validate と on_update の違い
| 項目 | validate | on_update |
|---|---|---|
| タイミング | DB 書き込み前 | DB 書き込み後 |
self.x = y は保存されるか | はい | いいえ — db_set を使用します |
| throw で中断できるか | はい | すでに保存済みです |
get_doc_before_save() | 利用可能 | 利用可能 |
| 用途 | 検証、計算 | 通知、リンクされたドキュメント |
# WRONG — changes in on_update are NOT saved
def on_update(self):
self.status = "Completed" # LOST!
# CORRECT — use db_set
def on_update(self):
frappe.db.set_value(self.doctype, self.name, "status", "Completed")
ワークフロー 3:エラーを収集する検証
def validate(self):
errors = []
if not self.items:
errors.append(_("At least one item is required"))
for item in self.items:
if item.qty <= 0:
errors.append(_("Row {0}: Qty must be positive").format(item.idx))
if self.from_date > self.to_date:
errors.append(_("From Date cannot be after To Date"))
if errors:
frappe.throw("<br>".join(errors))
ワークフロー 4:フィールドの変更を検出する
def validate(self):
old = self.get_doc_before_save()
if old and old.status != self.status:
self.flags.status_changed = True
self.status_changed_on = frappe.utils.now()
def on_update(self):
if self.flags.get('status_changed'):
self.notify_status_change()
ルール:フック間でデータを渡す場合は、必ず self.flags を使用してください。外部状態には絶対に依存しないでください。
ワークフロー 5:カスタム命名(autoname)
from frappe.model.naming import getseries
def autoname(self):
# Format: PRJ-CUST-2025-001
code = (self.customer or "GEN")[:4].upper()
year = frappe.utils.getdate(self.start_date or frappe.utils.today()).year
prefix = f"PRJ-{code}-{year}-"
self.name = getseries(prefix, 3)
別の方法 — before_naming:
def before_naming(self):
if self.is_priority:
self.naming_series = "PRIORITY-.#####"
else:
self.naming_series = "STD-.#####"
ワークフロー 6:提出可能なドキュメント
DRAFT (docstatus=0) → submit() → SUBMITTED (docstatus=1) → cancel() → CANCELLED (docstatus=2)
submit(): validate → before_submit → [DB: docstatus=1] → on_update → on_submit
cancel(): before_cancel → [DB: docstatus=2] → on_cancel
class PurchaseOrder(Document):
def validate(self):
self.validate_items()
self.calculate_totals()
def before_submit(self):
# ONLY submit-specific checks here
if self.total > 100000 and not self.manager_approval:
frappe.throw(_("Manager approval required for POs over 100,000"))
def on_submit(self):
self.update_ordered_qty()
self.create_purchase_receipt_draft()
def before_cancel(self):
if frappe.db.exists("Purchase Invoice",
{"purchase_order": self.name, "docstatus": 1}):
frappe.throw(_("Cancel linked invoices first"))
def on_cancel(self):
self.reverse_ordered_qty()
ルール:validate と before_submit の間で検証を重複させないでください。validate は必ず before_submit より先に実行されます。
ワークフロー 7:標準の ERPNext コントローラーをオーバーライドする
方法 A:全面的なオーバーライド(hooks.py)
# hooks.py
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
# myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # ALWAYS call parent first
self.custom_validation()
方法 B:イベントハンドラー(より安全で、クラスをオーバーライドしない)
# hooks.py
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.validate_sales_invoice",
}
}
# myapp/events.py
def validate_sales_invoice(doc, method=None):
if doc.grand_total < 0:
frappe.throw(_("Invalid total"))
方法 C:extend_doctype_class(v16+)
# hooks.py
extend_doctype_class = {
"Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}
# myapp/extends.py — Only methods to add/override
class SalesInvoiceExtend:
def custom_method(self):
pass
ルール:オーバーライドでは必ず super().validate() を呼び出してください。親メソッドを省略しないでください。標準の ERPNext ロジックは親メソッドに依存しています。
ワークフロー 8:ホワイトリスト登録されたメソッド(クライアントから呼び出し可能)
class Quotation(Document):
@frappe.whitelist()
def apply_discount(self, discount_percent):
if discount_percent < 0 or discount_percent > 100:
frappe.throw(_("Discount must be 0-100"))
self.discount_amount = self.total * (discount_percent / 100)
self.grand_total = self.total - self.discount_amount
self.save()
return {"grand_total": self.grand_total}
クライアント側の呼び出し:
frm.call('apply_discount', { discount_percent: 10 }).then(r => {
frm.reload_doc();
});
ワークフロー 9:フラグシステム
# Document-level flags (built-in)
doc.flags.ignore_permissions = True # Bypass permission checks
doc.flags.ignore_validate = True # Skip validate() hook
doc.flags.ignore_mandatory = True # Skip required field check
# Custom flags for inter-hook communication
def validate(self):
if self.is_urgent:
self.flags.needs_notification = True
def on_update(self):
if self.flags.get('needs_notification'):
self.notify_team()
ワークフロー 10:コントローラーをテストする
# tests/test_my_doctype.py
import frappe
from frappe.tests.utils import FrappeTestCase
class TestMyDocType(FrappeTestCase):
def test_validate_dates(self):
doc = frappe.get_doc({
"doctype": "My DocType",
"from_date": "2025-01-10",
"to_date": "2025-01-01" # Before from_date
})
self.assertRaises(frappe.ValidationError, doc.insert)
def test_calculate_totals(self):
doc = frappe.get_doc({
"doctype": "My DocType",
"items": [
{"item": "A", "qty": 2, "rate": 100},
{"item": "B", "qty": 3, "rate": 50}
]
})
doc.insert()
self.assertEqual(doc.total, 350)
実行:bench run-tests --module myapp.module.doctype.my_doctype.test_my_doctype
実行順序のリファレンス
INSERT
before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert →
on_update → on_change
SAVE(既存)
before_validate → validate → before_save → [DB UPDATE] →
on_update → on_change
SUBMIT
validate → before_submit → [DB: docstatus=1] →
on_update → on_submit → on_change
アンチパターンのクイックチェック
| してはいけないこと | 代わりに行うこと |
|---|---|
on_update 内の self.x = y | frappe.db.set_value(...) |
on_update 内の self.save() | 無限ループが発生します |
フック内の frappe.db.commit() | フレームワークに任せます |
| validate 内の重い処理 | on_update 内で frappe.enqueue() を使用します |
super().validate() を省略する | 必ず最初に親メソッドを呼び出します |
ループ内の frappe.get_doc() | frappe.get_cached_doc() を使用します |
| しきい値をハードコードする | Settings DocType を使用します |
完全な一覧については、references/anti-patterns.md を参照してください。
関連スキル
frappe-syntax-controllers— フックの正確なシグネチャと APIfrappe-errors-controllers— エラー処理のパターンfrappe-impl-serverscripts— Server Script で十分な場合frappe-syntax-hooks— hooks.py の設定frappe-core-database—frappe.db.*の操作
すべてのフックについては、references/decision-tree.md を参照してください。 拡張パターンについては、references/workflows.md を参照してください。 完全な動作例については、references/examples.md を参照してください。