mcpField
F
frappe-errors-controllers

frappe-errors-controllers

github.com/impertio-studio開発198115 回インストール30 週間前に更新
リポジトリ

frappe-errors-controllers について

Frappe 開発者向けのコントローラーエラーガイドです。ライフサイクル段階やエラーメッセージから原因と対処法を確認でき、フック、トランザクション、権限処理の注意点も扱います。

主な機能

  1. 01ライフサイクル段階別の診断
  2. 02エラー・原因・修正方法の一覧
  3. 03命名と検証のトラブルシューティング
  4. 04フックのオーバーライドと親クラス呼び出し
  5. 05トランザクションのロールバック時点
  6. 06権限クエリと再帰防止の指針

活用シーン

  1. 01コントローラーのライフサイクルエラーの診断
  2. 02命名や送信の失敗の修正
  3. 03フックの再帰とトランザクション問題の防止
  4. 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.md
examples.md
patterns.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 setDocType で naming_series を使用していますが、フィールドがありませんDocType に naming_series フィールドを追加するか、コントローラーで autoname を設定します
DuplicateEntryErrorautoname が一意ではない名前を生成しましたカウンター付きの naming_series を使うか、ハッシュサフィックスを追加します
Maximum recursion depth exceededvalidate/on_update で self.save() が呼び出されましたフック内で self.save() を呼び出さないでください。on_update では self.db_set() を使います
Not allowed to submitDocType に 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_updateself.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 errorWHERE 句のフラグメントの形式が正しくありません条件文字列を個別にテストし、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_submitdocstatus = 1 — エラーが表示されますが、すでに送信済みです
on_canceldocstatus = 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"
        )

必ず行うこと/決して行わないこと

必ず守ること
  1. オーバーライドしたフックでは super().method() を呼び出す — 親クラスのロジックを維持します
  2. 検証は on_submit ではなく before_submit で行う — ここが最後に正常に処理を中断できるポイントです
  3. on_update では self.db_set() を使う — self.field = x での変更は失われます
  4. 再帰ガードには self.flags を使う — フックの循環トリガーを防ぎます
  5. クリーンアップ処理は on_cancel で個別に分離する — 1つの失敗で全体を止めないようにします
  6. 権限クエリでは frappe.db.escape() を使う — SQL インジェクションを防ぎます
  7. has_permission から明示的に True/False を返す — None の場合はデフォルトの動作にフォールバックします
  8. 予期しない例外には frappe.log_error() を使う — 決して黙って無視しないでください
  9. ユーザー向けのエラーメッセージにはすべて _() ラッパーを使う — 翻訳に対応できます
絶対にしないこと
  1. validate/on_update で self.save() を呼び出さない — 無限再帰が発生します
  2. コントローラーで frappe.db.commit() を呼び出さない — トランザクションはフレームワークが管理します
  3. on_submit に処理を中断する検証を入れない — ドキュメントはすでに送信済みです
  4. オーバーライドしたメソッドで super() を省略しない — 親クラスのロジックが壊れます
  5. has_permission から None を返さない — 予測できない結果になります
  6. 単独の except: pass で例外を握りつぶさない — 必ずエラーをログに記録してください
  7. extend_doctype_class で対応できる場合に override_doctype_class を使わない [v16+]
  8. validate に負荷の高い処理を入れない — on_update から frappe.enqueue() を使います

参照ファイル

ファイル内容
references/examples.md実際のコントローラーエラーのシナリオと診断
references/anti-patterns.mdよくあるコントローラーの誤りと修正方法
references/patterns.mdライフサイクルフック別の防御的なエラー処理パターン

コメント