关于 frappe-errors-hooks
面向 Frappe 开发者的钩子错误诊断与解决参考,按常见症状说明可能原因和处理方式。内容涵盖事件、权限、调度器、Fixtures 和类扩展等钩子。
主要功能
- 01按错误症状查找原因与修复
- 02诊断钩子路径和注册问题
- 03排查调度任务与 Fixtures
- 04处理权限和事件钩子错误
- 05了解覆盖与扩展冲突
适用场景
- 01定位钩子未触发的原因
- 02排查调度器任务未运行
- 03修复权限钩子导致的列表错误
- 04诊断 Fixtures 安装或导入失败
安装
复制下面的命令,在终端中运行。
$
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.mdexamples.mdpatterns.md详细介绍
Frappe 钩子错误诊断与解决
交叉参考:frappe-syntax-hooks(语法)、frappe-impl-hooks(工作流)、frappe-errors-controllers(控制器错误)。
错误与修复对照表
| 错误 / 症状 | 原因 | 修复方法 |
|---|---|---|
| 钩子完全未触发 | 点分路径拼写错误 | 确认模块路径与实际文件位置一致 |
bench 启动时出现 ImportError | 模块路径错误或循环导入 | 修正导入路径;打破循环依赖 |
AttributeError: module has no attribute | hooks.py 中的函数名拼写错误 | 确保函数名与 Python 定义完全一致 |
app_include_js 未加载 | 路径缺少 assets/ 前缀或扩展名错误 | 使用 "assets/myapp/js/file.js" 格式 |
| scheduler_events 未运行 | Scheduler 已禁用或工作进程已停止 | 运行 bench scheduler enable,检查 bench doctor |
| doc_events 处理程序从未调用 | 字典键中的 DocType 名称拼写错误 | 使用准确的 DocType 名称,包含空格:"Sales Invoice" |
permission_query_conditions 导致列表视图异常 | SQL 语法错误,或处理程序中调用了 frappe.throw() | 返回有效的 SQL 字符串;绝不要抛出异常 |
override_doctype_class 导入失败 | 父类导入路径在不同版本间发生变化 | 将导入固定到目标版本的正确模块路径 |
extend_doctype_class [v16+] 方法冲突 | 两个扩展定义了同名方法 | 重命名冲突方法;检查钩子解析顺序 |
| 安装时未加载 Fixtures | dt 键错误,或目标环境中不存在该 DocType | 导出前确认 DocType 存在;检查筛选语法 |
extend_bootinfo 导致登录失败 | boot 处理程序中出现未处理的异常 | 将所有 bootinfo 代码包装在 try/except 中 |
通配符 "*" 处理程序导致所有保存操作失败 | 通配符 doc_events 中出现未处理的异常 | 始终将通配符处理程序包装在 try/except 中 |
| 钩子触发,但更改丢失 | Scheduler 中缺少 frappe.db.commit() | 在 Scheduler/后台任务中显式提交 |
| 多处理程序链中断 | 第一个处理程序抛出异常,后续处理程序不会运行 | 将非关键操作隔离在 try/except 中 |
钩子注册错误
钩子未触发:诊断检查清单
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,而不是由你的代码导入它。
不同钩子类型的错误字典结构
# ❌ 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 错误
按事件阶段处理错误
| 事件 | 抛出异常的影响 | 事务 | 模式 |
|---|---|---|---|
validate | 阻止保存,完全回滚 | 写入前 | 收集错误,然后一次性抛出 |
before_save | 阻止保存,完全回滚 | 写入前 | 与 validate 相同 |
on_update | 文档已保存,显示错误 | 写入后 | 隔离非关键操作 |
after_insert | 文档已保存,显示错误 | 写入后 | 隔离非关键操作 |
on_submit | 文档已提交 | 写入后 | 隔离非关键操作 |
on_cancel | 文档已取消 | 写入后 | 隔离非关键操作 |
多处理程序链问题
# 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 中提交
# ❌ 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_query_conditions:绝不要抛出异常
# ❌ 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:绝不要抛出异常
# ❌ 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_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 生效(“后写入者胜出”)。为兼容多个应用,请使用 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")
Fixtures 未加载
# ❌ 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 期间自动执行
关键规则
始终
- 更改 hooks.py 后重启 bench
- 在调度任务中使用 try/except——用户看不到错误
- 在调度任务中调用
frappe.db.commit()——不会自动提交 - 在权限钩子中返回安全的备用值——绝不抛出异常
- 在重写类中调用
super()——重新抛出 ValidationError - 在 try/except 中包装
extend_bootinfo——错误会导致无法登录 - 在 try/except 中包装通配符
"*"doc_events——错误会导致所有保存操作失败 - 为 extend_doctype_class [v16+] 方法添加应用名前缀
绝不
- 在
permission_query_conditions中抛出异常——会导致列表视图失效 - 在
has_permission中抛出异常——会导致文档无法访问 - 在 doc_events 中提交——会破坏事务管理
- 在事件处理程序中从 hooks.py 导入内容——会导致循环导入
- 假设只有一个处理程序——多个应用都可以注册 doc_events
- 在权限 SQL 中使用字符串格式化——存在 SQL 注入风险
- 忽略调度任务错误——它们会完全静默地失败
快速参考:各类钩子的错误处理
| 钩子类型 | 可以抛出异常? | 提交? | 错误处理策略 |
|---|---|---|---|
| doc_events(validate) | 是 | 绝不 | 收集错误,一次性抛出 |
| doc_events(on_update+) | 谨慎 | 绝不 | 隔离非关键操作 |
| scheduler_events | 没意义 | 始终 | try/except + log_error |
| permission_query_conditions | 绝不 | 绝不 | 返回 "" 或 owner 过滤条件 |
| 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— 钩子语法和字典结构frappe-impl-hooks— 实现工作流frappe-errors-controllers— 控制器错误处理frappe-errors-database— 数据库错误处理frappe-errors-serverscripts— Server Script 错误处理
评论
支持 Markdown