frappe-errors-controllers について
Frappe 開発者向けのコントローラーエラーガイドです。ライフサイクル段階やエラーメッセージから原因と対処法を確認でき、フック、トランザクション、権限処理の注意点も扱います。
主な機能
- 01ライフサイクル段階別の診断
- 02エラー・原因・修正方法の一覧
- 03命名と検証のトラブルシューティング
- 04フックのオーバーライドと親クラス呼び出し
- 05トランザクションのロールバック時点
- 06権限クエリと再帰防止の指針
活用シーン
- 01コントローラーのライフサイクルエラーの診断
- 02命名や送信の失敗の修正
- 03フックの再帰とトランザクション問題の防止
- 04権限クエリとキャンセル時の処理の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-errors-controllersインストール後、Claude Code で/frappe-errors-controllersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdexamples.mdpatterns.md詳細
コントローラーのエラー — 診断と解決
相互参照:frappe-syntax-controllers(構文)、frappe-impl-controllers(ワークフロー)、frappe-errors-serverscripts(サーバースクリプト)。
ライフサイクルフェーズ別のエラー診断
CONTROLLER ERROR
│
├─► NAMING PHASE (autoname / before_naming)
│ ├─► NamingSeries not set → Add naming_series field or autoname property
│ ├─► DuplicateEntryError → Name collision, check uniqueness
│ └─► "name cannot be set directly" → Use autoname method, not self.name = x
│
├─► VALIDATION PHASE (before_validate / validate / before_save)
│ ├─► Infinite recursion → doc.save() called inside validate
│ ├─► Validation skipped → Missing super().validate() in override
│ └─► Wrong error timing → Use validate, not on_update, to block save
│
├─► SAVE PHASE (before_save / on_update / after_insert)
│ ├─► Changes lost in on_update → Use db_set(), not self.field = x
│ ├─► Infinite loop → self.save() in on_update triggers on_update again
│ └─► Transaction broken → frappe.db.commit() in controller (DON'T)
│
├─► SUBMIT PHASE (before_submit / on_submit)
│ ├─► "Not allowed to submit" → DocType missing is_submittable = 1
│ ├─► Partial state → Validation in on_submit (too late, already submitted)
│ └─► Stock/GL failures → Entries fail but docstatus already = 1
│
├─► CANCEL PHASE (before_cancel / on_cancel)
│ ├─► "Cannot cancel: linked docs" → Check and handle linked documents
│ └─► Partial cleanup → One reversal fails, rest skipped
│
└─► PERMISSION PHASE (has_permission / get_list)
├─► "Not permitted" → has_permission returns None (should be True/False)
├─► get_list returns nothing → permission_query_conditions SQL error
└─► SQL injection → User input in conditions without escape
エラーメッセージ → 原因 → 修正方法
| エラーメッセージ | 原因 | 修正方法 |
|---|---|---|
NamingSeries is not set | DocType で naming_series を使用していますが、フィールドがありません | DocType に naming_series フィールドを追加するか、コントローラーで autoname を設定します |
DuplicateEntryError | autoname が一意ではない名前を生成しました | カウンター付きの naming_series を使うか、ハッシュサフィックスを追加します |
Maximum recursion depth exceeded | validate/on_update で self.save() が呼び出されました | フック内で self.save() を呼び出さないでください。on_update では self.db_set() を使います |
Not allowed to submit | DocType に is_submittable = 1 が設定されていません | DocType の設定で「Is Submittable」を有効にします |
Cannot cancel: linked docs exist | リンクされた送信済みドキュメントによってキャンセルがブロックされています | 先にリンクされたドキュメントをキャンセルするか、before_cancel で確認します |
AttributeError: super() | オーバーライドしたフックで super() の呼び出しがありません | オーバーライドでは、必ず最初に super().method_name() を呼び出します |
Value missing for: field | コントローラーの validate で親の処理がスキップされました | super().validate() が呼び出されていることを確認します |
frappe.db.commit() breaks transactions | コントローラーのフック内で手動コミットしています | コントローラーで frappe.db.commit() を呼び出さないでください |
Changes lost in on_update | self.db_set() ではなく self.field = x を設定しています | 保存後のフックでは self.db_set("field", value) を使います |
NestedSet: root cannot be child | 親が自分自身に設定されているか、循環参照があります | validate で親が自身と異なることを検証し、lft/rgt を確認します |
extend_doctype_class conflict [v16+] | 複数のアプリが同じクラスを拡張し、メソッドが競合しています | MRO を考慮した設計にし、メソッド解決順序を確認します |
has_permission returns wrong result | 関数が True/False ではなく None を返しています | 必ず True または False を明示的に返します |
permission_query_conditions SQL error | WHERE 句のフラグメントの形式が正しくありません | 条件文字列を個別にテストし、frappe.db.escape() を使います |
重大なエラーパターン
1. Autoname の失敗
# ❌ WRONG — Setting name directly fails
class CustomDoc(Document):
def autoname(self):
self.name = f"DOC-{self.customer}" # May cause DuplicateEntryError
# ✅ CORRECT — Use naming utilities
class CustomDoc(Document):
def autoname(self):
# Option 1: Naming series
from frappe.model.naming import set_name_by_naming_series
set_name_by_naming_series(self)
# Option 2: Safe format with counter
self.name = frappe.model.naming.make_autoname(
f"DOC-.{self.customer}.-.####"
)
# Option 3: Hash for guaranteed uniqueness
# Set autoname = "hash" in DocType JSON instead
Autoname のオプション:naming_series、field:fieldname、format:PREFIX-{fieldname}-.####、hash、Prompt、またはカスタムの autoname() メソッド。
2. Validate のループ:フック内の self.save()
# ❌ WRONG — Infinite recursion
class SalesOrder(Document):
def validate(self):
self.calculate_totals()
self.save() # Triggers validate again → infinite loop!
def on_update(self):
self.status = "Updated"
self.save() # Triggers on_update again → infinite loop!
# ✅ CORRECT — Framework handles save; use db_set after save
class SalesOrder(Document):
def validate(self):
self.calculate_totals()
# No save() — framework saves after validate completes
def on_update(self):
self.db_set("status", "Updated") # Direct DB write, no trigger
3. is_submittable を設定せずに on_submit を使用
# ❌ ERROR — "Not allowed to submit"
class MyDoc(Document):
def on_submit(self):
self.create_entries()
# This fails if DocType JSON lacks: "is_submittable": 1
# ✅ FIX — Enable in DocType definition
# In my_doc.json:
# { "is_submittable": 1 }
# Then before_submit and on_submit hooks work
4. ライフサイクルフックの誤り:エラーの発生タイミング
# ❌ WRONG — Validation in on_submit (document already submitted!)
class SalesOrder(Document):
def on_submit(self):
if not self.has_stock():
frappe.throw(_("Insufficient stock")) # docstatus already = 1!
# ✅ CORRECT — ALWAYS validate in before_submit
class SalesOrder(Document):
def before_submit(self):
if not self.has_stock():
frappe.throw(_("Insufficient stock")) # Clean abort, stays Draft
def on_submit(self):
self.create_stock_entries() # Only post-submit actions here
フック別のトランザクションロールバック規則:
| フック | frappe.throw() の影響 |
|---|---|
validate / before_save | 完全にロールバック — ドキュメントは保存されません |
before_submit | 完全にロールバック — Draft のままです |
before_cancel | 完全にロールバック — Submitted のままです |
on_update / after_insert | ドキュメントは保存されます — エラーが表示されますが、ドキュメントは残ります |
on_submit | docstatus = 1 — エラーが表示されますが、すでに送信済みです |
on_cancel | docstatus = 2 — エラーが表示されますが、すでにキャンセル済みです |
5. オーバーライド内の super() の欠落
# ❌ WRONG — Parent validation completely skipped
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
# Parent validate() never runs! All ERPNext validations bypassed!
self.custom_check()
# ✅ CORRECT — ALWAYS call super() first
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate() # Run all parent validations first
self.custom_check() # Then add custom logic
6. extend_doctype_class [v16+]
# In hooks.py — v16+ preferred approach
extend_doctype_class = {
"Sales Order": ["myapp.overrides.sales_order.SalesOrderMixin"]
}
# myapp/overrides/sales_order.py
class SalesOrderMixin:
"""Mixin class — extends, does not replace."""
def validate(self):
super().validate() # ALWAYS call super — runs original + other mixins
self.custom_validation()
解決順序:class ExtendedSalesOrder(Mixin2, Mixin1, OriginalSalesOrder) — 最後に記載されたミックスインの優先度が最も高くなります。
7. 再帰ガード用のフラグ
# ❌ WRONG — on_update of linked doc triggers this doc's on_update
class SalesOrder(Document):
def on_update(self):
self.update_quotation() # Quotation.on_update triggers back here
# ✅ CORRECT — Use flags to prevent recursion
class SalesOrder(Document):
def on_update(self):
if self.flags.get("skip_linked_update"):
return
self.flags.skip_linked_update = True
self.update_quotation()
def update_quotation(self):
if self.quotation:
q = frappe.get_doc("Quotation", self.quotation)
q.flags.skip_linked_update = True # Prevent back-trigger
q.db_set("status", "Ordered")
8. get_list の権限エラー
# ❌ WRONG — permission_query_conditions returns None (fallback to no filter)
def get_permission_query(user):
pass # Returns None — shows ALL records!
# ❌ WRONG — SQL injection
def get_permission_query(user):
dept = frappe.db.get_value("User", user, "department")
return f"department = '{dept}'" # INJECTION RISK
# ✅ CORRECT — Explicit conditions with escape
def get_permission_query(user):
if "System Manager" in frappe.get_roles(user):
return "" # No filter — full access
dept = frappe.db.get_value("User", user, "department")
if dept:
return f"department = {frappe.db.escape(dept)}"
return "owner = {0}".format(frappe.db.escape(user))
注:permission_query_conditions の影響を受けるのは frappe.db.get_list() のみで、frappe.db.get_all() は対象外です。
9. NestedSet のエラー
# ❌ WRONG — Circular reference causes lft/rgt corruption
class Territory(NestedSet):
def validate(self):
# No parent validation!
pass
# ✅ CORRECT — Validate parent chain
class Territory(NestedSet):
def validate(self):
super().validate()
if self.parent_territory == self.name:
frappe.throw(_("Territory cannot be its own parent"))
# NestedSet.validate() checks circular refs automatically
# but explicit check gives better error message
on_cancel:クリーンアップ処理を分離する
# ❌ WRONG — First failure stops all cleanup
def on_cancel(self):
self.reverse_stock() # If this fails...
self.reverse_gl() # ...this never runs
self.update_linked() # ...neither does this
# ✅ CORRECT — Isolate each reversal
def on_cancel(self):
errors = []
for operation, label in [
(self.reverse_stock, "Stock reversal"),
(self.reverse_gl, "GL reversal"),
(self.update_linked, "Linked docs"),
]:
try:
operation()
except Exception as e:
errors.append(f"{label}: {str(e)}")
frappe.log_error(frappe.get_traceback(), f"{label} Error")
if errors:
frappe.msgprint(
_("Cancelled with errors:<br>{0}").format("<br>".join(errors)),
indicator="orange"
)
必ず行うこと/決して行わないこと
必ず守ること
- オーバーライドしたフックでは
super().method()を呼び出す — 親クラスのロジックを維持します - 検証は
on_submitではなくbefore_submitで行う — ここが最後に正常に処理を中断できるポイントです on_updateではself.db_set()を使う —self.field = xでの変更は失われます- 再帰ガードには
self.flagsを使う — フックの循環トリガーを防ぎます - クリーンアップ処理は
on_cancelで個別に分離する — 1つの失敗で全体を止めないようにします - 権限クエリでは
frappe.db.escape()を使う — SQL インジェクションを防ぎます has_permissionから明示的に True/False を返す — None の場合はデフォルトの動作にフォールバックします- 予期しない例外には
frappe.log_error()を使う — 決して黙って無視しないでください - ユーザー向けのエラーメッセージにはすべて
_()ラッパーを使う — 翻訳に対応できます
絶対にしないこと
validate/on_updateでself.save()を呼び出さない — 無限再帰が発生します- コントローラーで
frappe.db.commit()を呼び出さない — トランザクションはフレームワークが管理します on_submitに処理を中断する検証を入れない — ドキュメントはすでに送信済みです- オーバーライドしたメソッドで
super()を省略しない — 親クラスのロジックが壊れます has_permissionから None を返さない — 予測できない結果になります- 単独の
except: passで例外を握りつぶさない — 必ずエラーをログに記録してください extend_doctype_classで対応できる場合にoverride_doctype_classを使わない [v16+]validateに負荷の高い処理を入れない —on_updateからfrappe.enqueue()を使います
参照ファイル
| ファイル | 内容 |
|---|---|
references/examples.md | 実際のコントローラーエラーのシナリオと診断 |
references/anti-patterns.md | よくあるコントローラーの誤りと修正方法 |
references/patterns.md | ライフサイクルフック別の防御的なエラー処理パターン |
コメント
Markdown 対応