erpnext-syntax-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext 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
undefined
python
undefined

In 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" } }

```python
doc_events = { "*": { "after_insert": "myapp.events.log_all_inserts" }, "Sales Invoice": { "validate": "myapp.events.si_validate", "on_submit": "myapp.events.si_on_submit" } }

```python

In 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")
undefined
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")
undefined

scheduler_events - Periodic Tasks

scheduler_events - 周期性任务

python
undefined
python
undefined

In 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"] } }

```python
scheduler_events = { "daily": ["myapp.tasks.daily_cleanup"], "hourly_long": ["myapp.tasks.heavy_sync"], "cron": { "0 9 * * 1-5": ["myapp.tasks.weekday_morning"] } }

```python

In myapp/tasks.py

In myapp/tasks.py

def daily_cleanup(): """No arguments - called automatically""" frappe.db.delete("Log", {"creation": ["<", one_month_ago()]})
undefined
def daily_cleanup(): """No arguments - called automatically""" frappe.db.delete("Log", {"creation": ["<", one_month_ago()]})
undefined

extend_bootinfo - Client Data Injection

extend_bootinfo - 客户端数据注入

python
undefined
python
undefined

In hooks.py

In hooks.py

extend_bootinfo = "myapp.boot.extend_boot"

```python
extend_bootinfo = "myapp.boot.extend_boot"

```python

In 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

EventWhenUse Case
validate
Before every saveValidation, calculations
on_update
After every saveNotifications, sync
after_insert
After new docCreation-only actions
on_submit
After submitLedger entries
on_cancel
After cancelReverse entries
on_trash
Before deleteCleanup
Complete list: See doc-events.md

事件触发时机使用场景
validate
每次保存前验证、计算
on_update
每次保存后通知、同步
after_insert
新文档创建后仅创建时执行的操作
on_submit
提交后分类账分录
on_cancel
取消后反向分录
on_trash
删除前清理操作
完整列表: 参见 doc-events.md

Scheduler Event Types

调度事件类型

EventFrequencyQueue/Timeout
hourly
Every hourdefault / 5 min
daily
Every daydefault / 5 min
weekly
Every weekdefault / 5 min
monthly
Every monthdefault / 5 min
hourly_long
Every hourlong / 25 min
daily_long
Every daylong / 25 min
cron
Custom timingdefault / 5 min
Cron syntax and examples: See scheduler-events.md

事件频率队列/超时时间
hourly
每小时default / 5分钟
daily
每天default / 5分钟
weekly
每周default / 5分钟
monthly
每月default / 5分钟
hourly_long
每小时long / 25分钟
daily_long
每天long / 25分钟
cron
自定义时间default / 5分钟
Cron语法及示例: 参见 scheduler-events.md

Critical Rules

重要规则

1. bench migrate after scheduler changes

1. 调度任务变更后执行bench migrate

bash
undefined
bash
undefined

REQUIRED - otherwise changes won't be picked up

REQUIRED - otherwise changes won't be picked up

bench --site sitename migrate
undefined
bench --site sitename migrate
undefined

2. No commits in doc_events

2. doc_events中不要执行commit

python
undefined
python
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)
undefined
def on_update(doc, method=None): update_related_docs(doc)
undefined

3. Changes after on_update via db_set

3. on_update后通过db_set修改数据

python
undefined
python
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")
undefined
def on_update(doc, method=None): frappe.db.set_value(doc.doctype, doc.name, "status", "Processed")
undefined

4. Heavy tasks to _long queue

4. 重型任务使用_long队列

python
undefined
python
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"] }
undefined
scheduler_events = { "daily_long": ["myapp.tasks.process_all_records"] }
undefined

5. Tasks receive no arguments

5. 任务不接收参数

python
undefined
python
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)
PatternMeaning
*/5 * * * *
Every 5 minutes
0 9 * * *
Daily at 09:00
0 9 * * 1-5
Weekdays at 09:00
0 0 1 * *
First day of month
0 17 * * 5
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 * * * *
每5分钟一次
0 9 * * *
每天09:00执行
0 9 * * 1-5
工作日09:00执行
0 0 1 * *
每月第一天执行
0 17 * * 5
周五17:00执行

doc_events vs Controller Hooks

doc_events vs 控制器Hooks

Aspectdoc_events (hooks.py)Controller Methods
Location
hooks.py
doctype/xxx/xxx.py
ScopeHook OTHER doctypesOnly OWN doctype
Multiple handlers✅ Yes (list)❌ No
PriorityAfter controllerFirst
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)控制器方法
位置
hooks.py
doctype/xxx/xxx.py
范围钩住其他文档类型仅自己的文档类型
多个处理器✅ 支持(列表形式)❌ 不支持
优先级控制器之后执行最先执行
通配符(
*
)
✅ 支持❌ 不支持
使用doc_events的场景:
  • 从自定义应用中钩住其他应用的DocType
  • 响应所有DocType(通配符)
  • 注册多个处理器
使用控制器方法的场景:
  • 处理自己的DocType
  • 需要完整的生命周期控制

Reference Files

参考文件

FileContents
doc-events.mdAll document events, signatures, execution order
scheduler-events.mdScheduler types, cron syntax, timeouts
bootinfo.mdextend_bootinfo, session hooks
overrides.mdOverride and extend patterns
permissions.mdPermission hooks
fixtures.mdFixtures configuration
examples.mdComplete hooks.py examples
anti-patterns.mdMistakes and corrections

文件内容
doc-events.md所有文档事件、签名、执行顺序
scheduler-events.md调度任务类型、Cron语法、超时时间
bootinfo.mdextend_bootinfo、会话Hooks
overrides.md重写和扩展模式
permissions.md权限Hooks
fixtures.md固定数据配置
examples.md完整的hooks.py示例
anti-patterns.md常见错误及修正

Configuration Hooks

配置Hooks

Override DocType Controller

重写DocType控制器

python
undefined
python
undefined

In hooks.py

In hooks.py

override_doctype_class = { "Sales Invoice": "myapp.overrides.CustomSalesInvoice" }

```python
override_doctype_class = { "Sales Invoice": "myapp.overrides.CustomSalesInvoice" }

```python

In 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
undefined
python
undefined

In hooks.py

In hooks.py

override_whitelisted_methods = { "frappe.client.get_count": "myapp.overrides.custom_get_count" }

```python
override_whitelisted_methods = { "frappe.client.get_count": "myapp.overrides.custom_get_count" }

```python

Method 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)
undefined
def custom_get_count(doctype, filters=None, debug=False, cache=False): # Custom implementation return frappe.db.count(doctype, filters)
undefined

Permission Hooks

权限Hooks

python
undefined
python
undefined

In hooks.py

In hooks.py

permission_query_conditions = { "Sales Invoice": "myapp.permissions.si_query_conditions" } has_permission = { "Sales Invoice": "myapp.permissions.si_has_permission" }

```python
permission_query_conditions = { "Sales Invoice": "myapp.permissions.si_query_conditions" } has_permission = { "Sales Invoice": "myapp.permissions.si_has_permission" }

```python

In 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
undefined
python
undefined

In 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%"]]} ]

```bash
fixtures = [ {"dt": "Custom Field", "filters": [["module", "=", "My App"]]}, {"dt": "Property Setter", "filters": [["module", "=", "My App"]]}, {"dt": "Role", "filters": [["name", "like", "MyApp%"]]} ]

```bash

Export fixtures to JSON

Export fixtures to JSON

bench --site sitename export-fixtures
undefined
bench --site sitename export-fixtures
undefined

Asset Includes

资源引入

python
undefined
python
undefined

In 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" }
undefined
doctype_js = { "Sales Invoice": "public/js/sales_invoice.js" }
undefined

Install/Migrate Hooks

安装/迁移Hooks

python
undefined
python
undefined

In hooks.py

In hooks.py

after_install = "myapp.setup.after_install" after_migrate = "myapp.setup.after_migrate"

```python
after_install = "myapp.setup.after_install" after_migrate = "myapp.setup.after_migrate"

```python

In 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_migrate

What 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_migrate

Version Differences

版本差异

Featurev14v15v16
doc_events
scheduler_events
extend_bootinfo
override_doctype_class
extend_doctype_class
permission_query_conditions
has_permission
fixtures

功能v14v15v16
doc_events
scheduler_events
extend_bootinfo
override_doctype_class
extend_doctype_class
permission_query_conditions
has_permission
fixtures

Anti-Patterns Summary

反模式总结

❌ Wrong✅ Correct
frappe.db.commit()
in handler
Frappe commits automatically
doc.field = x
in on_update
frappe.db.set_value()
Heavy task in
daily
Use
daily_long
Change scheduler without migrateAlways
bench migrate
Sensitive data in bootinfoOnly public config
Override without
super()
Always
super().method()
first
get_all
with permission_query
Use
get_list
Fixtures without filtersFilter by module/app
Full anti-patterns: See anti-patterns.md
❌ 错误做法✅ 正确做法
frappe.db.commit()
在处理器中执行
Frappe会自动执行commit
on_update中使用
doc.field = x
使用
frappe.db.set_value()
重型任务放在
daily
队列
使用
daily_long
队列
修改调度任务后不执行migrate必须执行
bench migrate
敏感数据放入bootinfo仅放入公开配置
重写时不调用
super()
必须先调用
super().method()
结合
get_all
使用permission_query_conditions
使用
get_list
固定数据不设置过滤条件按模块/应用设置过滤条件
完整反模式: 参见 anti-patterns.md