Loading...
Loading...
Compare original and translation side by side
erpnext-syntax-controllerserpnext-impl-controllerserpnext-syntax-controllerserpnext-impl-controllers┌─────────────────────────────────────────────────────────────────────┐
│ 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:可能出现部分回滚情况 │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │
│ └─► 记录错误但尽量继续清理操作 │
│ │
└─────────────────────────────────────────────────────────────────────────┘| 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 |
| 方法 | 是否终止执行? | 是否回滚? | 用户可见? | 适用场景 |
|---|---|---|---|---|
| ✅ 是 | 取决于钩子 | 弹窗 | 验证错误 |
| ✅ 是 | 取决于钩子 | 错误页面 | 内部错误 |
| ❌ 否 | ❌ 否 | 弹窗 | 警告提示 |
| ❌ 否 | ❌ 否 | 错误日志 | 调试/审计 |
| 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,仅显示错误 |
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"))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=_("验证错误"))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."))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(_("信用检查失败,请重试。"))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"
)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"
)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."))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(_("会计分录失败,库存分录已回滚。"))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"
)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"
)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
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
undefinedundefinedif error_condition:
frappe.throw("Error") # EVERYTHING rolls backif error_condition:
frappe.throw("Error") # Only on_update changes roll back
# The document itself is already saved!undefinedif error_condition:
frappe.throw("错误") # 所有修改都会回滚if error_condition:
frappe.throw("错误") # 仅回滚on_update中的修改
# 文档本身已保存!undefineddef 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"))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(_("总账分录失败"))frappe.log_error(frappe.get_traceback())frappe.log_error(frappe.get_traceback())undefinedundefined
---
---| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整的错误处理模式 |
| 完整的可运行示例 |
| 需避免的常见错误 |
erpnext-syntax-controllerserpnext-impl-controllerserpnext-errors-serverscriptserpnext-errors-hookserpnext-syntax-controllerserpnext-impl-controllerserpnext-errors-serverscriptserpnext-errors-hooks