erpnext-syntax-controllers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Syntax: Document Controllers
ERPNext 语法规范:Document Controllers
Document Controllers are Python classes that implement the server-side logic of a DocType.
Document Controllers是实现DocType服务端逻辑的Python类。
Quick Reference
快速参考
Controller Basic Structure
控制器基础结构
python
import frappe
from frappe.model.document import Document
class SalesOrder(Document):
def validate(self):
"""Main validation - runs on every save."""
if not self.items:
frappe.throw(_("Items are required"))
self.total = sum(item.amount for item in self.items)
def on_update(self):
"""After save - changes to self are NOT saved."""
self.update_linked_docs()python
import frappe
from frappe.model.document import Document
class SalesOrder(Document):
def validate(self):
"""主要验证逻辑 - 每次保存时执行。"""
if not self.items:
frappe.throw(_("Items are required"))
self.total = sum(item.amount for item in self.items)
def on_update(self):
"""保存后执行 - 对self的修改不会被保存。"""
self.update_linked_docs()Location and Naming
位置与命名规则
| DocType | Class | File |
|---|---|---|
| Sales Order | | |
| Custom Doc | | |
Rule: DocType name → PascalCase (remove spaces) → snake_case filename
| DocType | 类名 | 文件路径 |
|---|---|---|
| Sales Order | | |
| Custom Doc | | |
规则:DocType名称 → 转换为PascalCase(移除空格)→ 文件名使用snake_case
Most Used Hooks
最常用的钩子
| Hook | When | Typical Use |
|---|---|---|
| Before every save | Validation, calculations |
| After every save | Notifications, linked docs |
| After new doc | Creation-only actions |
| After submit | Ledger entries, stock |
| After cancel | Reverse ledger entries |
| Before delete | Cleanup related data |
| On naming | Custom document name |
Complete list and execution order: See lifecycle-methods.md
| 钩子方法 | 执行时机 | 典型用途 |
|---|---|---|
| 每次保存前 | 验证逻辑、计算操作 |
| 每次保存后 | 通知推送、关联文档更新 |
| 新文档创建后 | 仅创建时执行的操作 |
| 提交后 | 分类账条目、库存操作 |
| 取消后 | 反向分类账条目 |
| 删除前 | 清理相关数据 |
| 命名时 | 自定义文档名称 |
完整钩子列表与执行顺序:查看 lifecycle-methods.md
Hook Selection Decision Tree
钩子方法选择决策树
What do you want to do?
│
├─► Validate or calculate fields?
│ └─► validate
│
├─► Action after save (emails, linked docs)?
│ └─► on_update
│
├─► Only for NEW docs?
│ └─► after_insert
│
├─► On SUBMIT?
│ ├─► Check beforehand? → before_submit
│ └─► Action afterwards? → on_submit
│
├─► On CANCEL?
│ ├─► Check beforehand? → before_cancel
│ └─► Cleanup? → on_cancel
│
├─► Custom document name?
│ └─► autoname
│
└─► Cleanup before delete?
└─► on_trash你需要执行什么操作?
│
├─► 验证或计算字段?
│ └─► validate
│
├─► 保存后执行操作(邮件、关联文档)?
│ └─► on_update
│
├─► 仅针对新文档?
│ └─► after_insert
│
├─► 提交文档时?
│ ├─► 提交前检查? → before_submit
│ └─► 提交后执行操作? → on_submit
│
├─► 取消文档时?
│ ├─► 取消前检查? → before_cancel
│ └─► 清理操作? → on_cancel
│
├─► 自定义文档名称?
│ └─► autoname
│
└─► 删除前清理数据?
└─► on_trashCritical Rules
关键规则
1. Changes after on_update are NOT saved
1. on_update后修改self不会被保存
python
undefinedpython
undefined❌ WRONG - change is lost
❌ 错误 - 修改会丢失
def on_update(self):
self.status = "Completed" # NOT saved
def on_update(self):
self.status = "Completed" # 不会被保存
✅ CORRECT - use db_set
✅ 正确 - 使用db_set
def on_update(self):
frappe.db.set_value(self.doctype, self.name, "status", "Completed")
undefineddef on_update(self):
frappe.db.set_value(self.doctype, self.name, "status", "Completed")
undefined2. No commits in controllers
2. 控制器中禁止手动提交事务
python
undefinedpython
undefined❌ WRONG - Frappe handles commits
❌ 错误 - Frappe会自动处理提交
def on_update(self):
frappe.db.commit() # DON'T DO THIS
def on_update(self):
frappe.db.commit() # 请勿这样做
✅ CORRECT - no commit needed
✅ 正确 - 无需手动提交
def on_update(self):
self.update_related() # Frappe commits automatically
undefineddef on_update(self):
self.update_related() # Frappe会自动提交
undefined3. Always call super() when overriding
3. 重写方法时必须调用super()
python
undefinedpython
undefined❌ WRONG - parent logic is skipped
❌ 错误 - 父类逻辑会被跳过
def validate(self):
self.custom_check()
def validate(self):
self.custom_check()
✅ CORRECT - parent logic is preserved
✅ 正确 - 保留父类逻辑
def validate(self):
super().validate()
self.custom_check()
undefineddef validate(self):
super().validate()
self.custom_check()
undefined4. Use flags for recursion prevention
4. 使用flags防止递归调用
python
def on_update(self):
if self.flags.get('from_linked_doc'):
return
linked = frappe.get_doc("Linked Doc", self.linked_doc)
linked.flags.from_linked_doc = True
linked.save()python
def on_update(self):
if self.flags.get('from_linked_doc'):
return
linked = frappe.get_doc("Linked Doc", self.linked_doc)
linked.flags.from_linked_doc = True
linked.save()Document Naming (autoname)
文档命名(autoname)
Available Naming Options
可用命名选项
| Option | Example | Result | Version |
|---|---|---|---|
| | | All |
| | | All |
| | | All |
| | | All |
| | User enters name | All |
| | | v16+ |
| Custom method | Controller autoname() | Any pattern | All |
| 选项 | 示例 | 结果 | 版本 |
|---|---|---|---|
| | | 所有版本 |
| | | 所有版本 |
| | | 所有版本 |
| | | 所有版本 |
| | 用户输入名称 | 所有版本 |
| | | v16+ |
| 自定义方法 | 控制器中的autoname() | 任意格式 | 所有版本 |
UUID Naming (v16+)
UUID命名(v16+)
New in v16: UUID-based naming for globally unique identifiers.
json
{
"doctype": "DocType",
"autoname": "UUID"
}Benefits:
- Globally unique across systems
- Better data integrity and traceability
- Reduced database storage
- Faster bulk record creation
- Link fields store UUID in native format
Implementation:
python
undefinedv16版本新增:基于UUID的命名方式,用于全局唯一标识符。
json
{
"doctype": "DocType",
"autoname": "UUID"
}优势:
- 跨系统全局唯一
- 更好的数据完整性与可追溯性
- 减少数据库存储占用
- 批量创建记录速度更快
- 链接字段以原生格式存储UUID
实现逻辑:
python
undefinedFrappe automatically generates UUID7
Frappe自动生成UUID7
In naming.py:
在naming.py中:
if meta.autoname == "UUID":
doc.name = str(uuid_utils.uuid7())
**Validation:**
```pythonif meta.autoname == "UUID":
doc.name = str(uuid_utils.uuid7())
**验证逻辑**:
```pythonUUID names are validated on import
导入时会验证UUID格式
from uuid import UUID
try:
UUID(doc.name)
except ValueError:
frappe.throw(_("Invalid UUID: {}").format(doc.name))
undefinedfrom uuid import UUID
try:
UUID(doc.name)
except ValueError:
frappe.throw(_("Invalid UUID: {}").format(doc.name))
undefinedCustom autoname Method
自定义autoname方法
python
from frappe.model.naming import getseries
class Project(Document):
def autoname(self):
# Custom naming based on customer
prefix = f"P-{self.customer}-"
self.name = getseries(prefix, 3)
# Result: P-ACME-001, P-ACME-002, etc.python
from frappe.model.naming import getseries
class Project(Document):
def autoname(self):
# 基于客户的自定义命名
prefix = f"P-{self.customer}-"
self.name = getseries(prefix, 3)
# 结果:P-ACME-001, P-ACME-002, 等Format Patterns
格式模式
| Pattern | Description | Example |
|---|---|---|
| Counter | 1, 2, 3 |
| Zero-padded counter | 01, 02, 03 |
| 4-digit counter | 0001, 0002 |
| Full year | 2024 |
| 2-digit year | 24 |
| Month | 01-12 |
| Day | 01-31 |
| Field value | (value) |
| 模式 | 说明 | 示例 |
|---|---|---|
| 计数器 | 1, 2, 3 |
| 补零计数器 | 01, 02, 03 |
| 4位计数器 | 0001, 0002 |
| 完整年份 | 2024 |
| 两位年份 | 24 |
| 月份 | 01-12 |
| 日期 | 01-31 |
| 字段值 | (对应字段的值) |
Controller Override
控制器重写
Via hooks.py (override_doctype_class)
通过hooks.py(override_doctype_class)
python
undefinedpython
undefinedhooks.py
hooks.py
override_doctype_class = {
"Sales Order": "custom_app.overrides.CustomSalesOrder"
}
override_doctype_class = {
"Sales Order": "custom_app.overrides.CustomSalesOrder"
}
custom_app/overrides.py
custom_app/overrides.py
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()
undefinedfrom erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()
undefinedVia doc_events (hooks.py)
通过doc_events(hooks.py)
python
undefinedpython
undefinedhooks.py
hooks.py
doc_events = {
"Sales Order": {
"validate": "custom_app.events.validate_sales_order",
"on_submit": "custom_app.events.on_submit_sales_order"
}
}
doc_events = {
"Sales Order": {
"validate": "custom_app.events.validate_sales_order",
"on_submit": "custom_app.events.on_submit_sales_order"
}
}
custom_app/events.py
custom_app/events.py
def validate_sales_order(doc, method):
if doc.total > 100000:
doc.requires_approval = 1
**Choice**: `override_doctype_class` for full control, `doc_events` for individual hooks.
---def validate_sales_order(doc, method):
if doc.total > 100000:
doc.requires_approval = 1
**选择建议**:`override_doctype_class`用于完全控制,`doc_events`用于单独钩子方法的扩展。
---Submittable Documents
可提交文档
Documents with have a docstatus lifecycle:
is_submittable = 1| docstatus | Status | Editable | Can go to |
|---|---|---|---|
| 0 | Draft | ✅ Yes | 1 (Submit) |
| 1 | Submitted | ❌ No | 2 (Cancel) |
| 2 | Cancelled | ❌ No | - |
python
class StockEntry(Document):
def on_submit(self):
"""After submit - create stock ledger entries."""
self.update_stock_ledger()
def on_cancel(self):
"""After cancel - reverse the entries."""
self.reverse_stock_ledger()设置的文档具有docstatus生命周期:
is_submittable = 1| docstatus | 状态 | 可编辑 | 可转换至 |
|---|---|---|---|
| 0 | 草稿 | ✅ 是 | 1(提交) |
| 1 | 已提交 | ❌ 否 | 2(取消) |
| 2 | 已取消 | ❌ 否 | - |
python
class StockEntry(Document):
def on_submit(self):
"""提交后 - 创建库存分类账条目。"""
self.update_stock_ledger()
def on_cancel(self):
"""取消后 - 反向生成条目。"""
self.reverse_stock_ledger()Virtual DocTypes
虚拟DocTypes
For external data sources (no database table):
python
class ExternalCustomer(Document):
@staticmethod
def get_list(args):
return external_api.get_customers(args.get("filters"))
@staticmethod
def get_count(args):
return external_api.count_customers(args.get("filters"))
@staticmethod
def get_stats(args):
return {}用于外部数据源(无数据库表):
python
class ExternalCustomer(Document):
@staticmethod
def get_list(args):
return external_api.get_customers(args.get("filters"))
@staticmethod
def get_count(args):
return external_api.count_customers(args.get("filters"))
@staticmethod
def get_stats(args):
return {}Inheritance Patterns
继承模式
Standard Controller
标准控制器
python
from frappe.model.document import Document
class MyDocType(Document):
passpython
from frappe.model.document import Document
class MyDocType(Document):
passTree DocType
树形DocType
python
from frappe.utils.nestedset import NestedSet
class Department(NestedSet):
passpython
from frappe.utils.nestedset import NestedSet
class Department(NestedSet):
passExtend Existing Controller
扩展现有控制器
python
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()python
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()Type Annotations (v15+)
类型注解(v15+)
python
class Person(Document):
if TYPE_CHECKING:
from frappe.types import DF
first_name: DF.Data
last_name: DF.Data
birth_date: DF.DateEnable in :
hooks.pypython
export_python_type_annotations = Truepython
class Person(Document):
if TYPE_CHECKING:
from frappe.types import DF
first_name: DF.Data
last_name: DF.Data
birth_date: DF.Date在中启用:
hooks.pypython
export_python_type_annotations = TrueReference Files
参考文件
| File | Contents |
|---|---|
| lifecycle-methods.md | All hooks, execution order, examples |
| methods.md | All doc.* methods with signatures |
| flags.md | Flags system documentation |
| examples.md | Complete working controller examples |
| anti-patterns.md | Common mistakes and corrections |
| 文件 | 内容 |
|---|---|
| lifecycle-methods.md | 所有钩子方法、执行顺序、示例 |
| methods.md | 所有doc.*方法及签名 |
| flags.md | Flags系统文档 |
| examples.md | 完整的可运行控制器示例 |
| anti-patterns.md | 常见错误与修正方案 |
Version Differences
版本差异
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| Type annotations | ❌ | ✅ Auto-generated | ✅ |
| ❌ | ✅ | ✅ |
| ❌ | ✅ | ✅ |
| ❌ | ✅ | ✅ |
| UUID autoname | ❌ | ❌ | ✅ |
| UUID in Link fields (native) | ❌ | ❌ | ✅ |
| 特性 | v14 | v15 | v16 |
|---|---|---|---|
| 类型注解 | ❌ 不支持 | ✅ 自动生成 | ✅ 支持 |
| ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| UUID自动命名 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 |
| 链接字段原生支持UUID | ❌ 不支持 | ❌ 不支持 | ✅ 支持 |
v16-Specific Notes
v16专属说明
UUID Naming:
- Set in DocType definition
autoname = "UUID" - Uses for time-ordered UUIDs
uuid7() - Link fields store UUIDs in native format (not text)
- Improves performance for bulk operations
Choosing UUID vs Traditional Naming:
When to use UUID:
├── Cross-system data synchronization
├── Bulk record creation
├── Global uniqueness required
└── No human-readable name needed
When to use traditional naming:
├── User-facing document references (SO-00001)
├── Sequential numbering required
├── Auditing requires readable names
└── Integration with legacy systemsUUID命名:
- 在DocType定义中设置
autoname = "UUID" - 使用生成带时间戳的UUID
uuid7() - 链接字段以原生格式存储UUID(而非文本)
- 提升批量操作性能
UUID与传统命名的选择:
何时使用UUID:
├── 跨系统数据同步
├── 批量创建记录
├── 需要全局唯一标识
└── 无需人类可读名称
何时使用传统命名:
├── 用户可见的文档编号(如SO-00001)
├── 需要连续编号
├── 审计要求可读名称
└── 与遗留系统集成Anti-Patterns
反模式
❌ Direct field change after on_update
❌ on_update后直接修改self
python
def on_update(self):
self.status = "Done" # Will be lost!python
def on_update(self):
self.status = "Done" # 修改会丢失!❌ frappe.db.commit() in controller
❌ 控制器中调用frappe.db.commit()
python
def validate(self):
frappe.db.commit() # Breaks transaction!python
def validate(self):
frappe.db.commit() # 破坏事务逻辑!❌ Forgetting to call super()
❌ 重写方法时忘记调用super()
python
def validate(self):
self.my_check() # Parent validate is skipped→ See anti-patterns.md for complete list.
python
def validate(self):
self.my_check() # 父类validate逻辑被跳过→ 查看 anti-patterns.md 获取完整列表。
Related Skills
相关技能
- – Server Scripts (sandbox alternative)
erpnext-syntax-serverscripts - – hooks.py configuration
erpnext-syntax-hooks - – Implementation workflows
erpnext-impl-controllers
- – 服务端脚本(沙箱替代方案)
erpnext-syntax-serverscripts - – hooks.py配置
erpnext-syntax-hooks - – 实现工作流
erpnext-impl-controllers