erpnext-impl-hooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Hooks - Implementation
ERPNext Hooks - 实现指南
This skill helps you determine HOW to implement hooks.py configurations. For exact syntax, see .
erpnext-syntax-hooksVersion: v14/v15/v16 compatible (with V16-specific features noted)
本技能可帮助你确定如何实现hooks.py配置。如需具体语法,请参考。
erpnext-syntax-hooks版本兼容:v14/v15/v16兼容(标注了V16专属特性)
Main Decision: What Are You Trying to Do?
核心决策:你想要实现什么?
┌─────────────────────────────────────────────────────────────────────────┐
│ WHAT DO YOU WANT TO ACHIEVE? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► React to document events on OTHER apps' DocTypes? │
│ └── doc_events in hooks.py │
│ │
│ ► Run code periodically (hourly, daily, custom schedule)? │
│ └── scheduler_events │
│ │
│ ► Modify behavior of existing DocType controller? │
│ ├── V16+: extend_doctype_class (RECOMMENDED - multiple apps work) │
│ └── V14/V15: override_doctype_class (last app wins) │
│ │
│ ► Modify existing API endpoint behavior? │
│ └── override_whitelisted_methods │
│ │
│ ► Add custom permission logic? │
│ ├── List filtering: permission_query_conditions │
│ └── Document-level: has_permission │
│ │
│ ► Send data to client on page load? │
│ └── extend_bootinfo │
│ │
│ ► Export/import configuration between sites? │
│ └── fixtures │
│ │
│ ► Add JS/CSS to desk or portal? │
│ ├── Desk: app_include_js/css │
│ ├── Portal: web_include_js/css │
│ └── Specific form: doctype_js │
│ │
└─────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 你想要实现什么? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► 响应其他应用中DocType的文档事件? │
│ └── 在hooks.py中使用doc_events │
│ │
│ ► 定期运行代码(每小时、每天、自定义调度)? │
│ └── 使用scheduler_events │
│ │
│ ► 修改现有DocType控制器的行为? │
│ ├── V16及以上版本:extend_doctype_class(推荐 - 支持多应用协作) │
│ └── V14/V15版本:override_doctype_class(最后安装的应用生效) │
│ │
│ ► 修改现有API端点的行为? │
│ └── 使用override_whitelisted_methods │
│ │
│ ► 添加自定义权限逻辑? │
│ ├── 列表过滤:permission_query_conditions │
│ └── 文档级控制:has_permission │
│ │
│ ► 在页面加载时向客户端发送数据? │
│ └── 使用extend_bootinfo │
│ │
│ ► 在站点间导出/导入配置? │
│ └── 使用fixtures │
│ │
│ ► 向工作台或门户添加JS/CSS? │
│ ├── 工作台:app_include_js/css │
│ ├── 门户:web_include_js/css │
│ └── 特定表单:doctype_js │
│ │
└─────────────────────────────────────────────────────────────────────────┘Decision Tree: doc_events vs Controller Methods
决策树:doc_events 与 控制器方法的对比
WHERE IS THE DOCTYPE?
│
├─► DocType is in YOUR custom app?
│ └─► Use controller methods (doctype/xxx/xxx.py)
│ - Direct control over lifecycle
│ - Cleaner code organization
│
├─► DocType is in ANOTHER app (ERPNext, Frappe)?
│ └─► Use doc_events in hooks.py
│ - Only way to hook external DocTypes
│ - Can register multiple handlers
│
└─► Need to hook ALL DocTypes (logging, audit)?
└─► Use doc_events with wildcard "*"Rule: Controller methods for YOUR DocTypes, doc_events for OTHER apps' DocTypes.
DocType属于哪里?
│
├─► DocType在你的自定义应用中?
│ └─► 使用控制器方法(doctype/xxx/xxx.py)
│ - 直接控制生命周期
│ - 代码组织更清晰
│
├─► DocType在其他应用中(ERPNext、Frappe)?
│ └── 在hooks.py中使用doc_events
│ - 这是挂钩外部DocType的唯一方式
│ - 可注册多个处理程序
│
└─► 需要挂钩所有DocType(日志、审计)?
└── 使用带通配符"*"的doc_events规则:自定义应用的DocType使用控制器方法,其他应用的DocType使用doc_events。
Decision Tree: Which doc_event?
决策树:选择哪种doc_event?
WHAT DO YOU NEED TO DO?
│
├─► Validate data or calculate fields?
│ ├─► Before any save → validate
│ └─► Only on new documents → before_insert
│
├─► React after document is saved?
│ ├─► Only first save → after_insert
│ ├─► Every save → on_update
│ └─► ANY change (including db_set) → on_change
│
├─► Handle submittable documents?
│ ├─► Before submit → before_submit
│ ├─► After submit → on_submit (ledger entries here)
│ ├─► Before cancel → before_cancel
│ └─► After cancel → on_cancel (reverse entries here)
│
├─► Handle document deletion?
│ ├─► Before delete (can prevent) → on_trash
│ └─► After delete (cleanup) → after_delete
│
└─► Handle document rename?
├─► Before rename → before_rename
└─► After rename → after_rename你需要实现什么功能?
│
├─► 验证数据或计算字段?
│ ├─► 保存前验证 → validate
│ └─► 仅针对新文档 → before_insert
│
├─► 文档保存后执行操作?
│ ├─► 仅首次保存后 → after_insert
│ ├─► 每次保存后 → on_update
│ └─► 任何变更后(包括db_set) → on_change
│
├─► 处理可提交的文档?
│ ├─► 提交前 → before_submit
│ ├─► 提交后 → on_submit(在此处理分类账条目)
│ ├─► 取消前 → before_cancel
│ └─► 取消后 → on_cancel(在此处理反向条目)
│
├─► 处理文档删除?
│ ├─► 删除前(可阻止删除) → on_trash
│ └─► 删除后(清理操作) → after_delete
│
└─► 处理文档重命名?
├─► 重命名前 → before_rename
└─► 重命名后 → after_renameDecision Tree: Scheduler Event Type
决策树:调度事件类型选择
HOW LONG DOES YOUR TASK RUN?
│
├─► < 5 minutes
│ │
│ │ HOW OFTEN?
│ ├─► Every ~60 seconds → all
│ ├─► Every hour → hourly
│ ├─► Every day → daily
│ ├─► Every week → weekly
│ ├─► Every month → monthly
│ └─► Specific time → cron
│
└─► > 5 minutes (up to 25 minutes)
│
│ HOW OFTEN?
├─► Every hour → hourly_long
├─► Every day → daily_long
├─► Every week → weekly_long
└─► Every month → monthly_long
⚠️ Tasks > 25 minutes: Split into chunks or use background jobs你的任务运行时长?
│
├─► 小于5分钟
│ │
│ │ 运行频率?
│ ├─► 约每60秒一次 → all
│ ├─► 每小时一次 → hourly
│ ├─► 每天一次 → daily
│ ├─► 每周一次 → weekly
│ ├─► 每月一次 → monthly
│ └─► 特定时间 → cron
│
└─► 5-25分钟
│
│ 运行频率?
├─► 每小时一次 → hourly_long
├─► 每天一次 → daily_long
├─► 每周一次 → weekly_long
└─► 每月一次 → monthly_long
⚠️ 超过25分钟的任务:拆分为多个子任务或使用后台作业Decision Tree: Override vs Extend (V16)
决策树:覆盖与扩展(V16版本)
FRAPPE VERSION?
│
├─► V16+
│ │
│ │ WHAT DO YOU NEED?
│ ├─► Add methods/properties to DocType?
│ │ └─► extend_doctype_class (RECOMMENDED)
│ │ - Multiple apps can extend same DocType
│ │ - Safer, less breakage on updates
│ │
│ └─► Completely replace controller logic?
│ └─► override_doctype_class (use sparingly)
│
└─► V14/V15
└─► override_doctype_class (only option)
⚠️ Last installed app wins!
⚠️ Always call super() in methods!Frappe版本?
│
├─► V16及以上
│ │
│ │ 你需要实现什么?
│ ├─► 为DocType添加方法/属性?
│ │ └─► extend_doctype_class(推荐)
│ │ - 多个应用可扩展同一个DocType
│ │ - 更安全,版本更新时更少出现中断
│ │
│ └─► 完全替换控制器逻辑?
│ └─► override_doctype_class(谨慎使用)
│
└─► V14/V15
└─► override_doctype_class(唯一选项)
⚠️ 最后安装的应用生效!
⚠️ 务必在方法中调用super()!Implementation Workflow: doc_events
实现工作流:doc_events
Step 1: Add to hooks.py
步骤1:添加到hooks.py
python
undefinedpython
undefinedmyapp/hooks.py
myapp/hooks.py
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.sales_invoice.validate",
"on_submit": "myapp.events.sales_invoice.on_submit"
}
}
undefineddoc_events = {
"Sales Invoice": {
"validate": "myapp.events.sales_invoice.validate",
"on_submit": "myapp.events.sales_invoice.on_submit"
}
}
undefinedStep 2: Create handler module
步骤2:创建处理程序模块
python
undefinedpython
undefinedmyapp/events/sales_invoice.py
myapp/events/sales_invoice.py
import frappe
def validate(doc, method=None):
"""
Args:
doc: The document object
method: Event name ("validate")
Changes to doc ARE saved (before save event)
"""
if doc.grand_total < 0:
frappe.throw("Total cannot be negative")
# Calculate custom field
doc.custom_margin = doc.grand_total - doc.total_costdef on_submit(doc, method=None):
"""
After submit - document already saved
Use frappe.db.set_value for additional changes
"""
create_external_record(doc)
undefinedimport frappe
def validate(doc, method=None):
"""
Args:
doc: 文档对象
method: 事件名称("validate")
对doc的修改会被保存(保存前事件)
"""
if doc.grand_total < 0:
frappe.throw("总金额不能为负数")
# 计算自定义字段
doc.custom_margin = doc.grand_total - doc.total_costdef on_submit(doc, method=None):
"""
提交后 - 文档已保存
使用frappe.db.set_value进行额外修改
"""
create_external_record(doc)
undefinedStep 3: Deploy
步骤3:部署
bash
bench --site sitename migratebash
bench --site sitename migrateImplementation Workflow: scheduler_events
实现工作流:scheduler_events
Step 1: Add to hooks.py
步骤1:添加到hooks.py
python
undefinedpython
undefinedmyapp/hooks.py
myapp/hooks.py
scheduler_events = {
"daily": ["myapp.tasks.daily_cleanup"],
"daily_long": ["myapp.tasks.heavy_processing"],
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_report"]
}
}
undefinedscheduler_events = {
"daily": ["myapp.tasks.daily_cleanup"],
"daily_long": ["myapp.tasks.heavy_processing"],
"cron": {
"0 9 * * 1-5": ["myapp.tasks.weekday_report"]
}
}
undefinedStep 2: Create task module
步骤2:创建任务模块
python
undefinedpython
undefinedmyapp/tasks.py
myapp/tasks.py
import frappe
def daily_cleanup():
"""NO arguments - scheduler calls with no args"""
old_logs = frappe.get_all(
"Error Log",
filters={"creation": ["<", frappe.utils.add_days(None, -30)]},
pluck="name"
)
for name in old_logs:
frappe.delete_doc("Error Log", name)
def heavy_processing():
"""Long task - use _long variant in hooks"""
for batch in get_batches():
process_batch(batch)
frappe.db.commit() # Commit per batch for long tasks
undefinedimport frappe
def daily_cleanup():
"""无参数 - 调度器调用时不传入参数"""
old_logs = frappe.get_all(
"Error Log",
filters={"creation": ["<", frappe.utils.add_days(None, -30)]},
pluck="name"
)
for name in old_logs:
frappe.delete_doc("Error Log", name)
def heavy_processing():
"""长任务 - 在hooks中使用_long变体"""
for batch in get_batches():
process_batch(batch)
frappe.db.commit() # 长任务中每处理一个批次就提交一次
undefinedStep 3: Deploy and verify
步骤3:部署并验证
bash
bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler statusbash
bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler statusImplementation Workflow: extend_doctype_class (V16+)
实现工作流:extend_doctype_class(V16+)
Step 1: Add to hooks.py
步骤1:添加到hooks.py
python
undefinedpython
undefinedmyapp/hooks.py
myapp/hooks.py
extend_doctype_class = {
"Sales Invoice": ["myapp.extensions.SalesInvoiceMixin"]
}
undefinedextend_doctype_class = {
"Sales Invoice": ["myapp.extensions.SalesInvoiceMixin"]
}
undefinedStep 2: Create mixin class
步骤2:创建Mixin类
python
undefinedpython
undefinedmyapp/extensions.py
myapp/extensions.py
import frappe
from frappe.model.document import Document
class SalesInvoiceMixin(Document):
"""Mixin that extends Sales Invoice"""
@property
def profit_margin(self):
"""Add computed property"""
if self.grand_total:
return ((self.grand_total - self.total_cost) / self.grand_total) * 100
return 0
def validate(self):
"""Extend validation - ALWAYS call super()"""
super().validate()
self.validate_margin()
def validate_margin(self):
"""Custom validation logic"""
if self.profit_margin < 10:
frappe.msgprint("Warning: Low margin invoice")undefinedimport frappe
from frappe.model.document import Document
class SalesInvoiceMixin(Document):
"""扩展Sales Invoice的Mixin类"""
@property
def profit_margin(self):
"""添加计算属性"""
if self.grand_total:
return ((self.grand_total - self.total_cost) / self.grand_total) * 100
return 0
def validate(self):
"""扩展验证逻辑 - 务必调用super()"""
super().validate()
self.validate_margin()
def validate_margin(self):
"""自定义验证逻辑"""
if self.profit_margin < 10:
frappe.msgprint("警告:利润率过低的发票")undefinedStep 3: Deploy
步骤3:部署
bash
bench --site sitename migratebash
bench --site sitename migrateImplementation Workflow: Permission Hooks
实现工作流:权限钩子
Step 1: Add to hooks.py
步骤1:添加到hooks.py
python
undefinedpython
undefinedmyapp/hooks.py
myapp/hooks.py
permission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_permission"
}
undefinedpermission_query_conditions = {
"Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
"Sales Invoice": "myapp.permissions.si_permission"
}
undefinedStep 2: Create permission handlers
步骤2:创建权限处理程序
python
undefinedpython
undefinedmyapp/permissions.py
myapp/permissions.py
import frappe
def si_query(user):
"""
Returns SQL WHERE clause for list filtering.
ONLY works with get_list, NOT get_all!
"""
if not user:
user = frappe.session.user
if "Sales Manager" in frappe.get_roles(user):
return "" # No filter - see all
# Regular users see only their own
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"def si_permission(doc, user=None, permission_type=None):
"""
Document-level permission check.
Return: True (allow), False (deny), None (use default)
NOTE: Can only DENY, not grant additional permissions!
"""
if permission_type == "write" and doc.status == "Closed":
return False # Deny write on closed invoices
return None # Use default permission system
---import frappe
def si_query(user):
"""
返回用于列表过滤的SQL WHERE子句。
仅适用于get_list,不适用于get_all!
"""
if not user:
user = frappe.session.user
if "Sales Manager" in frappe.get_roles(user):
return "" # 无过滤 - 查看所有数据
# 普通用户仅能查看自己创建的发票
return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"def si_permission(doc, user=None, permission_type=None):
"""
文档级权限检查。
返回值:True(允许)、False(拒绝)、None(使用默认逻辑)
注意:仅能拒绝权限,无法额外授予权限!
"""
if permission_type == "write" and doc.status == "Closed":
return False # 拒绝修改已关闭的发票
return None # 使用默认权限系统
---Quick Reference: Handler Signatures
快速参考:处理程序签名
| Hook | Signature |
|---|---|
| doc_events | |
| rename events | |
| scheduler_events | |
| extend_bootinfo | |
| permission_query | |
| has_permission | |
| override methods | Must match original signature exactly |
| 钩子 | 签名 |
|---|---|
| doc_events | |
| 重命名事件 | |
| scheduler_events | |
| extend_bootinfo | |
| permission_query | |
| has_permission | |
| 覆盖方法 | 必须与原方法签名完全一致 |
Critical Rules
关键规则
1. Never commit in doc_events
1. 切勿在doc_events中提交事务
python
undefinedpython
undefined❌ WRONG - breaks transaction
❌ 错误 - 破坏事务流程
def on_update(doc, method=None):
frappe.db.commit()
def on_update(doc, method=None):
frappe.db.commit()
✅ CORRECT - Frappe commits automatically
✅ 正确 - Frappe会自动提交
def on_update(doc, method=None):
update_related(doc)
undefineddef on_update(doc, method=None):
update_related(doc)
undefined2. Use db_set_value after on_update
2. 在on_update后使用db_set_value
python
undefinedpython
undefined❌ WRONG - change is lost
❌ 错误 - 修改会丢失
def on_update(doc, method=None):
doc.status = "Processed"
def on_update(doc, method=None):
doc.status = "Processed"
✅ 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")
undefined3. Always call super() in overrides
3. 在覆盖方法中务必调用super()
python
undefinedpython
undefined❌ WRONG - breaks core functionality
❌ 错误 - 破坏核心功能
class CustomInvoice(SalesInvoice):
def validate(self):
self.my_validation()
class CustomInvoice(SalesInvoice):
def validate(self):
self.my_validation()
✅ CORRECT
✅ 正确
class CustomInvoice(SalesInvoice):
def validate(self):
super().validate() # FIRST!
self.my_validation()
undefinedclass CustomInvoice(SalesInvoice):
def validate(self):
super().validate() # 务必先调用!
self.my_validation()
undefined4. Always migrate after hooks changes
4. 修改hooks后务必执行迁移
bash
undefinedbash
undefinedRequired after ANY hooks.py change
任何hooks.py修改后都需要执行
bench --site sitename migrate
undefinedbench --site sitename migrate
undefined5. permission_query only works with get_list
5. permission_query仅适用于get_list
python
undefinedpython
undefined❌ NOT filtered by permission_query_conditions
❌ 不会被permission_query_conditions过滤
frappe.db.get_all("Sales Invoice", filters={})
frappe.db.get_all("Sales Invoice", filters={})
✅ Filtered by permission_query_conditions
✅ 会被permission_query_conditions过滤
frappe.db.get_list("Sales Invoice", filters={})
---frappe.db.get_list("Sales Invoice", filters={})
---Version Differences
版本差异
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| doc_events | ✅ | ✅ | ✅ |
| scheduler_events | ✅ | ✅ | ✅ |
| override_doctype_class | ✅ | ✅ | ✅ |
| extend_doctype_class | ❌ | ❌ | ✅ |
| permission hooks | ✅ | ✅ | ✅ |
| Scheduler tick | 4 min | 4 min | 60 sec |
| 特性 | V14 | V15 | V16 |
|---|---|---|---|
| doc_events | ✅ | ✅ | ✅ |
| scheduler_events | ✅ | ✅ | ✅ |
| override_doctype_class | ✅ | ✅ | ✅ |
| extend_doctype_class | ❌ | ❌ | ✅ |
| 权限钩子 | ✅ | ✅ | ✅ |
| 调度器间隔 | 4分钟 | 4分钟 | 60秒 |
Reference Files
参考文件
| File | Contents |
|---|---|
| decision-tree.md | Complete hook selection flowcharts |
| workflows.md | Step-by-step implementation patterns |
| examples.md | Working code examples |
| anti-patterns.md | Common mistakes and solutions |
| 文件 | 内容 |
|---|---|
| decision-tree.md | 完整的钩子选择流程图 |
| workflows.md | 分步实现模式 |
| examples.md | 可运行的代码示例 |
| anti-patterns.md | 常见错误及解决方案 |