← スキル一覧に戻る

erpnext-errors-hooks
by OpenAEC-Foundation
28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.
⭐ 1🍴 1📅 2026年1月23日
SKILL.md
name: erpnext-errors-hooks description: "Error handling patterns for ERPNext hooks.py configurations. Use when debugging doc_events errors, scheduler failures, boot session issues, and app initialization problems. V14/V15/V16 compatible. Triggers: hooks.py error, doc_events error, scheduler error, boot session error, app initialization error."
ERPNext Hooks - Error Handling
This skill covers error handling patterns for hooks.py configurations. For syntax, see erpnext-syntax-hooks. For implementation workflows, see erpnext-impl-hooks.
Version: v14/v15/v16 compatible
Hooks Error Handling Overview
┌─────────────────────────────────────────────────────────────────────┐
│ HOOKS HAVE UNIQUE ERROR HANDLING CHARACTERISTICS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ Full Python power (try/except, raise) │
│ ⚠️ Multiple handlers in chain - one failure affects others │
│ ⚠️ Some hooks are silent (scheduler, permission_query) │
│ ⚠️ Transaction behavior varies by hook type │
│ │
│ Key differences from controllers: │
│ • doc_events runs AFTER controller methods │
│ • Multiple apps can register handlers (order matters!) │
│ • Scheduler has NO user feedback - logging is critical │
│ • Permission hooks should NEVER throw errors │
│ │
└─────────────────────────────────────────────────────────────────────┘
Main Decision: Error Handling by Hook Type
┌─────────────────────────────────────────────────────────────────────────┐
│ WHICH HOOK TYPE ARE YOU USING? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► doc_events (validate, on_update, on_submit, etc.) │
│ └─► Same as controllers: frappe.throw() rolls back in validate │
│ └─► Multiple handlers: first error stops chain │
│ └─► Isolate non-critical operations in try/except │
│ │
│ ► scheduler_events (daily, hourly, cron) │
│ └─► NO user feedback - frappe.log_error() is essential │
│ └─► ALWAYS use try/except around operations │
│ └─► MUST call frappe.db.commit() manually │
│ │
│ ► permission_query_conditions │
│ └─► NEVER throw errors - return empty string on error │
│ └─► Silent failures break list views │
│ └─► Log errors but return safe fallback │
│ │
│ ► has_permission │
│ └─► NEVER throw errors - return False on error │
│ └─► Return None to defer to default permission │
│ │
│ ► override_doctype_class / extend_doctype_class │
│ └─► ALWAYS call super() in try/except │
│ └─► Parent errors should usually propagate │
│ │
│ ► extend_bootinfo │
│ └─► Errors break page load entirely! │
│ └─► ALWAYS wrap in try/except with fallback │
│ │
└─────────────────────────────────────────────────────────────────────────┘
doc_events Error Handling
Transaction Behavior (Same as Controllers)
| Event | frappe.throw() Effect |
|---|---|
validate | ✅ Full rollback - document NOT saved |
before_save | ✅ Full rollback - document NOT saved |
on_update | ⚠️ Document IS saved, error shown |
after_insert | ⚠️ Document IS saved, error shown |
on_submit | ⚠️ docstatus=1, error shown |
on_cancel | ⚠️ docstatus=2, error shown |
Multiple Handler Chain
# hooks.py - Multiple apps can register handlers
# App A
doc_events = {
"Sales Invoice": {
"validate": "app_a.events.validate_si" # Runs first
}
}
# App B
doc_events = {
"Sales Invoice": {
"validate": "app_b.events.validate_si" # Runs second
}
}
# If App A throws error, App B's handler NEVER runs!
Pattern: Validate Handler
# myapp/events/sales_invoice.py
import frappe
from frappe import _
def validate(doc, method=None):
"""Validate handler with proper error handling."""
errors = []
# Collect validation errors
if doc.grand_total < 0:
errors.append(_("Total cannot be negative"))
if doc.custom_field and not doc.customer:
errors.append(_("Customer required when custom field is set"))
# Throw all at once
if errors:
frappe.throw("<br>".join(errors))
Pattern: on_update Handler (Isolated Operations)
def on_update(doc, method=None):
"""Post-save handler with isolated operations."""
# Critical operation - let errors propagate
update_linked_records(doc)
# Non-critical operations - isolate errors
try:
send_notification(doc)
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Notification failed for {doc.name}"
)
try:
sync_to_external(doc)
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"External sync failed for {doc.name}"
)
scheduler_events Error Handling
Critical: No User Feedback!
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ SCHEDULER TASKS HAVE NO USER - LOGGING IS ESSENTIAL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ • No one sees frappe.throw() - task just fails silently │
│ • No automatic email on failure (unless configured) │
│ • frappe.log_error() is your ONLY debugging tool │
│ • Always commit changes manually │
│ │
└─────────────────────────────────────────────────────────────────────┘
Pattern: Scheduler Task with Error Handling
# myapp/tasks.py
import frappe
def daily_sync():
"""Daily sync task with comprehensive error handling."""
results = {
"processed": 0,
"errors": []
}
try:
# Get records to process (ALWAYS with limit!)
records = frappe.get_all(
"Sales Invoice",
filters={"sync_status": "Pending"},
limit=500
)
for record in records:
try:
process_record(record.name)
results["processed"] += 1
except Exception as e:
results["errors"].append(f"{record.name}: {str(e)}")
frappe.log_error(
frappe.get_traceback(),
f"Sync error: {record.name}"
)
# REQUIRED: Commit changes
frappe.db.commit()
except Exception as e:
# Log fatal errors
frappe.log_error(
frappe.get_traceback(),
"Daily Sync Fatal Error"
)
return
# Log summary
if results["errors"]:
summary = f"Processed: {results['processed']}, Errors: {len(results['errors'])}"
frappe.log_error(
summary + "\n\n" + "\n".join(results["errors"][:50]),
"Daily Sync Summary"
)
Pattern: Scheduler with Batch Commits
def process_large_dataset():
"""Process large dataset with periodic commits."""
BATCH_SIZE = 100
try:
records = frappe.get_all("Item", limit=5000)
total = len(records)
for i in range(0, total, BATCH_SIZE):
batch = records[i:i + BATCH_SIZE]
for record in batch:
try:
update_item(record.name)
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Item update error: {record.name}"
)
# Commit after each batch
frappe.db.commit()
except Exception:
frappe.log_error(frappe.get_traceback(), "Batch Processing Error")
Permission Hooks Error Handling
permission_query_conditions - NEVER Throw!
# ❌ WRONG - Breaks list view entirely!
def query_conditions(user):
if not user:
frappe.throw("User required") # DON'T DO THIS!
return f"owner = '{user}'"
# ✅ CORRECT - Return safe fallback
def query_conditions(user):
"""Permission query with error handling."""
try:
if not user:
user = frappe.session.user
if "System Manager" in frappe.get_roles(user):
return "" # No restrictions
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
except Exception:
frappe.log_error(
frappe.get_traceback(),
"Permission Query Error"
)
# Safe fallback - restrict to own records
return f"`tabSales Invoice`.owner = {frappe.db.escape(frappe.session.user)}"
has_permission - NEVER Throw!
# ❌ WRONG - Breaks document access!
def has_permission(doc, user=None, permission_type=None):
if doc.status == "Locked":
frappe.throw("Document is locked") # DON'T DO THIS!
# ✅ CORRECT - Return boolean or None
def has_permission(doc, user=None, permission_type=None):
"""Document permission check with error handling."""
try:
user = user or frappe.session.user
# Deny access to locked documents
if doc.status == "Locked" and permission_type == "write":
return False
# Custom logic
if permission_type == "delete":
if doc.has_linked_records():
return False
# Return None to defer to default permission system
return None
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Permission check error: {doc.name}"
)
# Safe fallback - defer to default
return None
Override Hooks Error Handling
override_doctype_class
# myapp/overrides.py
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
import frappe
from frappe import _
class CustomSalesOrder(SalesOrder):
def validate(self):
"""Override with proper error handling."""
# ALWAYS call parent first in try/except
try:
super().validate()
except frappe.ValidationError:
# Re-raise validation errors
raise
except Exception as e:
frappe.log_error(frappe.get_traceback(), "Parent Validate Error")
raise
# Custom validation
self.custom_validate()
def custom_validate(self):
if self.custom_approval_required and not self.custom_approved:
frappe.throw(_("Approval required before saving"))
extend_doctype_class (V16+)
# myapp/extends.py
import frappe
from frappe import _
class SalesOrderExtend:
"""Extension class - only add new methods."""
def custom_approval_check(self):
"""New method with error handling."""
try:
if not self.custom_approver:
frappe.throw(_("Approver not set"))
approver = frappe.get_doc("User", self.custom_approver)
if not approver.enabled:
frappe.throw(_("Approver is disabled"))
except frappe.DoesNotExistError:
frappe.throw(_("Approver not found"))
extend_bootinfo Error Handling
Critical: Errors Break Page Load!
# ❌ WRONG - Unhandled error breaks desk entirely!
def extend_boot(bootinfo):
settings = frappe.get_single("My Settings") # What if it doesn't exist?
bootinfo.my_config = settings.config
# ✅ CORRECT - Always handle errors
def extend_boot(bootinfo):
"""Extend bootinfo with error handling."""
try:
if frappe.db.exists("My Settings", "My Settings"):
settings = frappe.get_single("My Settings")
bootinfo.my_config = settings.config or {}
else:
bootinfo.my_config = {}
except Exception:
frappe.log_error(
frappe.get_traceback(),
"Bootinfo Extension Error"
)
# Safe fallback
bootinfo.my_config = {}
Critical Rules
✅ ALWAYS
- Use try/except in scheduler tasks - No user feedback otherwise
- Call frappe.db.commit() in scheduler - Changes aren't auto-saved
- Return safe fallbacks in permission hooks - Never throw
- Call super() in override classes - Preserve parent behavior
- Log errors with context - Include document name, operation
- Wrap extend_bootinfo in try/except - Errors break page load
❌ NEVER
- Don't throw in permission_query_conditions - Breaks list views
- Don't throw in has_permission - Breaks document access
- Don't assume single handler - Multiple apps can register
- Don't commit in doc_events - Framework handles transactions
- Don't ignore scheduler errors - They fail silently
Quick Reference: Error Handling by Hook
| Hook Type | Can Throw? | Commit? | Key Pattern |
|---|---|---|---|
| doc_events (validate) | ✅ YES | ❌ NO | Collect errors, throw once |
| doc_events (on_update) | ⚠️ Careful | ❌ NO | Isolate non-critical ops |
| scheduler_events | ❌ Pointless | ✅ YES | Try/except + log_error |
| permission_query_conditions | ❌ NEVER | ❌ NO | Return "" on error |
| has_permission | ❌ NEVER | ❌ NO | Return None on error |
| extend_bootinfo | ❌ NEVER | ❌ NO | Try/except + fallback |
| override class | ✅ YES | ❌ NO | super() + re-raise |
Reference Files
| File | Contents |
|---|---|
references/patterns.md | Complete error handling patterns |
references/examples.md | Full working examples |
references/anti-patterns.md | Common mistakes to avoid |
See Also
erpnext-syntax-hooks- Hooks syntaxerpnext-impl-hooks- Implementation workflowserpnext-errors-controllers- Controller error handlingerpnext-errors-serverscripts- Server Script error handling
スコア
総合スコア
75/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
レビュー
💬
レビュー機能は近日公開予定です

