erpnext-errors-hooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Hooks - Error Handling
ERPNext Hooks - 错误处理
This skill covers error handling patterns for hooks.py configurations. For syntax, see . For implementation workflows, see .
erpnext-syntax-hookserpnext-impl-hooksVersion: v14/v15/v16 compatible
本技能涵盖hooks.py配置的错误处理模式。语法相关内容请参考。实现工作流相关内容请参考。
erpnext-syntax-hookserpnext-impl-hooks版本:兼容v14/v15/v16
Hooks Error Handling Overview
Hooks错误处理概述
┌─────────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ HOOKS具有独特的错误处理特性 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 拥有完整的Python能力(try/except、raise) │
│ ⚠️ 链式调用中的多个处理器 - 一个失败会影响其他处理器 │
│ ⚠️ 部分钩子无提示(调度器、permission_query) │
│ ⚠️ 事务行为因钩子类型而异 │
│ │
│ 与控制器的主要区别: │
│ • doc_events在控制器方法之后运行 │
│ • 多个应用可注册处理器(执行顺序很重要!) │
│ • 调度器无用户反馈 - 日志记录至关重要 │
│ • 权限钩子绝不能抛出错误 │
│ │
└─────────────────────────────────────────────────────────────────────┘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(validate、on_update、on_submit等) │
│ └─► 与控制器相同:frappe.throw()会在validate阶段触发完全回滚 │
│ └─► 多处理器链式调用:第一个错误会终止后续调用 │
│ └─► 将非关键操作隔离在try/except中 │
│ │
│ ► scheduler_events(daily、hourly、cron) │
│ └─► 无用户反馈 - frappe.log_error()必不可少 │
│ └─► 务必在操作外层使用try/except │
│ └─► 必须手动调用frappe.db.commit() │
│ │
│ ► permission_query_conditions │
│ └─► 绝不能抛出错误 - 出错时返回空字符串 │
│ └─► 无提示失败会破坏列表视图 │
│ └─► 记录错误但返回安全的回退值 │
│ │
│ ► has_permission │
│ └─► 绝不能抛出错误 - 出错时返回False │
│ └─► 返回None以 defer 到默认权限系统 │
│ │
│ ► override_doctype_class / extend_doctype_class │
│ └─► 务必在try/except中调用super() │
│ └─► 父类错误通常应向上传播 │
│ │
│ ► extend_bootinfo │
│ └─► 错误会完全中断页面加载! │
│ └─► 务必用try/except包裹并设置回退值 │
│ │
└─────────────────────────────────────────────────────────────────────────┘doc_events Error Handling
doc_events错误处理
Transaction Behavior (Same as Controllers)
事务行为(与控制器相同)
| Event | frappe.throw() Effect |
|---|---|
| ✅ Full rollback - document NOT saved |
| ✅ Full rollback - document NOT saved |
| ⚠️ Document IS saved, error shown |
| ⚠️ Document IS saved, error shown |
| ⚠️ docstatus=1, error shown |
| ⚠️ docstatus=2, error shown |
| 事件 | frappe.throw() 效果 |
|---|---|
| ✅ 完全回滚 - 文档不保存 |
| ✅ 完全回滚 - 文档不保存 |
| ⚠️ 文档已保存,显示错误 |
| ⚠️ 文档已保存,显示错误 |
| ⚠️ docstatus=1,显示错误 |
| ⚠️ docstatus=2,显示错误 |
Multiple Handler Chain
多处理器链式调用
python
undefinedpython
undefinedhooks.py - Multiple apps can register handlers
hooks.py - Multiple apps can register handlers
App A
App A
doc_events = {
"Sales Invoice": {
"validate": "app_a.events.validate_si" # Runs first
}
}
doc_events = {
"Sales Invoice": {
"validate": "app_a.events.validate_si" # Runs first
}
}
App B
App B
doc_events = {
"Sales Invoice": {
"validate": "app_b.events.validate_si" # Runs second
}
}
doc_events = {
"Sales Invoice": {
"validate": "app_b.events.validate_si" # Runs second
}
}
If App A throws error, App B's handler NEVER runs!
If App A throws error, App B's handler NEVER runs!
undefinedundefinedPattern: Validate Handler
模式:验证处理器
python
undefinedpython
undefinedmyapp/events/sales_invoice.py
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))undefinedimport 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))undefinedPattern: on_update Handler (Isolated Operations)
模式:on_update处理器(隔离操作)
python
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}"
)python
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
scheduler_events错误处理
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 │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ 调度器任务无用户交互 - 日志记录至关重要 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ • 无人会看到frappe.throw() - 任务只会无提示失败 │
│ • 失败时无自动邮件通知(除非配置) │
│ • frappe.log_error()是你唯一的调试工具 │
│ • 务必手动提交变更 │
│ │
└─────────────────────────────────────────────────────────────────────┘Pattern: Scheduler Task with Error Handling
模式:带错误处理的调度器任务
python
undefinedpython
undefinedmyapp/tasks.py
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"
)undefinedimport 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"
)undefinedPattern: Scheduler with Batch Commits
模式:带批量提交的调度器
python
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")python
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!
permission_query_conditions - 绝不能抛出错误!
python
undefinedpython
undefined❌ WRONG - Breaks list view entirely!
❌ WRONG - Breaks list view entirely!
def query_conditions(user):
if not user:
frappe.throw("User required") # DON'T DO THIS!
return f"owner = '{user}'"
def query_conditions(user):
if not user:
frappe.throw("User required") # DON'T DO THIS!
return f"owner = '{user}'"
✅ CORRECT - Return safe fallback
✅ 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)}"undefineddef 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)}"undefinedhas_permission - NEVER Throw!
has_permission - 绝不能抛出错误!
python
undefinedpython
undefined❌ WRONG - Breaks document access!
❌ 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!
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
✅ 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
---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
override_doctype_class
python
undefinedpython
undefinedmyapp/overrides.py
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"))undefinedfrom 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"))undefinedextend_doctype_class (V16+)
extend_doctype_class(V16+)
python
undefinedpython
undefinedmyapp/extends.py
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"))
---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
extend_bootinfo错误处理
Critical: Errors Break Page Load!
关键提示:错误会中断页面加载!
python
undefinedpython
undefined❌ WRONG - Unhandled error breaks desk entirely!
❌ 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
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
✅ 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 = {}
---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
- 在调度器任务中使用try/except - 否则无用户反馈
- 在调度器中调用frappe.db.commit() - 变更不会自动保存
- 在权限钩子中返回安全回退值 - 绝不能抛出错误
- 在覆盖类中调用super() - 保留父类行为
- 记录带上下文的错误 - 包含文档名称、操作
- 用try/except包裹extend_bootinfo - 错误会中断页面加载
❌ 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
- 不要在permission_query_conditions中抛出错误 - 会破坏列表视图
- 不要在has_permission中抛出错误 - 会破坏文档访问
- 不要假设只有单个处理器 - 多个应用可注册处理器
- 不要在doc_events中提交事务 - 框架会处理事务
- 不要忽略调度器错误 - 它们会无提示失败
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 |
| 钩子类型 | 可抛出错误? | 需要提交? | 核心模式 |
|---|---|---|---|
| doc_events (validate) | ✅ 是 | ❌ 否 | 收集错误,一次性抛出 |
| doc_events (on_update) | ⚠️ 谨慎使用 | ❌ 否 | 隔离非关键操作 |
| scheduler_events | ❌ 无意义 | ✅ 是 | Try/except + log_error |
| permission_query_conditions | ❌ 绝不能 | ❌ 否 | 出错时返回空字符串 |
| has_permission | ❌ 绝不能 | ❌ 否 | 出错时返回None |
| extend_bootinfo | ❌ 绝不能 | ❌ 否 | Try/except + 回退值 |
| override class | ✅ 是 | ❌ 否 | super() + 重新抛出 |
Reference Files
参考文件
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整错误处理模式 |
| 完整可用示例 |
| 需避免的常见错误 |
See Also
另请参阅
- - Hooks syntax
erpnext-syntax-hooks - - Implementation workflows
erpnext-impl-hooks - - Controller error handling
erpnext-errors-controllers - - Server Script error handling
erpnext-errors-serverscripts
- - Hooks语法
erpnext-syntax-hooks - - 实现工作流
erpnext-impl-hooks - - 控制器错误处理
erpnext-errors-controllers - - 服务器脚本错误处理
erpnext-errors-serverscripts