erpnext-syntax-controllers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ERPNext 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

位置与命名规则

DocTypeClassFile
Sales Order
SalesOrder
selling/doctype/sales_order/sales_order.py
Custom Doc
CustomDoc
module/doctype/custom_doc/custom_doc.py
Rule: DocType name → PascalCase (remove spaces) → snake_case filename

DocType类名文件路径
Sales Order
SalesOrder
selling/doctype/sales_order/sales_order.py
Custom Doc
CustomDoc
module/doctype/custom_doc/custom_doc.py
规则:DocType名称 → 转换为PascalCase(移除空格)→ 文件名使用snake_case

Most Used Hooks

最常用的钩子

HookWhenTypical Use
validate
Before every saveValidation, calculations
on_update
After every saveNotifications, linked docs
after_insert
After new docCreation-only actions
on_submit
After submitLedger entries, stock
on_cancel
After cancelReverse ledger entries
on_trash
Before deleteCleanup related data
autoname
On namingCustom document name
Complete list and execution order: See lifecycle-methods.md

钩子方法执行时机典型用途
validate
每次保存前验证逻辑、计算操作
on_update
每次保存后通知推送、关联文档更新
after_insert
新文档创建后仅创建时执行的操作
on_submit
提交后分类账条目、库存操作
on_cancel
取消后反向分类账条目
on_trash
删除前清理相关数据
autoname
命名时自定义文档名称
完整钩子列表与执行顺序:查看 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_trash

Critical Rules

关键规则

1. Changes after on_update are NOT saved

1. on_update后修改self不会被保存

python
undefined
python
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")
undefined
def on_update(self): frappe.db.set_value(self.doctype, self.name, "status", "Completed")
undefined

2. No commits in controllers

2. 控制器中禁止手动提交事务

python
undefined
python
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
undefined
def on_update(self): self.update_related() # Frappe会自动提交
undefined

3. Always call super() when overriding

3. 重写方法时必须调用super()

python
undefined
python
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()
undefined
def validate(self): super().validate() self.custom_check()
undefined

4. 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

可用命名选项

OptionExampleResultVersion
field:fieldname
field:customer_name
ABC Company
All
naming_series:
naming_series:
SO-2024-00001
All
format:PREFIX-{##}
format:INV-{YYYY}-{####}
INV-2024-0001
All
hash
hash
a1b2c3d4e5
All
Prompt
Prompt
User enters nameAll
UUID
UUID
01948d5f-...
v16+
Custom methodController autoname()Any patternAll
选项示例结果版本
field:fieldname
field:customer_name
ABC Company
所有版本
naming_series:
naming_series:
SO-2024-00001
所有版本
format:PREFIX-{##}
format:INV-{YYYY}-{####}
INV-2024-0001
所有版本
hash
hash
a1b2c3d4e5
所有版本
Prompt
Prompt
用户输入名称所有版本
UUID
UUID
01948d5f-...
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
undefined
v16版本新增:基于UUID的命名方式,用于全局唯一标识符。
json
{
  "doctype": "DocType",
  "autoname": "UUID"
}
优势
  • 跨系统全局唯一
  • 更好的数据完整性与可追溯性
  • 减少数据库存储占用
  • 批量创建记录速度更快
  • 链接字段以原生格式存储UUID
实现逻辑
python
undefined

Frappe automatically generates UUID7

Frappe自动生成UUID7

In naming.py:

在naming.py中:

if meta.autoname == "UUID": doc.name = str(uuid_utils.uuid7())

**Validation:**
```python
if meta.autoname == "UUID": doc.name = str(uuid_utils.uuid7())

**验证逻辑**:
```python

UUID names are validated on import

导入时会验证UUID格式

from uuid import UUID try: UUID(doc.name) except ValueError: frappe.throw(_("Invalid UUID: {}").format(doc.name))
undefined
from uuid import UUID try: UUID(doc.name) except ValueError: frappe.throw(_("Invalid UUID: {}").format(doc.name))
undefined

Custom 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

格式模式

PatternDescriptionExample
{#}
Counter1, 2, 3
{##}
Zero-padded counter01, 02, 03
{####}
4-digit counter0001, 0002
{YYYY}
Full year2024
{YY}
2-digit year24
{MM}
Month01-12
{DD}
Day01-31
{fieldname}
Field value(value)

模式说明示例
{#}
计数器1, 2, 3
{##}
补零计数器01, 02, 03
{####}
4位计数器0001, 0002
{YYYY}
完整年份2024
{YY}
两位年份24
{MM}
月份01-12
{DD}
日期01-31
{fieldname}
字段值(对应字段的值)

Controller Override

控制器重写

Via hooks.py (override_doctype_class)

通过hooks.py(override_doctype_class)

python
undefined
python
undefined

hooks.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()
undefined
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder): def validate(self): super().validate() self.custom_validation()
undefined

Via doc_events (hooks.py)

通过doc_events(hooks.py)

python
undefined
python
undefined

hooks.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
is_submittable = 1
have a docstatus lifecycle:
docstatusStatusEditableCan go to
0Draft✅ Yes1 (Submit)
1Submitted❌ No2 (Cancel)
2Cancelled❌ 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()

设置
is_submittable = 1
的文档具有docstatus生命周期:
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):
    pass
python
from frappe.model.document import Document

class MyDocType(Document):
    pass

Tree DocType

树形DocType

python
from frappe.utils.nestedset import NestedSet

class Department(NestedSet):
    pass
python
from frappe.utils.nestedset import NestedSet

class Department(NestedSet):
    pass

Extend 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.Date
Enable in
hooks.py
:
python
export_python_type_annotations = True

python
class Person(Document):
    if TYPE_CHECKING:
        from frappe.types import DF
        first_name: DF.Data
        last_name: DF.Data
        birth_date: DF.Date
hooks.py
中启用:
python
export_python_type_annotations = True

Reference Files

参考文件

FileContents
lifecycle-methods.mdAll hooks, execution order, examples
methods.mdAll doc.* methods with signatures
flags.mdFlags system documentation
examples.mdComplete working controller examples
anti-patterns.mdCommon mistakes and corrections

文件内容
lifecycle-methods.md所有钩子方法、执行顺序、示例
methods.md所有doc.*方法及签名
flags.mdFlags系统文档
examples.md完整的可运行控制器示例
anti-patterns.md常见错误与修正方案

Version Differences

版本差异

Featurev14v15v16
Type annotations✅ Auto-generated
before_discard
hook
on_discard
hook
flags.notify_update
UUID autoname
UUID in Link fields (native)
特性v14v15v16
类型注解❌ 不支持✅ 自动生成✅ 支持
before_discard
钩子
❌ 不支持✅ 支持✅ 支持
on_discard
钩子
❌ 不支持✅ 支持✅ 支持
flags.notify_update
❌ 不支持✅ 支持✅ 支持
UUID自动命名❌ 不支持❌ 不支持✅ 支持
链接字段原生支持UUID❌ 不支持❌ 不支持✅ 支持

v16-Specific Notes

v16专属说明

UUID Naming:
  • Set
    autoname = "UUID"
    in DocType definition
  • Uses
    uuid7()
    for time-ordered UUIDs
  • 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 systems

UUID命名
  • 在DocType定义中设置
    autoname = "UUID"
  • 使用
    uuid7()
    生成带时间戳的UUID
  • 链接字段以原生格式存储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

相关技能

  • erpnext-syntax-serverscripts
    – Server Scripts (sandbox alternative)
  • erpnext-syntax-hooks
    – hooks.py configuration
  • erpnext-impl-controllers
    – Implementation workflows
  • erpnext-syntax-serverscripts
    – 服务端脚本(沙箱替代方案)
  • erpnext-syntax-hooks
    – hooks.py配置
  • erpnext-impl-controllers
    – 实现工作流