关于 frappe-impl-hooks
面向 Frappe 开发者的 hooks.py 实现工作流指南,涵盖事件、调度、权限及资源注入等配置。按步骤说明选择、编写、部署和测试,并指出不同版本的差异。
主要功能
- 01配置文档生命周期事件
- 02设置定时任务与 Cron
- 03扩展 DocType 类与权限
- 04注入前端资源和网站钩子
- 05迁移并测试 Hook 配置
适用场景
- 01为其他应用的 DocType 添加事件处理
- 02安排定期清理或报告任务
- 03为 Sales Invoice 扩展逻辑
- 04配置权限过滤或页面资源
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-impl-hooks安装后在 Claude Code 中输入/frappe-impl-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mddecision-tree.mdexamples.mdworkflows.md详细介绍
Frappe 钩子实现工作流
实现 hooks.py 配置的分步工作流。API 语法参考请参阅 frappe-syntax-hooks。
版本:v14/v15/v16(V16 专属功能会注明)
主要决策:你要实现什么?
WHAT DO YOU WANT TO ACHIEVE?
│
├─► React to document lifecycle events?
│ ├─► On OTHER app's DocTypes → doc_events in hooks.py
│ ├─► On YOUR OWN DocTypes → controller methods (preferred)
│ └─► On ALL DocTypes → doc_events with "*" wildcard
│
├─► Run code on a schedule?
│ └─► scheduler_events (daily, hourly, cron, etc.)
│
├─► Modify an existing DocType's behavior?
│ ├─► V16+: extend_doctype_class (RECOMMENDED)
│ └─► V14/V15: override_doctype_class (last app wins!)
│
├─► Override an existing API endpoint?
│ └─► override_whitelisted_methods
│
├─► Add custom permission logic?
│ ├─► List filtering → permission_query_conditions
│ └─► Document-level → has_permission
│
├─► Send config data to client on page load?
│ └─► extend_bootinfo
│
├─► Export/import configuration?
│ └─► fixtures
│
├─► Add JS/CSS to desk or portal?
│ ├─► Desk-wide → app_include_js / app_include_css
│ ├─► Portal-wide → web_include_js / web_include_css
│ └─► Specific form → doctype_js
│
├─► Customize website/portal behavior?
│ └─► website_context, portal_menu_items, website_route_rules
│
└─► Hook into session/auth lifecycle?
└─► on_login, on_session_creation, on_logout
工作流 1:实现 doc_events
适用场景
如果需要响应由其他应用(ERPNext、Frappe 核心)拥有的 DocType 的文档生命周期事件,请使用 doc_events。对于你自己的 DocType,始终优先使用控制器方法。
分步说明
步骤 1:选择合适的事件(参见 references/decision-tree.md)
BEFORE save: validate (every save), before_insert (new only)
AFTER save: after_insert (new only), on_update (every save), on_change (any change)
SUBMIT flow: before_submit → on_submit → on_change
CANCEL flow: before_cancel → on_cancel → on_change
DELETE: on_trash (before), after_delete (after)
RENAME: before_rename, after_rename
步骤 2:添加到 hooks.py
# myapp/hooks.py
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.sales_invoice.validate",
"on_submit": "myapp.events.sales_invoice.on_submit"
}
}
步骤 3:创建处理程序模块
# myapp/events/sales_invoice.py
import frappe
def validate(doc, method=None):
"""Changes to doc ARE saved (before-save event)."""
if doc.grand_total < 0:
frappe.throw("Total cannot be negative")
def on_submit(doc, method=None):
"""Document already saved. Use db_set_value for changes."""
frappe.db.set_value("Sales Invoice", doc.name,
"custom_external_id", create_external(doc))
步骤 4:部署
bench --site sitename migrate
步骤 5:测试
bench --site sitename execute myapp.events.sales_invoice.validate --kwargs '{"doc_name": "INV-001"}'
# Or in bench console:
# doc = frappe.get_doc("Sales Invoice", "INV-001"); doc.save()
doc_events 的关键规则
- 绝不要在 doc_event 处理程序中调用
frappe.db.commit()——Frappe 会管理事务 - 绝不要在
on_update中修改doc字段——更改会丢失;请改用frappe.db.set_value() - 始终在处理程序签名中将
method=None作为第二个参数 - 始终使用重命名签名:
def handler(doc, method, old, new, merge) - 始终在更改 hooks.py 后运行
bench --site sitename migrate
工作流 2:实现 scheduler_events
分步说明
步骤 1:选择频率
| 频率 | 短任务(< 5 分钟) | 长任务(5-25 分钟) |
|---|---|---|
| 每次 tick | all | — |
| 每小时 | hourly | hourly_long |
| 每天 | daily | daily_long |
| 每周 | weekly | weekly_long |
| 每月 | monthly | monthly_long |
| 自定义 | cron | cron(手动使用长任务队列) |
步骤 2:添加到 hooks.py
scheduler_events = {
"daily": ["myapp.tasks.daily_cleanup"],
"daily_long": ["myapp.tasks.heavy_sync"],
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_report"]
}
}
步骤 3:实现任务(不带参数)
# myapp/tasks.py
import frappe
def daily_cleanup():
"""Scheduler calls with NO arguments."""
frappe.db.delete("Error Log", {
"creation": ["<", frappe.utils.add_days(None, -30)]
})
frappe.db.commit()
def heavy_sync():
"""Long task — commit periodically."""
records = get_records_to_sync()
for i, record in enumerate(records):
process(record)
if i % 100 == 0:
frappe.db.commit()
frappe.db.commit()
步骤 4:部署并验证
bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status
# Test manually:
bench --site sitename execute myapp.tasks.daily_cleanup
Scheduler 的关键规则
- 绝不要为 scheduler 任务函数添加参数——scheduler 不会传递任何参数
- 始终为耗时超过 5 分钟的任务使用
_long变体(默认队列超时时间为 5 分钟) - 始终在长任务中定期提交,以保存进度
- 任务耗时超过 25 分钟:拆分成多个批次,或使用
frappe.enqueue()
工作流 3:实现 extend_doctype_class(V16+)
分步说明
步骤 1:添加到 hooks.py
extend_doctype_class = {
"Sales Invoice": ["myapp.extensions.sales_invoice.SalesInvoiceMixin"]
}
步骤 2:创建混入类
# myapp/extensions/sales_invoice.py
import frappe
from frappe.model.document import Document
class SalesInvoiceMixin(Document):
def validate(self):
super().validate() # ALWAYS call super() FIRST
self.custom_validation()
def custom_validation(self):
if self.grand_total > 1000000:
frappe.msgprint("High-value invoice", indicator="orange")
步骤 3:部署——bench --site sitename migrate
何时使用 extend 而非 override
- 始终在 V16+ 优先使用
extend_doctype_class——多个应用可以安全地扩展 - 仅在必须完全替换控制器逻辑时使用
override_doctype_class - 在 V14/V15 中,
override_doctype_class是唯一选项——最后安装的应用生效
工作流 4:实现权限钩子
分步说明
步骤 1:添加到 hooks.py
permission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_permission"
}
步骤 2:实现处理程序
# myapp/permissions.py
import frappe
def si_query(user):
"""Returns SQL WHERE clause for list filtering."""
if not user:
user = frappe.session.user
if "Sales Manager" in frappe.get_roles(user):
return "" # See all
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"
def si_permission(doc, user=None, permission_type=None):
"""Returns True (allow), False (deny), or None (use default)."""
if not user:
user = frappe.session.user
if permission_type == "write" and doc.status == "Closed":
return False
return None
权限钩子的关键规则
permission_query_conditions仅适用于get_list,绝不适用于get_allhas_permission只能拒绝访问——返回 True 并不会授予额外权限- 始终处理
user=None的情况,默认使用frappe.session.user
工作流 5:资源注入和 doctype_js
添加全局 JS/CSS
# hooks.py
app_include_js = "/assets/myapp/js/myapp.min.js" # Desk
app_include_css = "/assets/myapp/css/myapp.min.css" # Desk
web_include_js = "/assets/myapp/js/portal.min.js" # Portal
web_include_css = "/assets/myapp/css/portal.min.css" # Portal
扩展特定表单
# hooks.py
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js"
}
// myapp/public/js/sales_invoice.js
frappe.ui.form.on("Sales Invoice", {
refresh(frm) {
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__("Custom Action"), () => {
frappe.call({
method: "myapp.api.custom_action",
args: { invoice: frm.doc.name },
freeze: true
});
}, __("Actions"));
}
}
});
更改 JS/CSS 文件后,始终运行 bench build --app myapp。
工作流 6:Fixtures、Boot Info 和网站钩子
Fixtures
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My App"]]}
]
绝不要在没有筛选条件的情况下导出 fixtures——这会捕获所有应用的自定义项。
extend_bootinfo
extend_bootinfo = "myapp.boot.extend_with_config"
def extend_with_config(bootinfo):
bootinfo.my_app = {"feature_enabled": True}
# NEVER send secrets — bootinfo is visible in browser DevTools
网站钩子
website_route_rules = [
{"from_route": "/shop/<category>", "to_route": "shop"}
]
portal_menu_items = [
{"title": "My Orders", "route": "/my-orders", "role": "Customer"}
]
on_login = "myapp.handlers.on_login"
on_logout = "myapp.handlers.on_logout"
迁移:在钩子、控制器和服务器脚本之间迁移逻辑
| 来源 | 目标 | 步骤 |
|---|---|---|
| 服务器脚本 → hooks.py | 1. 创建 Python 处理程序,2. 添加 doc_events,3. 禁用服务器脚本,4. 迁移 | |
| hooks.py → 控制器 | 1. 将逻辑移至 doctype .py,2. 删除 doc_events 条目,3. 迁移 | |
| 控制器 → hooks.py | 1. 创建事件模块,2. 添加 doc_events,3. 从控制器中移除,4. 迁移 |
务必在每次更改 hooks.py 后迁移:bench --site sitename migrate
处理程序签名速查
| 钩子 | 签名 |
|---|---|
| doc_events | def handler(doc, method=None): |
| 重命名事件 | def handler(doc, method, old, new, merge): |
| scheduler_events | def handler():(无参数) |
| extend_bootinfo | def handler(bootinfo): |
| permission_query | def handler(user): 返回 SQL 字符串 |
| has_permission | def handler(doc, user=None, permission_type=None): 返回 True/False/None |
| on_login | def handler(login_manager): |
| on_logout | def handler(): |
版本差异
| 功能 | V14 | V15 | V16 |
|---|---|---|---|
| doc_events | 是 | 是 | 是 |
| scheduler_events | 是 | 是 | 是 |
| override_doctype_class | 是 | 是 | 是 |
| extend_doctype_class | 否 | 否 | 是 |
| 权限钩子 | 是 | 是 | 是 |
| Scheduler tick 间隔 | ~4 分钟 | ~4 分钟 | ~60 秒 |
| auth_hooks | 否 | 是 | 是 |
参考文件
| 文件 | 内容 |
|---|---|
| decision-tree.md | 完整的钩子选择流程图 |
| workflows.md | 分步实现模式 |
| examples.md | 所有钩子类型的可运行代码示例 |
评论
支持 Markdown