关于 frappe-syntax-hooks-events
面向 Frappe 开发者的 doc_events 生命周期钩子参考,说明事件执行顺序、注册方式与处理器选择,重点涵盖跨应用扩展和事务行为。
主要功能
- 01插入、保存、提交等事件顺序
- 02hooks.py 中注册处理器
- 03按需求选择生命周期事件
- 04doc_events 与控制器事件对比
- 05多应用事件排序与事务规则
适用场景
- 01在保存前验证文档数据
- 02提交后创建关联文档
- 03为其他应用的 DocType 添加事件处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-hooks-events安装后在 Claude Code 中输入/frappe-syntax-hooks-events确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/.gitkeepanti-patterns.mdevent-order.mdexamples.md详细介绍
文档生命周期钩子(doc_events)
快速参考:事件执行顺序
插入(新文档)
| 顺序 | 事件 | 用途 | 可抛出异常? |
|---|---|---|---|
| 1 | before_insert | 命名之前设置默认值 | 是 |
| 2 | before_naming | 修改命名逻辑 | 是 |
| 3 | autoname | 设置 name 属性 | 是 |
| 4 | before_validate | 自动设置缺失的值 | 是 |
| 5 | validate | 验证逻辑——抛出异常以中止 | 是 |
| 6 | before_save | 写入数据库之前进行最终修改 | 是 |
| 7 | db_insert | 内部——将行写入数据库 | — |
| 8 | after_insert | 插入后逻辑(仅运行一次) | 是 |
| 9 | on_update | 保存后逻辑(每次保存时运行) | 是 |
| 10 | on_change | 任意字段值发生变化时触发 | 是 |
保存(现有文档)
| 顺序 | 事件 | 用途 |
|---|---|---|
| 1 | before_validate | 自动设置缺失的值 |
| 2 | validate | 验证逻辑——抛出异常以中止 |
| 3 | before_save | 写入数据库之前进行最终修改 |
| 4 | db_update | 内部——更新数据库中的行 |
| 5 | on_update | 保存后逻辑 |
| 6 | on_change | 任意字段值发生变化时触发 |
提交
| 顺序 | 事件 | 用途 |
|---|---|---|
| 1 | before_validate | 自动设置缺失的值 |
| 2 | validate | 验证逻辑 |
| 3 | before_save | 写入数据库之前进行最终修改 |
| 4 | before_submit | 提交前逻辑——抛出异常以中止 |
| 5 | db_update | 内部——更新数据库中的行 |
| 6 | on_submit | 提交后逻辑(GL 分录等) |
| 7 | on_update | 保存后逻辑 |
| 8 | on_change | 任意字段值发生变化时触发 |
取消
| 顺序 | 事件 | 用途 |
|---|---|---|
| 1 | before_cancel | 取消前验证 |
| 2 | db_update | 内部——更新数据库中的行 |
| 3 | on_cancel | 取消后逻辑(冲销 GL 等) |
| 4 | on_change | 任意字段值发生变化时触发 |
删除
| 顺序 | 事件 | 用途 |
|---|---|---|
| 1 | on_trash | 删除前清理 |
| 2 | after_delete | 删除后逻辑 |
其他操作
| 操作 | 事件(按顺序) |
|---|---|
| 重命名 | before_rename → after_rename |
| 修订 | 新修订文档会执行 before_insert 链 |
| 提交后更新 | before_update_after_submit → db_update → on_update_after_submit → on_change |
hooks.py 中的 doc_events:语法
基本结构
# hooks.py
doc_events = {
"Sales Invoice": {
"on_submit": "myapp.events.sales_invoice.on_submit",
"on_cancel": "myapp.events.sales_invoice.on_cancel",
},
"Purchase Order": {
"validate": "myapp.events.purchase_order.validate",
}
}
通配符:应用于所有 DocType
doc_events = {
"*": {
"after_insert": "myapp.events.global_handler.after_insert_all",
"on_update": "myapp.events.global_handler.track_changes",
}
}
务必使用 "*"(包含星号的字符串)作为键。这样会为每个 DocType 触发处理函数。
每个事件使用多个处理函数
doc_events = {
"Sales Invoice": {
"on_submit": [
"myapp.events.accounting.create_gl_entries",
"myapp.events.notifications.send_invoice_email",
]
}
}
处理函数签名
# myapp/events/sales_invoice.py
def on_submit(doc, method=None):
"""
doc — the Document instance (e.g., Sales Invoice)
method — string name of the event (e.g., "on_submit"), or None
"""
if doc.grand_total > 10000:
frappe.sendmail(...)
务必将 method 作为第二个参数(默认值为 None)。Frappe 会自动传入该参数。
决策树:使用哪个事件
“我需要在保存前验证数据”
→ 使用 validate。此处始终调用 frappe.throw(),以阻止无效保存。
“我需要自动设置默认值”
→ 使用 before_validate。它会在 validate 之前运行,因此可在验证检查前设置默认值。
“我需要仅在首次创建时运行逻辑”
→ 使用 after_insert。它仅在插入时触发,不会在后续保存时触发。
“我需要在每次保存时运行逻辑(插入 + 更新)”
→ 使用 on_update。插入和保存操作都会触发它。
“我需要在提交后创建关联文档”
→ 使用 on_submit。切勿在 validate 中创建关联文档——此时文档尚未提交。
“我需要在取消时撤销关联文档”
→ 使用 on_cancel。务必在此处清理 GL 分录、库存分类账分录和关联文档。
“我需要修改文档名称”
→ 在控制器中使用 autoname,或使用 before_naming 实现条件逻辑。
“我需要在特定条件下阻止删除”
→ 使用 on_trash。调用 frappe.throw() 以阻止删除。
“我需要更新已提交文档的字段”
→ 使用 before_update_after_submit 进行验证,并使用 on_update_after_submit 执行副作用。
“我需要仅在值实际发生变化时运行逻辑”
→ 使用 on_change。只有至少一个字段值与数据库状态不同时才会触发。
doc_events 与控制器事件
这两种机制都会触发相同的事件。区别在于注册位置。
| 方面 | 控制器(类方法) | doc_events(hooks.py) |
|---|---|---|
| 位置 | {doctype}.py 控制器文件 | 应用中的 hooks.py |
| 适用场景 | 你拥有该 DocType | 你要扩展其他应用的 DocType |
| 执行顺序 | 先运行(控制器) | 在控制器方法之后运行 |
| 多个应用 | 每个 DocType 只能有一个控制器 | 多个应用可以注册处理程序 |
为不属于自己的 DocType 添加钩子时,务必使用 doc_events。切勿直接修改其他应用的控制器文件。
单个事件中的执行顺序
对于给定事件(例如 validate):
- 首先运行控制器方法(
def validate(self)) doc_events处理程序按照应用安装顺序运行- 通配符
"*"处理程序在特定 DocType 处理程序之后运行
extend_doctype_class [v16+]
在 Frappe v16+ 中,extend_doctype_class 是一种更简洁的替代方案,可用于向现有 DocType 添加方法,而无需使用 doc_events。
hooks.py
extend_doctype_class = {
"Sales Invoice": [
"myapp.overrides.sales_invoice.SalesInvoiceExtension"
]
}
扩展类(混入类)
# myapp/overrides/sales_invoice.py
import frappe
class SalesInvoiceExtension:
def validate(self):
"""This is called as part of the controller chain."""
if self.grand_total < 0:
frappe.throw("Grand total cannot be negative")
def custom_method(self):
"""Custom methods are also available on the doc instance."""
return self.items
重要规则
- 在 v16+ 中,如果可能有多个应用扩展同一个 DocType,始终优先使用
extend_doctype_class,而非override_doctype_class。 - 多个应用可以扩展同一个 DocType——扩展会通过 MRO 叠加。
- 类解析顺序遵循钩子优先级:
class Final(App2Mixin, App1Mixin, Original)。 - 扩展方法(如
validate)作为控制器的一部分运行,而不是作为独立的doc_events处理程序运行。
override_doctype_class [v14+]
完全替换控制器类。使用时务必格外谨慎。
# hooks.py
override_doctype_class = {
"ToDo": "myapp.overrides.todo.CustomToDo"
}
# myapp/overrides/todo.py
from frappe.desk.doctype.todo.todo import ToDo
class CustomToDo(ToDo):
def validate(self):
super().validate() # ALWAYS call super() to preserve original logic
# Your additions here
如果可以使用 extend_doctype_class(v16+),切勿使用 override_doctype_class。一个 DocType 只能由一个应用覆盖——最后安装的应用会生效,且会悄无声息地破坏其他应用。
多应用事件顺序
当多个应用为同一个 DocType 和事件注册 doc_events 时:
- 处理程序按照应用安装顺序执行(顺序见
sites/{site}/site_config.json→installed_apps)。 - 可通过设置 > 已安装应用程序 > 更新钩子解析顺序更改顺序。
- 对于
override_doctype_class,最后安装的应用会生效(仅应用一个覆盖)。 - 对于
extend_doctype_class(v16+),所有扩展都会累积叠加。
事务行为
从 before_validate 到 on_change 的所有文档事件都在同一个数据库事务中运行。
- 如果任意事件引发异常,整个操作都会回滚(包括
db_insert/db_update)。 after_insert、on_update、on_submit、on_cancel均在事务提交前运行。- 只有所有事件都成功完成后,事务才会提交。
after_delete在 DELETE 语句执行后运行,但仍处于请求事务中。
绝不要假设事件处理程序中的数据已提交到数据库。其他并发请求要等到整个请求完成后才能看到你的更改。
重要规则
- 始终使用
frappe.throw()中止操作,绝不要使用raise Exception。 - 绝不要在
autoname或before_naming之外修改doc.name。 - 在子类中重写控制器方法时,始终调用
super().{event}()。 - 绝不要在
validate或before_save中使用doc.save(),否则会导致无限递归。 - 如果钩子需要绕过权限,始终显式设置
doc.flags.ignore_permissions = True,绝不要假设钩子以 Administrator 身份运行。 - 绝不要在
validate中执行耗时操作(API 调用、文件 I/O),应改用after_insert或on_update配合frappe.enqueue()。 - 始终使用
doc.flags在同一请求中的事件之间传递信息(例如,doc.flags.skip_notification = True)。 - 绝不要依赖
on_change执行关键逻辑——只有当值与数据库状态实际不同时,它才会触发。
另请参阅
- 事件执行顺序——详细示意图
- 常见模式的实用示例
- 反模式和常见错误
frappe-syntax-hooks-config——应用级钩子(调度器、固定数据、权限)- 官方文档:https://docs.frappe.io/framework/user/en/basics/doctypes/controllers