erpnext-impl-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Hooks - Implementation

ERPNext Hooks - 实现指南

This skill helps you determine HOW to implement hooks.py configurations. For exact syntax, see
erpnext-syntax-hooks
.
Version: 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_rename

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

myapp/hooks.py

myapp/hooks.py

doc_events = { "Sales Invoice": { "validate": "myapp.events.sales_invoice.validate", "on_submit": "myapp.events.sales_invoice.on_submit" } }
undefined
doc_events = { "Sales Invoice": { "validate": "myapp.events.sales_invoice.validate", "on_submit": "myapp.events.sales_invoice.on_submit" } }
undefined

Step 2: Create handler module

步骤2:创建处理程序模块

python
undefined
python
undefined

myapp/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_cost
def on_submit(doc, method=None): """ After submit - document already saved Use frappe.db.set_value for additional changes """ create_external_record(doc)
undefined
import 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_cost
def on_submit(doc, method=None): """ 提交后 - 文档已保存 使用frappe.db.set_value进行额外修改 """ create_external_record(doc)
undefined

Step 3: Deploy

步骤3:部署

bash
bench --site sitename migrate

bash
bench --site sitename migrate

Implementation Workflow: scheduler_events

实现工作流:scheduler_events

Step 1: Add to hooks.py

步骤1:添加到hooks.py

python
undefined
python
undefined

myapp/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"] } }
undefined
scheduler_events = { "daily": ["myapp.tasks.daily_cleanup"], "daily_long": ["myapp.tasks.heavy_processing"], "cron": { "0 9 * * 1-5": ["myapp.tasks.weekday_report"] } }
undefined

Step 2: Create task module

步骤2:创建任务模块

python
undefined
python
undefined

myapp/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
undefined
import 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() # 长任务中每处理一个批次就提交一次
undefined

Step 3: Deploy and verify

步骤3:部署并验证

bash
bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status

bash
bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status

Implementation Workflow: extend_doctype_class (V16+)

实现工作流:extend_doctype_class(V16+)

Step 1: Add to hooks.py

步骤1:添加到hooks.py

python
undefined
python
undefined

myapp/hooks.py

myapp/hooks.py

extend_doctype_class = { "Sales Invoice": ["myapp.extensions.SalesInvoiceMixin"] }
undefined
extend_doctype_class = { "Sales Invoice": ["myapp.extensions.SalesInvoiceMixin"] }
undefined

Step 2: Create mixin class

步骤2:创建Mixin类

python
undefined
python
undefined

myapp/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")
undefined
import 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("警告:利润率过低的发票")
undefined

Step 3: Deploy

步骤3:部署

bash
bench --site sitename migrate

bash
bench --site sitename migrate

Implementation Workflow: Permission Hooks

实现工作流:权限钩子

Step 1: Add to hooks.py

步骤1:添加到hooks.py

python
undefined
python
undefined

myapp/hooks.py

myapp/hooks.py

permission_query_conditions = { "Sales Invoice": "myapp.permissions.si_query" } has_permission = { "Sales Invoice": "myapp.permissions.si_permission" }
undefined
permission_query_conditions = { "Sales Invoice": "myapp.permissions.si_query" } has_permission = { "Sales Invoice": "myapp.permissions.si_permission" }
undefined

Step 2: Create permission handlers

步骤2:创建权限处理程序

python
undefined
python
undefined

myapp/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

快速参考:处理程序签名

HookSignature
doc_events
def handler(doc, method=None):
rename events
def handler(doc, method, old, new, merge):
scheduler_events
def handler():
(no args)
extend_bootinfo
def handler(bootinfo):
permission_query
def handler(user):
→ returns SQL string
has_permission
def handler(doc, user=None, permission_type=None):
→ True/False/None
override methodsMust match original signature exactly

钩子签名
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
覆盖方法必须与原方法签名完全一致

Critical Rules

关键规则

1. Never commit in doc_events

1. 切勿在doc_events中提交事务

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

2. Use db_set_value after on_update

2. 在on_update后使用db_set_value

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

3. Always call super() in overrides

3. 在覆盖方法中务必调用super()

python
undefined
python
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()
undefined
class CustomInvoice(SalesInvoice): def validate(self): super().validate() # 务必先调用! self.my_validation()
undefined

4. Always migrate after hooks changes

4. 修改hooks后务必执行迁移

bash
undefined
bash
undefined

Required after ANY hooks.py change

任何hooks.py修改后都需要执行

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

5. permission_query only works with get_list

5. permission_query仅适用于get_list

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

版本差异

FeatureV14V15V16
doc_events
scheduler_events
override_doctype_class
extend_doctype_class
permission hooks
Scheduler tick4 min4 min60 sec

特性V14V15V16
doc_events
scheduler_events
override_doctype_class
extend_doctype_class
权限钩子
调度器间隔4分钟4分钟60秒

Reference Files

参考文件

FileContents
decision-tree.mdComplete hook selection flowcharts
workflows.mdStep-by-step implementation patterns
examples.mdWorking code examples
anti-patterns.mdCommon mistakes and solutions
文件内容
decision-tree.md完整的钩子选择流程图
workflows.md分步实现模式
examples.md可运行的代码示例
anti-patterns.md常见错误及解决方案