erpnext-impl-controllers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Controllers - Implementation
ERPNext控制器 - 实现指南
This skill helps you determine HOW to implement server-side DocType logic. For exact syntax, see .
erpnext-syntax-controllersVersion: v14/v15/v16 compatible
本指南帮助你确定如何实现服务器端DocType逻辑。如需具体语法,请参考。
erpnext-syntax-controllers版本:兼容v14/v15/v16
Main Decision: Controller vs Server Script?
核心决策:使用控制器还是服务器脚本?
┌───────────────────────────────────────────────────────────────────┐
│ WHAT DO YOU NEED? │
├───────────────────────────────────────────────────────────────────┤
│ │
│ ► Import external libraries (requests, pandas, numpy) │
│ └── Controller ✓ │
│ │
│ ► Complex multi-document transactions with rollback │
│ └── Controller ✓ │
│ │
│ ► Full Python power (try/except, classes, generators) │
│ └── Controller ✓ │
│ │
│ ► Extend/override standard ERPNext DocType │
│ └── Controller (override_doctype_class in hooks.py) │
│ │
│ ► Quick validation without custom app │
│ └── Server Script │
│ │
│ ► Simple auto-fill or calculation │
│ └── Server Script │
│ │
└───────────────────────────────────────────────────────────────────┘Rule: Controllers for custom apps with full Python power. Server Scripts for quick no-code solutions.
┌───────────────────────────────────────────────────────────────────┐
│ 你的需求是什么? │
├───────────────────────────────────────────────────────────────────┤
│ │
│ ► 导入外部库(requests, pandas, numpy) │
│ └── 选择控制器 ✓ │
│ │
│ ► 带回滚的复杂多文档事务处理 │
│ └── 选择控制器 ✓ │
│ │
│ ► 完整的Python功能支持(try/except、类、生成器) │
│ └── 选择控制器 ✓ │
│ │
│ ► 扩展/重写标准ERPNext DocType │
│ └── 选择控制器(在hooks.py中配置override_doctype_class) │
│ │
│ ► 无需自定义应用的快速验证 │
│ └── 选择服务器脚本 │
│ │
│ ► 简单的自动填充或计算 │
│ └── 选择服务器脚本 │
│ │
└───────────────────────────────────────────────────────────────────┘规则:需要完整Python功能的自定义应用使用控制器;快速无代码解决方案使用服务器脚本。
Decision Tree: Which Hook?
决策树:选择哪个钩子?
WHAT DO YOU WANT TO DO?
│
├─► Validate data or calculate fields before save?
│ └─► validate
│ NOTE: Changes to self ARE saved
│
├─► Action AFTER save (emails, linked docs, logs)?
│ └─► on_update
│ ⚠️ Changes to self are NOT saved! Use db_set instead
│
├─► Only for NEW documents?
│ └─► after_insert
│
├─► Only for SUBMIT (docstatus 0→1)?
│ ├─► Check before submit? → before_submit
│ └─► Action after submit? → on_submit
│
├─► Only for CANCEL (docstatus 1→2)?
│ ├─► Prevent cancel? → before_cancel
│ └─► Cleanup after cancel? → on_cancel
│
├─► Before DELETE?
│ └─► on_trash
│
├─► Custom document naming?
│ └─► autoname
│
└─► Detect any change (including db_set)?
└─► on_change→ See references/decision-tree.md for complete decision tree with all hooks.
你想要实现什么功能?
│
├─► 保存前验证数据或计算字段?
│ └─► 使用validate钩子
│ 注意:对self的修改会被保存
│
├─► 保存后执行操作(发送邮件、关联文档、日志记录)?
│ └─► 使用on_update钩子
│ ⚠️ 对self的修改不会被保存!请使用db_set替代
│
├─► 仅针对新文档?
│ └─► 使用after_insert钩子
│
├─► 仅针对提交操作(docstatus 0→1)?
│ ├─► 提交前检查? → 使用before_submit钩子
│ └─► 提交后执行操作? → 使用on_submit钩子
│
├─► 仅针对取消操作(docstatus 1→2)?
│ ├─► 阻止取消? → 使用before_cancel钩子
│ └─► 取消后清理? → 使用on_cancel钩子
│
├─► 删除前执行操作?
│ └─► 使用on_trash钩子
│
├─► 自定义文档命名?
│ └─► 使用autoname钩子
│
└─► 检测任何变更(包括db_set操作)?
└─► 使用on_change钩子→ 完整钩子决策树及所有钩子说明请参考references/decision-tree.md。
CRITICAL: Changes After on_update
重点注意:on_update后的修改操作
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ CHANGES TO self AFTER on_update ARE NOT SAVED │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ WRONG - This does NOTHING: │
│ def on_update(self): │
│ self.status = "Completed" # NOT SAVED! │
│ │
│ ✅ CORRECT - Use db_set: │
│ def on_update(self): │
│ frappe.db.set_value(self.doctype, self.name, │
│ "status", "Completed") │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ on_update钩子中对self的修改不会被保存 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 错误示例 - 此代码不会生效: │
│ def on_update(self): │
│ self.status = "Completed" # 不会被保存! │
│ │
│ ✅ 正确示例 - 使用db_set: │
│ def on_update(self): │
│ frappe.db.set_value(self.doctype, self.name, │
│ "status", "Completed") │
│ │
└─────────────────────────────────────────────────────────────────────┘Hook Comparison: validate vs on_update
钩子对比:validate vs on_update
| Aspect | validate | on_update |
|---|---|---|
| When | Before DB write | After DB write |
| Changes to self | ✅ Saved | ❌ NOT saved |
| Can throw error | ✅ Aborts save | ⚠️ Already saved |
| Use for | Validation, calculations | Notifications, linked docs |
| get_doc_before_save() | ✅ Available | ✅ Available |
| 维度 | validate | on_update |
|---|---|---|
| 执行时机 | 写入数据库前 | 写入数据库后 |
| 对self的修改 | ✅ 会被保存 | ❌ 不会被保存 |
| 可抛出错误 | ✅ 会终止保存操作 | ⚠️ 数据已保存 |
| 适用场景 | 数据验证、字段计算 | 发送通知、关联文档操作 |
| get_doc_before_save() | ✅ 可调用 | ✅ 可调用 |
Common Implementation Patterns
常见实现模式
Pattern 1: Validation with Error
模式1:带错误提示的验证
python
def validate(self):
if not self.items:
frappe.throw(_("At least one item is required"))
if self.from_date > self.to_date:
frappe.throw(_("From Date cannot be after To Date"))python
def validate(self):
if not self.items:
frappe.throw(_("At least one item is required"))
if self.from_date > self.to_date:
frappe.throw(_("From Date cannot be after To Date"))Pattern 2: Auto-Calculate Fields
模式2:字段自动计算
python
def validate(self):
self.total = sum(item.amount for item in self.items)
self.tax_amount = self.total * 0.1
self.grand_total = self.total + self.tax_amountpython
def validate(self):
self.total = sum(item.amount for item in self.items)
self.tax_amount = self.total * 0.1
self.grand_total = self.total + self.tax_amountPattern 3: Detect Field Changes
模式3:检测字段变更
python
def validate(self):
old_doc = self.get_doc_before_save()
if old_doc and old_doc.status != self.status:
self.flags.status_changed = True
def on_update(self):
if self.flags.get('status_changed'):
self.notify_status_change()python
def validate(self):
old_doc = self.get_doc_before_save()
if old_doc and old_doc.status != self.status:
self.flags.status_changed = True
def on_update(self):
if self.flags.get('status_changed'):
self.notify_status_change()Pattern 4: Post-Save Actions
模式4:保存后操作
python
def on_update(self):
# Update linked document
if self.linked_doc:
frappe.db.set_value("Other DocType", self.linked_doc,
"status", "Updated")
# Send notification (never fails the save)
try:
self.send_notification()
except Exception:
frappe.log_error("Notification failed")python
def on_update(self):
# Update linked document
if self.linked_doc:
frappe.db.set_value("Other DocType", self.linked_doc,
"status", "Updated")
# Send notification (never fails the save)
try:
self.send_notification()
except Exception:
frappe.log_error("Notification failed")Pattern 5: Custom Naming
模式5:自定义命名规则
python
from frappe.model.naming import getseries
def autoname(self):
# Format: CUST-ABC-001
prefix = f"CUST-{self.customer[:3].upper()}-"
self.name = getseries(prefix, 3)→ See references/workflows.md for more implementation patterns.
python
from frappe.model.naming import getseries
def autoname(self):
# Format: CUST-ABC-001
prefix = f"CUST-{self.customer[:3].upper()}-"
self.name = getseries(prefix, 3)→ 更多实现模式请参考references/workflows.md。
Submittable Documents Workflow
可提交文档工作流
DRAFT (docstatus=0)
│
├── save() → validate → on_update
│
└── submit()
│
├── validate
├── before_submit ← Last chance to abort
├── [DB: docstatus=1]
├── on_update
└── on_submit ← Post-submit actions
SUBMITTED (docstatus=1)
│
└── cancel()
│
├── before_cancel ← Last chance to abort
├── [DB: docstatus=2]
├── on_cancel ← Reverse actions
└── [check_no_back_links]草稿状态(docstatus=0)
│
├── save() → validate → on_update
│
└── submit()
│
├── validate
├── before_submit ← 最后可终止提交的时机
├── [数据库更新:docstatus=1]
├── on_update
└── on_submit ← 提交后执行操作
已提交状态(docstatus=1)
│
└── cancel()
│
├── before_cancel ← 最后可终止取消的时机
├── [数据库更新:docstatus=2]
├── on_cancel ← 执行回滚操作
└── [检查是否存在关联文档]Submittable Implementation
可提交文档实现示例
python
def before_submit(self):
# Validation that only applies on submit
if self.total > 50000 and not self.manager_approval:
frappe.throw(_("Manager approval required for orders over 50,000"))
def on_submit(self):
# Actions after submit
self.update_stock_ledger()
self.make_gl_entries()
def before_cancel(self):
# Prevent cancel if linked docs exist
if self.has_linked_invoices():
frappe.throw(_("Cannot cancel - linked invoices exist"))
def on_cancel(self):
# Reverse submitted actions
self.reverse_stock_ledger()
self.reverse_gl_entries()python
def before_submit(self):
# Validation that only applies on submit
if self.total > 50000 and not self.manager_approval:
frappe.throw(_("Manager approval required for orders over 50,000"))
def on_submit(self):
# Actions after submit
self.update_stock_ledger()
self.make_gl_entries()
def before_cancel(self):
# Prevent cancel if linked docs exist
if self.has_linked_invoices():
frappe.throw(_("Cannot cancel - linked invoices exist"))
def on_cancel(self):
# Reverse submitted actions
self.reverse_stock_ledger()
self.reverse_gl_entries()Controller Override (hooks.py)
控制器重写(hooks.py)
Method 1: Full Override
方式1:完全重写
python
undefinedpython
undefinedhooks.py
hooks.py
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
override_doctype_class = {
"Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}
myapp/overrides.py
myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # ALWAYS call parent
self.custom_validation()
undefinedfrom erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice):
def validate(self):
super().validate() # ALWAYS call parent
self.custom_validation()
undefinedMethod 2: Add Event Handler (Safer)
方式2:添加事件处理器(更安全)
python
undefinedpython
undefinedhooks.py
hooks.py
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.validate_sales_invoice",
}
}
doc_events = {
"Sales Invoice": {
"validate": "myapp.events.validate_sales_invoice",
}
}
myapp/events.py
myapp/events.py
def validate_sales_invoice(doc, method=None):
if doc.grand_total < 0:
frappe.throw(_("Invalid total"))
undefineddef validate_sales_invoice(doc, method=None):
if doc.grand_total < 0:
frappe.throw(_("Invalid total"))
undefinedV16: extend_doctype_class (New)
V16版本:extend_doctype_class(新增)
python
undefinedpython
undefinedhooks.py (v16+)
hooks.py (v16+)
extend_doctype_class = {
"Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}
extend_doctype_class = {
"Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}
myapp/extends.py - Only methods to add/override
myapp/extends.py - 仅添加/重写需要的方法
class SalesInvoiceExtend:
def custom_method(self):
pass
undefinedclass SalesInvoiceExtend:
def custom_method(self):
pass
undefinedFlags System
Flags系统
python
undefinedpython
undefinedDocument-level flags
文档级Flags
doc.flags.ignore_permissions = True # Bypass permissions
doc.flags.ignore_validate = True # Skip validate()
doc.flags.ignore_mandatory = True # Skip required fields
doc.flags.ignore_permissions = True # 绕过权限校验
doc.flags.ignore_validate = True # 跳过validate()钩子
doc.flags.ignore_mandatory = True # 跳过必填字段校验
Custom flags for inter-hook communication
用于钩子间通信的自定义Flags
def validate(self):
if self.is_urgent:
self.flags.needs_notification = True
def on_update(self):
if self.flags.get('needs_notification'):
self.notify_team()
def validate(self):
if self.is_urgent:
self.flags.needs_notification = True
def on_update(self):
if self.flags.get('needs_notification'):
self.notify_team()
Insert/save with flags
插入/保存时传入Flags
doc.insert(ignore_permissions=True, ignore_mandatory=True)
doc.save(ignore_permissions=True)
undefineddoc.insert(ignore_permissions=True, ignore_mandatory=True)
doc.save(ignore_permissions=True)
undefinedExecution Order Reference
执行顺序参考
INSERT (New Document)
插入新文档
before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert →
on_update → on_changebefore_insert → before_naming → autoname → before_validate →
validate → before_save → [数据库插入] → after_insert →
on_update → on_changeSAVE (Existing Document)
保存现有文档
before_validate → validate → before_save → [DB UPDATE] →
on_update → on_changebefore_validate → validate → before_save → [数据库更新] →
on_update → on_changeSUBMIT
提交文档
validate → before_submit → [DB: docstatus=1] → on_update →
on_submit → on_change→ See references/decision-tree.md for all execution orders.
validate → before_submit → [数据库更新:docstatus=1] → on_update →
on_submit → on_change→ 完整执行顺序请参考references/decision-tree.md。
Quick Anti-Pattern Check
常见反模式检查
| ❌ Don't | ✅ Do Instead |
|---|---|
| |
| Let framework handle commits |
| Heavy operations in validate | Use |
| Causes infinite loop! |
| Assume hook order across docs | Each doc has its own cycle |
→ See references/anti-patterns.md for complete list.
| ❌ 错误做法 | ✅ 正确做法 |
|---|---|
在on_update中使用 | 使用 |
在钩子中调用 | 由框架自动处理提交 |
| 在validate中执行重操作 | 在on_update中使用 |
在on_update中调用 | 会导致无限循环! |
| 假设不同文档的钩子执行顺序一致 | 每个文档有独立的执行周期 |
→ 完整反模式列表请参考references/anti-patterns.md。
References
参考文档
- decision-tree.md - Complete hook selection with all execution orders
- workflows.md - Extended implementation patterns
- examples.md - Complete working examples
- anti-patterns.md - Common mistakes to avoid
- decision-tree.md - 完整钩子选择指南及执行顺序
- workflows.md - 扩展实现模式
- examples.md - 完整可运行示例
- anti-patterns.md - 需避免的常见错误