erpnext-syntax-hooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Syntax: Hooks (hooks.py)
ERPNext 语法:Hooks (hooks.py)
Hooks in hooks.py enable custom apps to extend Frappe/ERPNext functionality.
hooks.py中的Hooks允许自定义应用扩展Frappe/ERPNext的功能。
Quick Reference
快速参考
doc_events - Document Lifecycle
doc_events - 文档生命周期
python
undefinedpython
undefinedIn hooks.py
In hooks.py
doc_events = {
"*": {
"after_insert": "myapp.events.log_all_inserts"
},
"Sales Invoice": {
"validate": "myapp.events.si_validate",
"on_submit": "myapp.events.si_on_submit"
}
}
```pythondoc_events = {
"*": {
"after_insert": "myapp.events.log_all_inserts"
},
"Sales Invoice": {
"validate": "myapp.events.si_validate",
"on_submit": "myapp.events.si_on_submit"
}
}
```pythonIn myapp/events.py
In myapp/events.py
import frappe
def si_validate(doc, method=None):
"""doc = document object, method = event name"""
if doc.grand_total < 0:
frappe.throw("Total cannot be negative")
undefinedimport frappe
def si_validate(doc, method=None):
"""doc = document object, method = event name"""
if doc.grand_total < 0:
frappe.throw("Total cannot be negative")
undefinedscheduler_events - Periodic Tasks
scheduler_events - 周期性任务
python
undefinedpython
undefinedIn hooks.py
In hooks.py
scheduler_events = {
"daily": ["myapp.tasks.daily_cleanup"],
"hourly_long": ["myapp.tasks.heavy_sync"],
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_morning"]
}
}
```pythonscheduler_events = {
"daily": ["myapp.tasks.daily_cleanup"],
"hourly_long": ["myapp.tasks.heavy_sync"],
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_morning"]
}
}
```pythonIn myapp/tasks.py
In myapp/tasks.py
def daily_cleanup():
"""No arguments - called automatically"""
frappe.db.delete("Log", {"creation": ["<", one_month_ago()]})
undefineddef daily_cleanup():
"""No arguments - called automatically"""
frappe.db.delete("Log", {"creation": ["<", one_month_ago()]})
undefinedextend_bootinfo - Client Data Injection
extend_bootinfo - 客户端数据注入
python
undefinedpython
undefinedIn hooks.py
In hooks.py
extend_bootinfo = "myapp.boot.extend_boot"
```pythonextend_bootinfo = "myapp.boot.extend_boot"
```pythonIn myapp/boot.py
In myapp/boot.py
def extend_boot(bootinfo):
"""bootinfo = dict that goes to frappe.boot"""
bootinfo.my_setting = frappe.get_single("My Settings").value
```javascript
// Client-side
console.log(frappe.boot.my_setting);def extend_boot(bootinfo):
"""bootinfo = dict that goes to frappe.boot"""
bootinfo.my_setting = frappe.get_single("My Settings").value
```javascript
// Client-side
console.log(frappe.boot.my_setting);Most Used doc_events
最常用的doc_events
| Event | When | Use Case |
|---|---|---|
| Before every save | Validation, calculations |
| After every save | Notifications, sync |
| After new doc | Creation-only actions |
| After submit | Ledger entries |
| After cancel | Reverse entries |
| Before delete | Cleanup |
Complete list: See doc-events.md
| 事件 | 触发时机 | 使用场景 |
|---|---|---|
| 每次保存前 | 验证、计算 |
| 每次保存后 | 通知、同步 |
| 新文档创建后 | 仅创建时执行的操作 |
| 提交后 | 分类账分录 |
| 取消后 | 反向分录 |
| 删除前 | 清理操作 |
完整列表: 参见 doc-events.md
Scheduler Event Types
调度事件类型
| Event | Frequency | Queue/Timeout |
|---|---|---|
| Every hour | default / 5 min |
| Every day | default / 5 min |
| Every week | default / 5 min |
| Every month | default / 5 min |
| Every hour | long / 25 min |
| Every day | long / 25 min |
| Custom timing | default / 5 min |
Cron syntax and examples: See scheduler-events.md
| 事件 | 频率 | 队列/超时时间 |
|---|---|---|
| 每小时 | default / 5分钟 |
| 每天 | default / 5分钟 |
| 每周 | default / 5分钟 |
| 每月 | default / 5分钟 |
| 每小时 | long / 25分钟 |
| 每天 | long / 25分钟 |
| 自定义时间 | default / 5分钟 |
Cron语法及示例: 参见 scheduler-events.md
Critical Rules
重要规则
1. bench migrate after scheduler changes
1. 调度任务变更后执行bench migrate
bash
undefinedbash
undefinedREQUIRED - otherwise changes won't be picked up
REQUIRED - otherwise changes won't be picked up
bench --site sitename migrate
undefinedbench --site sitename migrate
undefined2. No commits in doc_events
2. doc_events中不要执行commit
python
undefinedpython
undefined❌ WRONG
❌ WRONG
def on_update(doc, method=None):
frappe.db.commit() # Breaks transaction
def on_update(doc, method=None):
frappe.db.commit() # Breaks transaction
✅ CORRECT - Frappe commits automatically
✅ CORRECT - Frappe commits automatically
def on_update(doc, method=None):
update_related_docs(doc)
undefineddef on_update(doc, method=None):
update_related_docs(doc)
undefined3. Changes after on_update via db_set
3. on_update后通过db_set修改数据
python
undefinedpython
undefined❌ WRONG - change is lost
❌ WRONG - change is lost
def on_update(doc, method=None):
doc.status = "Processed"
def on_update(doc, method=None):
doc.status = "Processed"
✅ CORRECT
✅ CORRECT
def on_update(doc, method=None):
frappe.db.set_value(doc.doctype, doc.name, "status", "Processed")
undefineddef on_update(doc, method=None):
frappe.db.set_value(doc.doctype, doc.name, "status", "Processed")
undefined4. Heavy tasks to _long queue
4. 重型任务使用_long队列
python
undefinedpython
undefined❌ WRONG - timeout after 5 min
❌ WRONG - timeout after 5 min
scheduler_events = {
"daily": ["myapp.tasks.process_all_records"] # May take 20 min
}
scheduler_events = {
"daily": ["myapp.tasks.process_all_records"] # May take 20 min
}
✅ CORRECT - 25 min timeout
✅ CORRECT - 25 min timeout
scheduler_events = {
"daily_long": ["myapp.tasks.process_all_records"]
}
undefinedscheduler_events = {
"daily_long": ["myapp.tasks.process_all_records"]
}
undefined5. Tasks receive no arguments
5. 任务不接收参数
python
undefinedpython
undefined❌ WRONG
❌ WRONG
def my_task(some_arg):
pass
def my_task(some_arg):
pass
✅ CORRECT
✅ CORRECT
def my_task():
# Fetch data inside the function
pass
---def my_task():
# Fetch data inside the function
pass
---Cron Syntax Cheatsheet
Cron语法速查表
* * * * *
│ │ │ │ │
│ │ │ │ └── Day of week (0-6, Sun=0)
│ │ │ └──── Month (1-12)
│ │ └────── Day of month (1-31)
│ └──────── Hour (0-23)
└────────── Minute (0-59)| Pattern | Meaning |
|---|---|
| Every 5 minutes |
| Daily at 09:00 |
| Weekdays at 09:00 |
| First day of month |
| Friday at 17:00 |
* * * * *
│ │ │ │ │
│ │ │ │ └── Day of week (0-6, Sun=0)
│ │ │ └──── Month (1-12)
│ │ └────── Day of month (1-31)
│ └──────── Hour (0-23)
└────────── Minute (0-59)| 模式 | 含义 |
|---|---|
| 每5分钟一次 |
| 每天09:00执行 |
| 工作日09:00执行 |
| 每月第一天执行 |
| 周五17:00执行 |
doc_events vs Controller Hooks
doc_events vs 控制器Hooks
| Aspect | doc_events (hooks.py) | Controller Methods |
|---|---|---|
| Location | | |
| Scope | Hook OTHER doctypes | Only OWN doctype |
| Multiple handlers | ✅ Yes (list) | ❌ No |
| Priority | After controller | First |
Wildcard ( | ✅ Yes | ❌ No |
Use doc_events when:
- Hooking other apps' DocTypes from your custom app
- Reacting to ALL DocTypes (wildcard)
- Registering multiple handlers
Use controller methods when:
- Working on your own DocType
- You want full lifecycle control
| 维度 | doc_events (hooks.py) | 控制器方法 |
|---|---|---|
| 位置 | | |
| 范围 | 钩住其他文档类型 | 仅自己的文档类型 |
| 多个处理器 | ✅ 支持(列表形式) | ❌ 不支持 |
| 优先级 | 控制器之后执行 | 最先执行 |
通配符( | ✅ 支持 | ❌ 不支持 |
使用doc_events的场景:
- 从自定义应用中钩住其他应用的DocType
- 响应所有DocType(通配符)
- 注册多个处理器
使用控制器方法的场景:
- 处理自己的DocType
- 需要完整的生命周期控制
Reference Files
参考文件
| File | Contents |
|---|---|
| doc-events.md | All document events, signatures, execution order |
| scheduler-events.md | Scheduler types, cron syntax, timeouts |
| bootinfo.md | extend_bootinfo, session hooks |
| overrides.md | Override and extend patterns |
| permissions.md | Permission hooks |
| fixtures.md | Fixtures configuration |
| examples.md | Complete hooks.py examples |
| anti-patterns.md | Mistakes and corrections |
| 文件 | 内容 |
|---|---|
| doc-events.md | 所有文档事件、签名、执行顺序 |
| scheduler-events.md | 调度任务类型、Cron语法、超时时间 |
| bootinfo.md | extend_bootinfo、会话Hooks |
| overrides.md | 重写和扩展模式 |
| permissions.md | 权限Hooks |
| fixtures.md | 固定数据配置 |
| examples.md | 完整的hooks.py示例 |
| anti-patterns.md | 常见错误及修正 |
Configuration Hooks
配置Hooks
Override DocType Controller
重写DocType控制器
python
undefinedpython
undefinedIn hooks.py
In hooks.py
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
```pythonoverride_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
```pythonIn myapp/overrides.py
In myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # CRITICAL: always call super()!
self.custom_validation()
**Warning**: Last installed app wins when multiple apps override the same DocType.from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # CRITICAL: always call super()!
self.custom_validation()
**警告**: 当多个应用重写同一个DocType时,最后安装的应用生效。Override Whitelisted Methods
重写白名单方法
python
undefinedpython
undefinedIn hooks.py
In hooks.py
override_whitelisted_methods = {
"frappe.client.get_count": "myapp.overrides.custom_get_count"
}
```pythonoverride_whitelisted_methods = {
"frappe.client.get_count": "myapp.overrides.custom_get_count"
}
```pythonMethod signature MUST be identical to original!
Method signature MUST be identical to original!
def custom_get_count(doctype, filters=None, debug=False, cache=False):
# Custom implementation
return frappe.db.count(doctype, filters)
undefineddef custom_get_count(doctype, filters=None, debug=False, cache=False):
# Custom implementation
return frappe.db.count(doctype, filters)
undefinedPermission Hooks
权限Hooks
python
undefinedpython
undefinedIn hooks.py
In hooks.py
permission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query_conditions"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_has_permission"
}
```pythonpermission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query_conditions"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_has_permission"
}
```pythonIn myapp/permissions.py
In myapp/permissions.py
def si_query_conditions(user):
"""Returns SQL WHERE fragment for list filtering"""
if not user:
user = frappe.session.user
if "Sales Manager" in frappe.get_roles(user):
return "" # No restrictions
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"def si_has_permission(doc, user=None, permission_type=None):
"""Document-level permission check"""
if permission_type == "write" and doc.status == "Closed":
return False
return None # Fallback to default
**Note**: `permission_query_conditions` only works with `get_list`, NOT with `get_all`!def si_query_conditions(user):
"""Returns SQL WHERE fragment for list filtering"""
if not user:
user = frappe.session.user
if "Sales Manager" in frappe.get_roles(user):
return "" # No restrictions
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"def si_has_permission(doc, user=None, permission_type=None):
"""Document-level permission check"""
if permission_type == "write" and doc.status == "Closed":
return False
return None # Fallback to default
**注意**: `permission_query_conditions`仅适用于`get_list`,不适用于`get_all`!Fixtures
固定数据(Fixtures)
python
undefinedpython
undefinedIn hooks.py
In hooks.py
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My App"]]},
{"dt": "Role", "filters": [["name", "like", "MyApp%"]]}
]
```bashfixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My App"]]},
{"dt": "Role", "filters": [["name", "like", "MyApp%"]]}
]
```bashExport fixtures to JSON
Export fixtures to JSON
bench --site sitename export-fixtures
undefinedbench --site sitename export-fixtures
undefinedAsset Includes
资源引入
python
undefinedpython
undefinedIn hooks.py
In hooks.py
Desk (backend) assets
Desk(后端)资源
app_include_js = "/assets/myapp/js/myapp.min.js"
app_include_css = "/assets/myapp/css/myapp.min.css"
app_include_js = "/assets/myapp/js/myapp.min.js"
app_include_css = "/assets/myapp/css/myapp.min.css"
Website/Portal assets
Website/Portal资源
web_include_js = "/assets/myapp/js/web.min.js"
web_include_css = "/assets/myapp/css/web.min.css"
web_include_js = "/assets/myapp/js/web.min.js"
web_include_css = "/assets/myapp/css/web.min.css"
Form script extensions
表单脚本扩展
doctype_js = {
"Sales Invoice": "public/js/sales_invoice.js"
}
undefineddoctype_js = {
"Sales Invoice": "public/js/sales_invoice.js"
}
undefinedInstall/Migrate Hooks
安装/迁移Hooks
python
undefinedpython
undefinedIn hooks.py
In hooks.py
after_install = "myapp.setup.after_install"
after_migrate = "myapp.setup.after_migrate"
```pythonafter_install = "myapp.setup.after_install"
after_migrate = "myapp.setup.after_migrate"
```pythonIn myapp/setup.py
In myapp/setup.py
def after_install():
create_default_roles()
def after_migrate():
clear_custom_cache()
---def after_install():
create_default_roles()
def after_migrate():
clear_custom_cache()
---Complete Decision Tree
完整决策树
What do you want to achieve?
│
├─► REACT to document events from OTHER apps?
│ └─► doc_events
│
├─► Run PERIODIC tasks?
│ └─► scheduler_events
│ ├─► < 5 min → hourly/daily/weekly/monthly
│ ├─► > 5 min → hourly_long/daily_long/etc.
│ └─► Specific time → cron
│
├─► Send DATA to CLIENT at page load?
│ └─► extend_bootinfo
│
├─► Modify CONTROLLER of existing DocType?
│ ├─► Frappe v16+ → extend_doctype_class (recommended)
│ └─► Frappe v14/v15 → override_doctype_class
│
├─► Modify API ENDPOINT?
│ └─► override_whitelisted_methods
│
├─► Customize PERMISSIONS?
│ ├─► List filtering → permission_query_conditions
│ └─► Document-level → has_permission
│
├─► EXPORT/IMPORT configuration?
│ └─► fixtures
│
├─► ADD JS/CSS to desk or portal?
│ ├─► Desk → app_include_js/css
│ ├─► Portal → web_include_js/css
│ └─► Form specific → doctype_js
│
└─► SETUP on install/migrate?
└─► after_install, after_migrateWhat do you want to achieve?
│
├─► 响应其他应用的文档事件?
│ └─► doc_events
│
├─► 执行周期性任务?
│ └─► scheduler_events
│ ├─► <5分钟 → hourly/daily/weekly/monthly
│ ├─► >5分钟 → hourly_long/daily_long等
│ └─► 特定时间 → cron
│
├─► 页面加载时向客户端发送数据?
│ └─► extend_bootinfo
│
├─► 修改现有DocType的控制器?
│ ├─► Frappe v16+ → extend_doctype_class(推荐)
│ └─► Frappe v14/v15 → override_doctype_class
│
├─► 修改API端点?
│ └─► override_whitelisted_methods
│
├─► 自定义权限?
│ ├─► 列表过滤 → permission_query_conditions
│ └─► 文档级校验 → has_permission
│
├─► 导出/导入配置?
│ └─► fixtures
│
├─► 向后台或门户添加JS/CSS?
│ ├─► 后台 → app_include_js/css
│ ├─► 门户 → web_include_js/css
│ └─► 特定表单 → doctype_js
│
└─► 安装/迁移时执行设置?
└─► after_install, after_migrateVersion Differences
版本差异
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| doc_events | ✅ | ✅ | ✅ |
| scheduler_events | ✅ | ✅ | ✅ |
| extend_bootinfo | ✅ | ✅ | ✅ |
| override_doctype_class | ✅ | ✅ | ✅ |
| extend_doctype_class | ❌ | ❌ | ✅ |
| permission_query_conditions | ✅ | ✅ | ✅ |
| has_permission | ✅ | ✅ | ✅ |
| fixtures | ✅ | ✅ | ✅ |
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
| doc_events | ✅ | ✅ | ✅ |
| scheduler_events | ✅ | ✅ | ✅ |
| extend_bootinfo | ✅ | ✅ | ✅ |
| override_doctype_class | ✅ | ✅ | ✅ |
| extend_doctype_class | ❌ | ❌ | ✅ |
| permission_query_conditions | ✅ | ✅ | ✅ |
| has_permission | ✅ | ✅ | ✅ |
| fixtures | ✅ | ✅ | ✅ |
Anti-Patterns Summary
反模式总结
| ❌ Wrong | ✅ Correct |
|---|---|
| Frappe commits automatically |
| |
Heavy task in | Use |
| Change scheduler without migrate | Always |
| Sensitive data in bootinfo | Only public config |
Override without | Always |
| Use |
| Fixtures without filters | Filter by module/app |
Full anti-patterns: See anti-patterns.md
| ❌ 错误做法 | ✅ 正确做法 |
|---|---|
| Frappe会自动执行commit |
on_update中使用 | 使用 |
重型任务放在 | 使用 |
| 修改调度任务后不执行migrate | 必须执行 |
| 敏感数据放入bootinfo | 仅放入公开配置 |
重写时不调用 | 必须先调用 |
结合 | 使用 |
| 固定数据不设置过滤条件 | 按模块/应用设置过滤条件 |
完整反模式: 参见 anti-patterns.md