erpnext-impl-controllers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext Controllers - Implementation

ERPNext控制器 - 实现指南

This skill helps you determine HOW to implement server-side DocType logic. For exact syntax, see
erpnext-syntax-controllers
.
Version: 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

Aspectvalidateon_update
WhenBefore DB writeAfter DB write
Changes to self✅ Saved❌ NOT saved
Can throw error✅ Aborts save⚠️ Already saved
Use forValidation, calculationsNotifications, linked docs
get_doc_before_save()✅ Available✅ Available
维度validateon_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_amount
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_amount

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

hooks.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()
undefined
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
class CustomSalesInvoice(SalesInvoice): def validate(self): super().validate() # ALWAYS call parent self.custom_validation()
undefined

Method 2: Add Event Handler (Safer)

方式2:添加事件处理器(更安全)

python
undefined
python
undefined

hooks.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"))
undefined
def validate_sales_invoice(doc, method=None): if doc.grand_total < 0: frappe.throw(_("Invalid total"))
undefined

V16: extend_doctype_class (New)

V16版本:extend_doctype_class(新增)

python
undefined
python
undefined

hooks.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
undefined
class SalesInvoiceExtend: def custom_method(self): pass
undefined

Flags System

Flags系统

python
undefined
python
undefined

Document-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)
undefined
doc.insert(ignore_permissions=True, ignore_mandatory=True) doc.save(ignore_permissions=True)
undefined

Execution Order Reference

执行顺序参考

INSERT (New Document)

插入新文档

before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert → 
on_update → on_change
before_insert → before_naming → autoname → before_validate →
validate → before_save → [数据库插入] → after_insert → 
on_update → on_change

SAVE (Existing Document)

保存现有文档

before_validate → validate → before_save → [DB UPDATE] →
on_update → on_change
before_validate → validate → before_save → [数据库更新] →
on_update → on_change

SUBMIT

提交文档

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
self.x = y
in on_update
frappe.db.set_value(...)
frappe.db.commit()
in hooks
Let framework handle commits
Heavy operations in validateUse
frappe.enqueue()
in on_update
self.save()
in on_update
Causes infinite loop!
Assume hook order across docsEach doc has its own cycle
→ See references/anti-patterns.md for complete list.
❌ 错误做法✅ 正确做法
在on_update中使用
self.x = y
使用
frappe.db.set_value(...)
在钩子中调用
frappe.db.commit()
由框架自动处理提交
在validate中执行重操作在on_update中使用
frappe.enqueue()
异步执行
在on_update中调用
self.save()
会导致无限循环!
假设不同文档的钩子执行顺序一致每个文档有独立的执行周期
→ 完整反模式列表请参考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 - 需避免的常见错误