mcpField
F
frappe-errors-hooks

frappe-errors-hooks

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

frappe-errors-hooks について

Frappe のフックを扱う開発者向けの、エラー診断と解決方法のリファレンスです。イベント、権限、スケジューラー、Fixture、クラス拡張などの症状と原因、対処法をまとめています。

主な機能

  1. 01症状から原因と対処法を確認
  2. 02フックのパスと登録を診断
  3. 03スケジューラーと Fixture を調査
  4. 04権限・イベントフックのエラーに対応
  5. 05オーバーライドと拡張の競合を確認

活用シーン

  1. 01フックが実行されない原因の特定
  2. 02スケジューラータスクの停止原因の調査
  3. 03権限フックによるリストエラーの修正
  4. 04Fixture のインストールやインポート失敗の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-errors-hooks

インストール後、Claude Code で/frappe-errors-hooksを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
anti-patterns.md
examples.md
patterns.md

詳細

Frappe Hooks のエラー診断と解決

相互参照:frappe-syntax-hooks(構文)、frappe-impl-hooks(ワークフロー)、frappe-errors-controllers(コントローラーのエラー)。


エラーから修正方法への対応表

エラー/症状原因修正方法
Hook がまったく実行されないドット区切りパスのタイプミスモジュールパスが実際のファイルの場所と一致していることを確認します
bench の起動時に ImportError が発生するモジュールパスが間違っているか、循環インポートが発生しているインポートパスを修正し、循環依存を解消します
AttributeError: module has no attributehooks.py の関数名のタイプミス関数名を Python の定義と完全に一致させます
app_include_js が読み込まれないパスに assets/ のプレフィックスがないか、拡張子が間違っている"assets/myapp/js/file.js" の形式を使用します
scheduler_events が実行されないスケジューラーが無効になっているか、ワーカーが停止しているbench scheduler enable を実行し、bench doctor を確認します
doc_events ハンドラーが呼び出されないdict のキーに指定した DocType 名の誤記スペースを含む正確な DocType 名を使用します:"Sales Invoice"
permission_query_conditions によってリストビューが壊れるSQL 構文エラー、またはハンドラー内の frappe.throw()有効な SQL 文字列を返します。絶対に throw しないでください
override_doctype_class のインポートに失敗するバージョン間で親クラスのインポートパスが変更された対象バージョンに正しいモジュールパスを指定します
extend_doctype_class [v16+] でメソッドが競合する2 つの拡張機能が同じメソッド名を定義している競合するメソッド名を変更し、hook の解決順序を確認します
インストール時に Fixtures が読み込まれないdt キーが間違っているか、対象に DocType が存在しないエクスポート前に DocType の存在を確認し、フィルター構文を確認します
extend_bootinfo によってログインできなくなるboot ハンドラーで未処理の例外が発生しているbootinfo のコード全体を try/except で囲みます
ワイルドカード "*" ハンドラーによってすべての保存が失敗するワイルドカード doc_events で未処理の例外が発生しているワイルドカードハンドラーは必ず try/except で囲みます
hook は実行されるが変更が失われるscheduler に frappe.db.commit() がないscheduler/バックグラウンドタスクに明示的な commit を追加します
複数ハンドラーのチェーンが壊れる最初のハンドラーで例外が発生し、ほかのハンドラーが実行されない重要度の低い処理を try/except で分離します

Hook 登録エラー

Hook が実行されない場合:診断チェックリスト
IS YOUR HOOK NOT FIRING?
│
├─► Check 1: Is the dotted path correct?
│   hooks.py: "myapp.events.sales.validate"
│   File:     myapp/events/sales.py → def validate(doc, method=None):
│   COMMON MISTAKE: "myapp.events.sales_invoice.validate" when file is sales.py
│
├─► Check 2: Is the dict structure correct?
│   doc_events uses NESTED dict: {"Sales Invoice": {"validate": "path"}}
│   scheduler_events uses LIST: {"daily": ["path1", "path2"]}
│   permission_query uses FLAT dict: {"Sales Invoice": "path"}
│
├─► Check 3: Is bench restarted after hooks.py change?
│   ALWAYS run: bench restart (or bench clear-cache for dev)
│
├─► Check 4: Is the DocType name exact?
│   "Sales Invoice" NOT "SalesInvoice" NOT "sales_invoice"
│   Use exact DocType name as shown in Frappe UI
│
└─► Check 5: Is the app installed on the site?
    bench --site mysite list-apps
循環インポートエラー
# ❌ CAUSES ImportError — circular dependency
# myapp/hooks.py imports from myapp.events
# myapp/events/sales.py imports from myapp.hooks

# ✅ CORRECT — break the cycle
# Move shared constants to myapp/constants.py
# Import from constants in both hooks.py and events/

ルール:イベントハンドラーから hooks.py をインポートしてはいけません。hooks.py はフレームワークによって読み込まれるものであり、コードからインポートするものではありません。

Hook の種類ごとの誤った Dict 構造
# ❌ WRONG — doc_events needs nested dict, not flat
doc_events = {
    "Sales Invoice": "myapp.events.validate"  # WRONG: string, not dict
}

# ✅ CORRECT
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales.validate"
    }
}

# ❌ WRONG — scheduler_events daily needs list
scheduler_events = {
    "daily": "myapp.tasks.daily_sync"  # WRONG: string, not list
}

# ✅ CORRECT
scheduler_events = {
    "daily": ["myapp.tasks.daily_sync"]
}

# ❌ WRONG — cron needs nested dict with list values
scheduler_events = {
    "cron": ["0 9 * * *", "myapp.tasks.morning"]  # WRONG structure
}

# ✅ CORRECT
scheduler_events = {
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.morning_report"]
    }
}

app_include_js/app_include_css のエラー

# ❌ WRONG — missing assets/ prefix
app_include_js = "js/myapp.js"

# ❌ WRONG — using Python module path instead of file path
app_include_js = "myapp.public.js.myapp"

# ✅ CORRECT — full asset path
app_include_js = "assets/myapp/js/myapp.js"

# ✅ CORRECT — multiple files as list
app_include_js = ["assets/myapp/js/app.js", "assets/myapp/js/utils.js"]
app_include_css = "assets/myapp/css/myapp.css"

診断:JS/CSS が読み込まれない場合は、ブラウザーの DevTools の Network タブで 404 が発生していないか確認します。新しいファイルを追加した後は bench build を実行します。ファイルが myapp/public/js/myapp.js に存在することを必ず確認します。


scheduler_events が実行されない

診断手順
# Step 1: Is scheduler enabled?
bench scheduler status
# If disabled: bench scheduler enable

# Step 2: Are workers running?
bench doctor
# Look for: "Workers online: X"
# If 0: bench start (dev) or supervisorctl restart all (prod)

# Step 3: Check Scheduled Job Log
# In Frappe UI: /api/method/frappe.client.get_list?doctype=Scheduled Job Log&limit=5

# Step 4: Check Error Log for task failures
# In Frappe UI: /app/error-log

# Step 5: Is the task registered?
bench execute frappe.utils.scheduler.get_all_tasks
よくある Scheduler の失敗
# ❌ PROBLEM: Task runs but changes not persisted
def daily_sync():
    for item in frappe.get_all("Item", limit=100):
        frappe.db.set_value("Item", item.name, "synced", 1)
    # MISSING: frappe.db.commit() — ALL changes lost!

# ✅ FIX: ALWAYS commit in scheduler tasks
def daily_sync():
    for item in frappe.get_all("Item", limit=100):
        frappe.db.set_value("Item", item.name, "synced", 1)
    frappe.db.commit()

# ❌ PROBLEM: Task fails silently — no debugging possible
def daily_task():
    try:
        process_records()
    except Exception:
        pass  # Silent death

# ✅ FIX: ALWAYS log errors in scheduler
def daily_task():
    try:
        process_records()
        frappe.db.commit()
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Daily Task Error")

doc_events のエラー

イベントフェーズごとのエラー処理
イベントThrow による影響トランザクションパターン
validate保存を防止し、完全にロールバックする書き込み前エラーを収集して、一度だけ throw します
before_save保存を防止し、完全にロールバックする書き込み前validate と同様
on_updateDoc はすでに保存済みで、エラーが表示される書き込み後重要度の低い処理を分離します
after_insertDoc はすでに保存済みで、エラーが表示される書き込み後重要度の低い処理を分離します
on_submitDoc はすでに送信済み書き込み後重要度の低い処理を分離します
on_cancelDoc はすでにキャンセル済み書き込み後重要度の低い処理を分離します
複数ハンドラーのチェーンに関する問題
# If App A and App B both register validate for Sales Invoice:
# App A's handler throws → App B's handler NEVER runs

# ✅ ALWAYS be aware: your handler is not alone
def validate(doc, method=None):
    """Collect errors, throw once at end."""
    errors = []
    if doc.grand_total < 0:
        errors.append(_("Total cannot be negative"))
    if errors:
        frappe.throw("<br>".join(errors))

# ✅ For on_update: isolate independent operations
def on_update(doc, method=None):
    try:
        send_notification(doc)
    except Exception:
        frappe.log_error(frappe.get_traceback(), f"Notify error: {doc.name}")
    try:
        sync_external(doc)
    except Exception:
        frappe.log_error(frappe.get_traceback(), f"Sync error: {doc.name}")
doc_events 内で commit してはいけません
# ❌ BREAKS transaction management
def on_update(doc, method=None):
    frappe.db.set_value("Counter", "main", "count", 100)
    frappe.db.commit()  # Partial commit — dangerous!

# ✅ Framework handles commits automatically
def on_update(doc, method=None):
    frappe.db.set_value("Counter", "main", "count", 100)

Permission Hook のエラー

permission_query_conditions:絶対に Throw しないでください
# ❌ BREAKS list view entirely
def query_conditions(user):
    if "Sales User" not in frappe.get_roles(user):
        frappe.throw("Access denied")  # LIST VIEW CRASHES
    return f"owner = '{user}'"  # Also: SQL injection!

# ✅ CORRECT — safe fallback, escaped values
def query_conditions(user):
    try:
        user = user or frappe.session.user
        if "System Manager" in frappe.get_roles(user):
            return ""
        return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Query Conditions Error")
        return f"`tabSales Invoice`.owner = {frappe.db.escape(frappe.session.user)}"

注:permission_query_conditions は frappe.db.get_list() にのみ影響し、frappe.db.get_all() には影響しません。

has_permission:絶対に Throw しないでください
# ❌ BREAKS document access
def has_permission(doc, user=None, permission_type=None):
    if doc.status == "Locked":
        frappe.throw("Locked")  # DOCUMENT INACCESSIBLE

# ✅ Return False to deny, None to defer
def has_permission(doc, user=None, permission_type=None):
    try:
        user = user or frappe.session.user
        if doc.status == "Locked" and permission_type == "write":
            return False
        return None  # Defer to default permission system
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Permission Error")
        return None

Override と Extend のエラー

override_doctype_class:インポートの失敗
# ❌ COMMON: Import path changes between ERPNext versions
# v14 path:
override_doctype_class = {
    "Sales Invoice": "myapp.overrides.CustomSI"
}
# myapp/overrides.py:
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
# This path may change in v15/v16!

# ✅ ALWAYS call super(), re-raise validation errors
class CustomSalesInvoice(SalesInvoice):
    def validate(self):
        try:
            super().validate()
        except frappe.ValidationError:
            raise  # ALWAYS re-raise validation errors
        except Exception:
            frappe.log_error(frappe.get_traceback(), "Parent validate error")
            raise
        self.custom_validation()

警告:DocType ごとに有効になる override_doctype_class は 1 つのアプリのものだけです(「後勝ち」)。複数アプリとの互換性には extend_doctype_class [v16+] を使用してください。

extend_doctype_class [v16+]:競合
# hooks.py
extend_doctype_class = {
    "Sales Invoice": ["myapp.extensions.si.SalesInvoiceMixin"]
}

# ❌ CONFLICT: Two extensions define same method
# App A: class Mixin: def custom_calc(self): ...
# App B: class Mixin: def custom_calc(self): ...
# Result: Last app's method wins silently

# ✅ ALWAYS prefix method names with app name
class SalesInvoiceMixin:
    def myapp_custom_calc(self):
        """Prefixed to avoid conflicts with other extensions."""
        pass

extend_bootinfo のエラー

# ❌ BREAKS LOGIN — unhandled error prevents desk from loading
def extend_boot(bootinfo):
    settings = frappe.get_single("My Settings")  # DoesNotExistError!
    bootinfo.config = settings.config

# ✅ ALWAYS wrap in try/except with safe defaults
def extend_boot(bootinfo):
    bootinfo.myapp_config = {}
    try:
        if frappe.db.exists("My Settings", "My Settings"):
            settings = frappe.get_single("My Settings")
            bootinfo.myapp_config = {"feature": settings.feature or False}
    except Exception:
        frappe.log_error(frappe.get_traceback(), "Bootinfo Error")

フィクスチャが読み込まれない場合

# ❌ WRONG — dt key misspelled
fixtures = [{"doctype": "Custom Field", "filters": [...]}]  # "doctype" not "dt"!

# ✅ CORRECT — use "dt" key
fixtures = [{"dt": "Custom Field", "filters": [["module", "=", "My App"]]}]

# ❌ PROBLEM: DocType doesn't exist on target site
fixtures = [{"dt": "My Custom DocType"}]  # If not created yet → install fails

# ✅ FIX: Ensure DocType is created before fixtures are imported
# Order: DocType JSON → fixtures JSON (install order matters)

エクスポートコマンド:bench --site mysite export-fixtures インポート:bench --site mysite install-app myapp の実行中に自動で行われます


重要なルール

必ず行うこと
  1. hooks.py を変更した後は、bench を再起動します
  2. スケジューラータスクでは try/except を使います。エラーはユーザーに表示されません
  3. スケジューラーでは frappe.db.commit() を呼び出します。自動コミットは行われません
  4. 権限フックでは安全なフォールバックを返します。絶対に例外を発生させないでください
  5. オーバーライドクラスでは super() を呼び出し、ValidationError を再送出します
  6. extend_bootinfo を try/except で囲みます。エラーが発生するとログインできなくなります
  7. ワイルドカード "*" の doc_events を try/except で囲みます。エラーが発生するとすべての保存ができなくなります
  8. extend_doctype_class [v16+] のメソッドにはアプリ名をプレフィックスとして付けます
絶対に行わないこと
  1. permission_query_conditions 内で例外を発生させないでください。リストビューが使えなくなります
  2. has_permission 内で例外を発生させないでください。ドキュメントにアクセスできなくなります
  3. doc_events 内でコミットしないでください。トランザクション管理が壊れます
  4. イベントハンドラーから hooks.py をインポートしないでください。循環インポートが発生します
  5. ハンドラーが 1 つだけだと想定しないでください。複数のアプリが doc_events を登録できます
  6. 権限 SQL で文字列フォーマットを使わないでください。SQL インジェクションのリスクがあります
  7. スケジューラーのエラーを無視しないでください。エラーは完全に通知されないまま処理に失敗します

クイックリファレンス:フックの種類別エラー処理

フックの種類例外を発生させてもよいか?コミットするか?エラー処理の方針
doc_events(validate)はい絶対にしないエラーを収集し、最後に一度だけ例外を発生させます
doc_events(on_update 以降)注意が必要絶対にしない重要でない処理を分離します
scheduler_events無意味必ず行うtry/except + log_error
permission_query_conditions絶対にしない絶対にしない"" または所有者フィルターを返します
has_permission絶対にしない絶対にしないエラー時は None を返します
extend_bootinfo絶対にしない絶対にしないtry/except + 安全なデフォルト値
override_doctype_classはい絶対にしないsuper() を呼び出し、例外を再送出します
extend_doctype_class [v16+]はい絶対にしないメソッドにプレフィックスを付け、競合を避けます
fixtures該当なし該当なしdt キーと DocType の存在を確認します
app_include_js/css該当なし該当なしassets/ プレフィックスを確認し、bench build を実行します

参考ファイル

ファイル内容
references/patterns.mdフックの種類別のエラー処理パターン
references/examples.mdエラー処理を含む、完全に動作する例
references/anti-patterns.mdよくある間違いと、誤った例・正しい例の組み合わせ

関連項目

  • frappe-syntax-hooks — フックの構文と dict 構造
  • frappe-impl-hooks — 実装ワークフロー
  • frappe-errors-controllers — コントローラーのエラー処理
  • frappe-errors-database — データベースのエラー処理
  • frappe-errors-serverscripts — Server Script のエラー処理

コメント