erpnext-errors-controllers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Controllers - Error Handling
ERPNext Controllers - 错误处理
This skill covers error handling patterns for Document Controllers. For syntax, see . For implementation workflows, see .
erpnext-syntax-controllerserpnext-impl-controllersVersion: v14/v15/v16 compatible
本技能介绍Document Controllers的错误处理模式。语法相关内容请参考,实现工作流请参考。
erpnext-syntax-controllerserpnext-impl-controllers版本:兼容v14/v15/v16
Controllers vs Server Scripts: Error Handling
控制器与服务器脚本:错误处理
┌─────────────────────────────────────────────────────────────────────┐
│ CONTROLLERS HAVE FULL PYTHON POWER │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ try/except blocks - Full exception handling │
│ ✅ raise statements - Custom exceptions │
│ ✅ Multiple except clauses - Handle specific errors │
│ ✅ finally blocks - Cleanup operations │
│ ✅ frappe.throw() - Stop with user message │
│ ✅ frappe.log_error() - Silent error logging │
│ │
│ ⚠️ Transaction behavior varies by hook: │
│ • validate: throw rolls back entire save │
│ • on_update: document already saved! │
│ • on_submit: partial rollback possible │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ 控制器拥有完整的Python能力 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ try/except代码块 - 完整的异常处理 │
│ ✅ raise语句 - 自定义异常 │
│ ✅ 多except分支 - 处理特定错误 │
│ ✅ finally代码块 - 清理操作 │
│ ✅ frappe.throw() - 终止流程并显示用户消息 │
│ ✅ frappe.log_error() - 静默记录错误日志 │
│ │
│ ⚠️ 事务行为因钩子而异: │
│ • validate:throw会回滚整个保存操作 │
│ • on_update:文档已完成保存! │
│ • on_submit:可能出现部分回滚情况 │
│ │
└─────────────────────────────────────────────────────────────────────┘Main Decision: Error Handling by Hook
核心决策:按钩子类型处理错误
┌─────────────────────────────────────────────────────────────────────────┐
│ WHICH LIFECYCLE HOOK ARE YOU IN? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► validate / before_save │
│ └─► frappe.throw() → Rolls back, document NOT saved │
│ └─► try/except → Catch and re-throw or handle gracefully │
│ │
│ ► on_update / after_insert │
│ └─► Document already saved! frappe.throw() shows error but saved │
│ └─► Use try/except + log_error for non-critical operations │
│ └─► Critical failures: frappe.throw() (shows error, doc is saved) │
│ │
│ ► before_submit │
│ └─► frappe.throw() → Prevents submit, stays draft │
│ └─► Last chance for validation before docstatus=1 │
│ │
│ ► on_submit │
│ └─► Document is submitted! throw shows error but docstatus=1 │
│ └─► Critical: throw causes partial state (submitted but failed) │
│ └─► Better: validate everything in before_submit │
│ │
│ ► on_cancel │
│ └─► Reverse operations - use try/except for each reversal │
│ └─► Log errors but try to continue cleanup │
│ │
└─────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 你当前处于哪个生命周期钩子? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► validate / before_save │
│ └─► frappe.throw() → 执行回滚,文档不会被保存 │
│ └─► try/except → 捕获异常后重新抛出或优雅处理 │
│ │
│ ► on_update / after_insert │
│ └─► 文档已保存!frappe.throw()会显示错误但文档已保存 │
│ └─► 对非关键操作使用try/except + log_error │
│ └─► 严重故障:使用frappe.throw()(显示错误,文档已保存) │
│ │
│ ► before_submit │
│ └─► frappe.throw() → 阻止提交,文档保持草稿状态 │
│ └─► docstatus=1前的最后验证机会 │
│ │
│ ► on_submit │
│ └─► 文档已提交!throw会显示错误但docstatus=1 │
│ └─► 严重问题:throw会导致部分状态异常(已提交但操作失败) │
│ └─► 更佳方案:在before_submit中完成所有验证 │
│ │
│ ► on_cancel │
│ └─► 反向操作 - 对每个反向步骤使用try/except │
│ └─► 记录错误但尽量继续清理操作 │
│ │
└─────────────────────────────────────────────────────────────────────────┘Error Methods Reference
错误处理方法参考
Quick Reference
快速参考
| Method | Stops Execution? | Rolls Back? | User Sees? | Use For |
|---|---|---|---|---|
| ✅ YES | Depends on hook | Dialog | Validation errors |
| ✅ YES | Depends on hook | Error page | Internal errors |
| ❌ NO | ❌ NO | Dialog | Warnings |
| ❌ NO | ❌ NO | Error Log | Debug/audit |
| 方法 | 是否终止执行? | 是否回滚? | 用户可见? | 适用场景 |
|---|---|---|---|---|
| ✅ 是 | 取决于钩子 | 弹窗 | 验证错误 |
| ✅ 是 | 取决于钩子 | 错误页面 | 内部错误 |
| ❌ 否 | ❌ 否 | 弹窗 | 警告提示 |
| ❌ 否 | ❌ 否 | 错误日志 | 调试/审计 |
Transaction Rollback by Hook
按钩子划分的事务回滚规则
| Hook | frappe.throw() Effect |
|---|---|
| ✅ Full rollback - document NOT saved |
| ✅ Full rollback - document NOT saved |
| ⚠️ Document IS saved, error shown |
| ⚠️ Document IS saved, error shown |
| ✅ Full rollback - stays Draft |
| ⚠️ docstatus=1, error shown |
| ✅ Full rollback - stays Submitted |
| ⚠️ docstatus=2, error shown |
| 钩子 | frappe.throw() 效果 |
|---|---|
| ✅ 完全回滚 - 文档未保存 |
| ✅ 完全回滚 - 文档未保存 |
| ⚠️ 文档已保存,仅显示错误 |
| ⚠️ 文档已保存,仅显示错误 |
| ✅ 完全回滚 - 保持草稿状态 |
| ⚠️ docstatus=1,仅显示错误 |
| ✅ 完全回滚 - 保持已提交状态 |
| ⚠️ docstatus=2,仅显示错误 |
Error Handling Patterns
错误处理模式
Pattern 1: Validation with Error Collection
模式1:收集所有验证错误后抛出
python
def validate(self):
"""Collect all errors before throwing."""
errors = []
# Required fields
if not self.customer:
errors.append(_("Customer is required"))
if not self.items:
errors.append(_("At least one item is required"))
# Business rules
if self.discount_percent > 50:
errors.append(_("Discount cannot exceed 50%"))
# Child table validation
for idx, item in enumerate(self.items, 1):
if not item.item_code:
errors.append(_("Row {0}: Item Code is required").format(idx))
if (item.qty or 0) <= 0:
errors.append(_("Row {0}: Quantity must be positive").format(idx))
# Throw all errors at once
if errors:
frappe.throw("<br>".join(errors), title=_("Validation Error"))python
def validate(self):
"""收集所有错误后再抛出。"""
errors = []
# 必填字段检查
if not self.customer:
errors.append(_("客户为必填项"))
if not self.items:
errors.append(_("至少需要一个项目"))
# 业务规则检查
if self.discount_percent > 50:
errors.append(_("折扣不能超过50%"))
# 子表验证
for idx, item in enumerate(self.items, 1):
if not item.item_code:
errors.append(_("第{0}行:项目编码为必填项").format(idx))
if (item.qty or 0) <= 0:
errors.append(_("第{0}行:数量必须为正数").format(idx))
# 一次性抛出所有错误
if errors:
frappe.throw("<br>".join(errors), title=_("验证错误"))Pattern 2: External API Call with Fallback
模式2:带降级方案的外部API调用
python
def validate(self):
"""Call external API with error handling."""
if self.requires_credit_check:
try:
result = self.check_credit_external()
self.credit_score = result.get("score", 0)
except requests.Timeout:
# Timeout - use cached value
frappe.msgprint(
_("Credit check timed out. Using cached value."),
indicator="orange"
)
self.credit_score = self.get_cached_credit_score()
except requests.RequestException as e:
# API error - log and continue with warning
frappe.log_error(
f"Credit check failed: {str(e)}",
"External API Error"
)
frappe.msgprint(
_("Credit check unavailable. Please verify manually."),
indicator="orange"
)
self.credit_check_pending = 1
except Exception as e:
# Unexpected error - log and re-raise
frappe.log_error(frappe.get_traceback(), "Credit Check Error")
frappe.throw(_("Credit check failed. Please try again."))python
def validate(self):
"""带错误处理的外部API调用。"""
if self.requires_credit_check:
try:
result = self.check_credit_external()
self.credit_score = result.get("score", 0)
except requests.Timeout:
# 超时 - 使用缓存值
frappe.msgprint(
_("信用检查超时,将使用缓存值。"),
indicator="orange"
)
self.credit_score = self.get_cached_credit_score()
except requests.RequestException as e:
# API错误 - 记录日志并带警告继续
frappe.log_error(
f"信用检查失败:{str(e)}",
"外部API错误"
)
frappe.msgprint(
_("信用检查不可用,请手动验证。"),
indicator="orange"
)
self.credit_check_pending = 1
except Exception as e:
# 意外错误 - 记录日志并重抛
frappe.log_error(frappe.get_traceback(), "信用检查错误")
frappe.throw(_("信用检查失败,请重试。"))Pattern 3: Safe Post-Save Operations
模式3:安全的保存后操作
python
def on_update(self):
"""Handle post-save operations safely."""
# Critical operation - throw on failure
self.update_linked_documents()
# Non-critical operations - log errors, don't throw
try:
self.send_notification()
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"Notification failed for {self.name}"
)
try:
self.sync_to_external_system()
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"External sync failed for {self.name}"
)
# Queue for retry
frappe.enqueue(
"myapp.tasks.retry_sync",
doctype=self.doctype,
name=self.name,
queue="short"
)python
def on_update(self):
"""安全处理保存后操作。"""
# 关键操作 - 失败则抛出错误
self.update_linked_documents()
# 非关键操作 - 记录错误但不抛出
try:
self.send_notification()
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"{self.name}的通知发送失败"
)
try:
self.sync_to_external_system()
except Exception:
frappe.log_error(
frappe.get_traceback(),
f"{self.name}的外部系统同步失败"
)
# 加入队列重试
frappe.enqueue(
"myapp.tasks.retry_sync",
doctype=self.doctype,
name=self.name,
queue="short"
)Pattern 4: Submittable Document Error Handling
模式4:可提交文档的错误处理
python
def before_submit(self):
"""All validations that must pass before submit."""
# Validate everything here - last chance to abort cleanly
if not self.items:
frappe.throw(_("Cannot submit without items"))
if self.grand_total <= 0:
frappe.throw(_("Total must be greater than zero"))
# Check stock availability
for item in self.items:
available = get_stock_balance(item.item_code, item.warehouse)
if available < item.qty:
frappe.throw(
_("Row {0}: Insufficient stock for {1}. Available: {2}").format(
item.idx, item.item_code, available
)
)
def on_submit(self):
"""Post-submit actions - document is already submitted!"""
# These operations should not fail if before_submit passed
try:
self.create_stock_ledger_entries()
except Exception as e:
# CRITICAL: Document is submitted but entries failed!
frappe.log_error(frappe.get_traceback(), "Stock Ledger Error")
frappe.throw(
_("Stock entries failed. Please cancel and retry. Error: {0}").format(str(e))
)
try:
self.create_gl_entries()
except Exception as e:
# Rollback stock entries if GL fails
self.reverse_stock_ledger_entries()
frappe.log_error(frappe.get_traceback(), "GL Entry Error")
frappe.throw(_("Accounting entries failed. Stock entries reversed."))python
def before_submit(self):
"""提交前必须通过的所有验证。"""
# 在此处完成所有验证 - 这是干净终止的最后机会
if not self.items:
frappe.throw(_("无项目无法提交"))
if self.grand_total <= 0:
frappe.throw(_("总金额必须大于0"))
# 检查库存可用性
for item in self.items:
available = get_stock_balance(item.item_code, item.warehouse)
if available < item.qty:
frappe.throw(
_("第{0}行:{1}库存不足。可用库存:{2}").format(
item.idx, item.item_code, available
)
)
def on_submit(self):
"""提交后操作 - 文档已完成提交!"""
# 如果before_submit验证通过,这些操作不应失败
try:
self.create_stock_ledger_entries()
except Exception as e:
# 严重问题:文档已提交但库存分录失败!
frappe.log_error(frappe.get_traceback(), "库存台账错误")
frappe.throw(
_("库存分录失败,请取消后重试。错误:{0}").format(str(e))
)
try:
self.create_gl_entries()
except Exception as e:
# 如果总账分录失败,回滚库存分录
self.reverse_stock_ledger_entries()
frappe.log_error(frappe.get_traceback(), "总账分录错误")
frappe.throw(_("会计分录失败,库存分录已回滚。"))Pattern 5: Cancel with Cleanup
模式5:带清理操作的取消流程
python
def before_cancel(self):
"""Validate cancel is allowed."""
# Check for linked documents
linked_invoices = frappe.get_all(
"Sales Invoice Item",
filters={"sales_order": self.name, "docstatus": 1},
pluck="parent"
)
if linked_invoices:
frappe.throw(
_("Cannot cancel. Linked invoices exist: {0}").format(
", ".join(linked_invoices)
)
)
def on_cancel(self):
"""Reverse operations - try to complete all cleanup."""
errors = []
# Reverse stock
try:
self.reverse_stock_ledger_entries()
except Exception as e:
errors.append(f"Stock reversal: {str(e)}")
frappe.log_error(frappe.get_traceback(), "Stock Reversal Error")
# Reverse GL
try:
self.reverse_gl_entries()
except Exception as e:
errors.append(f"GL reversal: {str(e)}")
frappe.log_error(frappe.get_traceback(), "GL Reversal Error")
# Update linked docs
try:
self.update_linked_on_cancel()
except Exception as e:
errors.append(f"Linked docs: {str(e)}")
frappe.log_error(frappe.get_traceback(), "Linked Doc Update Error")
# Report any errors but don't prevent cancel
if errors:
frappe.msgprint(
_("Cancel completed with errors:<br>{0}").format("<br>".join(errors)),
title=_("Warning"),
indicator="orange"
)python
def before_cancel(self):
"""验证是否允许取消。"""
# 检查关联文档
linked_invoices = frappe.get_all(
"Sales Invoice Item",
filters={"sales_order": self.name, "docstatus": 1},
pluck="parent"
)
if linked_invoices:
frappe.throw(
_("无法取消,存在关联发票:{0}").format(
", ".join(linked_invoices)
)
)
def on_cancel(self):
"""反向操作 - 尽量完成所有清理。"""
errors = []
# 回滚库存
try:
self.reverse_stock_ledger_entries()
except Exception as e:
errors.append(f"库存回滚:{str(e)}")
frappe.log_error(frappe.get_traceback(), "库存回滚错误")
# 回滚总账
try:
self.reverse_gl_entries()
except Exception as e:
errors.append(f"总账回滚:{str(e)}")
frappe.log_error(frappe.get_traceback(), "总账回滚错误")
# 更新关联文档
try:
self.update_linked_on_cancel()
except Exception as e:
errors.append(f"关联文档:{str(e)}")
frappe.log_error(frappe.get_traceback(), "关联文档更新错误")
# 报告错误但不阻止取消
if errors:
frappe.msgprint(
_("取消操作已完成,但存在以下错误:<br>{0}").format("<br>".join(errors)),
title=_("警告"),
indicator="orange"
)Pattern 6: Database Operation Error Handling
模式6:数据库操作的错误处理
python
def validate(self):
"""Handle database errors gracefully."""
try:
# Check for duplicates
existing = frappe.db.exists(
"Customer Contract",
{"customer": self.customer, "status": "Active", "name": ["!=", self.name]}
)
if existing:
frappe.throw(_("Active contract already exists for this customer"))
except frappe.db.InternalError as e:
# Database error - log and show user-friendly message
frappe.log_error(frappe.get_traceback(), "Database Error")
frappe.throw(_("Database error. Please try again or contact support."))See:for more error handling patterns.references/patterns.md
python
def validate(self):
"""优雅处理数据库错误。"""
try:
# 检查重复项
existing = frappe.db.exists(
"Customer Contract",
{"customer": self.customer, "status": "Active", "name": ["!=", self.name]}
)
if existing:
frappe.throw(_("该客户已存在有效的合同"))
except frappe.db.InternalError as e:
# 数据库错误 - 记录日志并显示友好提示
frappe.log_error(frappe.get_traceback(), "数据库错误")
frappe.throw(_("数据库错误,请重试或联系支持人员。"))参考:更多错误处理模式请查看。references/patterns.md
Transaction Management
事务管理
Understanding Transactions
事务理解
python
undefinedpython
undefinedFrappe wraps each request in a transaction
Frappe会为每个请求自动包裹事务
- On success: auto-commit
- 成功时:自动提交
- On exception: auto-rollback
- 异常时:自动回滚
def validate(self):
# All these changes are in ONE transaction
self.calculate_totals()
frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
frappe.throw("Error") # EVERYTHING rolls backdef on_update(self):
# Document save is already committed!
# New changes here are in a NEW transaction
frappe.db.set_value("Other", "doc", "field", "value")
if error_condition:
frappe.throw("Error") # Only on_update changes roll back
# The document itself is already saved!undefineddef validate(self):
# 所有这些修改都在同一个事务中
self.calculate_totals()
frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
frappe.throw("错误") # 所有修改都会回滚def on_update(self):
# 文档保存已完成提交!
# 此处的新修改属于新事务
frappe.db.set_value("Other", "doc", "field", "value")
if error_condition:
frappe.throw("错误") # 仅回滚on_update中的修改
# 文档本身已保存!undefinedManual Savepoints (Advanced)
手动保存点(高级用法)
python
def on_submit(self):
"""Use savepoints for partial rollback."""
# Create savepoint before risky operation
frappe.db.savepoint("before_stock")
try:
self.create_stock_entries()
except Exception:
# Rollback only stock entries
frappe.db.rollback(save_point="before_stock")
frappe.log_error(frappe.get_traceback(), "Stock Entry Error")
frappe.throw(_("Stock entries failed"))
frappe.db.savepoint("before_gl")
try:
self.create_gl_entries()
except Exception:
frappe.db.rollback(save_point="before_gl")
frappe.log_error(frappe.get_traceback(), "GL Entry Error")
frappe.throw(_("GL entries failed"))python
def on_submit(self):
"""使用保存点实现部分回滚。"""
# 风险操作前创建保存点
frappe.db.savepoint("before_stock")
try:
self.create_stock_entries()
except Exception:
# 仅回滚库存操作
frappe.db.rollback(save_point="before_stock")
frappe.log_error(frappe.get_traceback(), "库存分录错误")
frappe.throw(_("库存分录失败"))
frappe.db.savepoint("before_gl")
try:
self.create_gl_entries()
except Exception:
frappe.db.rollback(save_point="before_gl")
frappe.log_error(frappe.get_traceback(), "总账分录错误")
frappe.throw(_("总账分录失败"))Critical Rules
核心规则
✅ ALWAYS
✅ 必须遵守
- Collect multiple validation errors - Better UX than one at a time
- Use try/except around external calls - APIs, file I/O, network
- Log unexpected errors -
frappe.log_error(frappe.get_traceback()) - Call super() in overridden methods - Preserve parent behavior
- Validate in before_submit - Last clean abort point for submittables
- Use _() for error messages - Enable translation
- 收集多个验证错误 - 一次性显示比逐个显示更优的用户体验
- 外部调用包裹try/except - 包括API、文件I/O、网络操作
- 记录意外错误 - 使用
frappe.log_error(frappe.get_traceback()) - 重写方法时调用super() - 保留父类行为
- 在before_submit中完成验证 - 可提交文档的最后干净终止点
- 错误消息使用_()包裹 - 支持多语言翻译
❌ NEVER
❌ 严禁操作
- Don't call frappe.db.commit() - Framework handles transactions
- Don't swallow errors silently - Always log unexpected exceptions
- Don't assume on_update can rollback doc - It's already saved
- Don't put critical logic in on_submit - Validate in before_submit
- Don't ignore return values - Check for None/empty results
- 不要调用frappe.db.commit() - 框架会自动处理事务
- 不要静默吞掉错误 - 意外异常必须记录日志
- 不要假设on_update可以回滚文档 - 文档已保存
- 不要在on_submit中放置关键逻辑 - 应在before_submit中验证
- 不要忽略返回值 - 检查None/空结果
Quick Reference: Exception Handling
快速参考:异常处理
python
undefinedpython
undefinedCatch specific exceptions first, general last
先捕获特定异常,最后捕获通用异常
try:
result = risky_operation()
except frappe.ValidationError:
# Re-raise validation errors
raise
except frappe.DoesNotExistError:
# Handle missing document
frappe.throw(("Referenced document not found"))
except requests.Timeout:
# Handle timeout specifically
frappe.msgprint(("Operation timed out"), indicator="orange")
except Exception as e:
# Log and handle unexpected errors
frappe.log_error(frappe.get_traceback(), "Unexpected Error")
frappe.throw(_("An error occurred: {0}").format(str(e)))
---try:
result = risky_operation()
except frappe.ValidationError:
# 重抛验证错误
raise
except frappe.DoesNotExistError:
# 处理文档不存在的情况
frappe.throw(("引用的文档不存在"))
except requests.Timeout:
# 专门处理超时
frappe.msgprint(("操作超时"), indicator="orange")
except Exception as e:
# 记录并处理意外错误
frappe.log_error(frappe.get_traceback(), "意外错误")
frappe.throw(_("发生错误:{0}").format(str(e)))
---Reference Files
参考文件
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整的错误处理模式 |
| 完整的可运行示例 |
| 需避免的常见错误 |
See Also
相关参考
- - Controller syntax
erpnext-syntax-controllers - - Implementation workflows
erpnext-impl-controllers - - Server Script error handling (sandbox)
erpnext-errors-serverscripts - - Hook error handling
erpnext-errors-hooks
- - 控制器语法
erpnext-syntax-controllers - - 实现工作流
erpnext-impl-controllers - - 服务器脚本错误处理(沙箱环境)
erpnext-errors-serverscripts - - 钩子错误处理
erpnext-errors-hooks