关于 frappe-syntax-controllers
面向 Frappe 开发者的 DocType 控制器语法参考,涵盖生命周期、命名与扩展方式,并整理常见规则和错误模式。
主要功能
- 01控制器文件与类命名规范
- 02文档生命周期钩子顺序
- 03文档命名与 autoname 方法
- 04控制器扩展机制对比
- 05白名单方法与文档状态规则
适用场景
- 01实现 DocType 服务端逻辑
- 02选择合适的生命周期钩子
- 03为现有 DocType 添加自定义行为
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-syntax-controllers安装后在 Claude Code 中输入/frappe-syntax-controllers确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/anti-patterns.mddocument-api-complete.mdevents.mdexamples.mdflags.mdhooks.mdlifecycle-methods.mdmethods.mdpatterns.mdsyntax.md详细介绍
Frappe 语法:文档控制器
文档控制器是定义 DocType 所有服务端逻辑的 Python 类。
每个 DocType 都恰好有一个控制器文件。控制器类继承 frappe.model.document.Document。
快速参考
import frappe
from frappe import _
from frappe.model.document import Document
class SalesOrder(Document):
def autoname(self):
"""Custom naming logic. Sets self.name."""
self.name = f"SO-{self.customer_code}-{frappe.utils.now_datetime().year}"
def validate(self):
"""MAIN validation — runs on EVERY save (insert and update).
Changes to self ARE saved to database."""
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.
Use frappe.db.set_value() for post-save field changes."""
self.notify_linked_docs()
def on_submit(self):
"""After submit (docstatus 0 -> 1). Create ledger entries here."""
self.create_gl_entries()
def on_cancel(self):
"""After cancel (docstatus 1 -> 2). Reverse ledger entries here."""
self.reverse_gl_entries()
@frappe.whitelist()
def recalculate(self):
"""Exposed to client JS via frm.call('recalculate')."""
self.total = sum(item.amount for item in self.items)
return {"total": self.total}
文件位置和命名
| DocType 名称 | 类名 | 文件路径 |
|---|---|---|
| Sales Order | SalesOrder | selling/doctype/sales_order/sales_order.py |
| My Custom Doc | MyCustomDoc | module/doctype/my_custom_doc/my_custom_doc.py |
规则:DocType 名称 -> PascalCase 类 -> snake_case 文件名。必须完全匹配。
生命周期钩子执行顺序
INSERT(新文档)
before_insert -> before_naming -> autoname -> before_validate -> validate
-> before_save -> [db_insert] -> after_insert -> on_update -> on_change
SAVE(现有文档)
before_validate -> validate -> before_save -> [db_update]
-> on_update -> on_change
SUBMIT(docstatus 0 -> 1)
before_validate -> validate -> before_submit -> [db_update]
-> on_submit -> on_update -> on_change
CANCEL(docstatus 1 -> 2)
before_cancel -> [db_update] -> on_cancel -> on_change
提交后更新
before_update_after_submit -> [db_update]
-> on_update_after_submit -> on_change
删除
on_trash -> [db_delete] -> after_delete
DISCARD [v15+]
before_discard -> [db_set docstatus=2] -> on_discard
包含参数的完整钩子参考:参见 lifecycle-methods.md
钩子选择决策树
What do you need to do?
|
+-- Validate data or calculate fields?
| +-- validate (changes to self ARE saved)
|
+-- Action AFTER save (emails, sync, linked docs)?
| +-- on_update (changes to self are NOT saved)
|
+-- Only for NEW documents?
| +-- after_insert (runs once on first save only)
|
+-- Custom document name?
| +-- autoname (set self.name)
|
+-- Before/after SUBMIT?
| +-- Validate before submit? -> before_submit
| +-- Create entries after submit? -> on_submit
|
+-- Before/after CANCEL?
| +-- Check linked docs? -> before_cancel
| +-- Reverse entries? -> on_cancel
|
+-- Cleanup before delete?
| +-- on_trash
|
+-- React to ANY value change (including db_set)?
| +-- on_change (MUST be idempotent)
关键规则
1. on_update 之后的更改不会保存
# WRONG - change is lost after on_update
def on_update(self):
self.status = "Completed" # NOT saved to database
# CORRECT - use db_set or frappe.db.set_value
def on_update(self):
self.db_set("status", "Completed")
2. 切勿在控制器中调用 frappe.db.commit()
# WRONG - breaks Frappe transaction management
def validate(self):
frappe.db.commit() # Can cause partial updates on error
# CORRECT - Frappe commits automatically at end of request
def validate(self):
self.update_related() # No commit needed
3. 重写时始终调用 super()
# WRONG - parent validation is skipped entirely
def validate(self):
self.custom_check()
# CORRECT - parent logic preserved
def validate(self):
super().validate()
self.custom_check()
4. 使用 flags 防止递归
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()
5. 切勿将验证逻辑放在 on_update 中
# WRONG - document is already saved when this throws
def on_update(self):
if self.total < 0:
frappe.throw("Invalid total") # Too late!
# CORRECT - validate BEFORE save
def validate(self):
if self.total < 0:
frappe.throw("Invalid total") # Blocks save
文档命名(autoname)
| 方法 | 示例 | 结果 | 版本 |
|---|---|---|---|
field:fieldname | field:customer_name | ABC Company | All |
naming_series: | naming_series: | SO-2024-00001 | All |
| 表达式 | PRE-.##### | PRE-00001 | All |
| 旧式格式 | INV-{YYYY}-{####} | INV-2024-0001 | Deprecated v16 |
hash / random | hash | a1b2c3d4e5 | All |
Prompt | Prompt | 用户输入名称 | All |
autoincrement | autoincrement | 1、2、3 | All |
UUID | UUID | 550e8400-e29b-... | v16+ |
| 自定义方法 | 控制器中的 autoname() | 任意模式 | All |
自定义 autoname 方法
from frappe.model.naming import getseries
class Project(Document):
def autoname(self):
prefix = f"P-{self.customer[:3].upper()}-"
self.name = getseries(prefix, 3)
# Result: P-ACM-001, P-ACM-002, etc.
UUID 命名 [v16+]
在 DocType 定义中设置 autoname = "UUID"。Frappe 会生成 UUID v4。
When to use UUID: When to use traditional naming:
- Cross-system sync - User-facing references (SO-00001)
- Bulk record creation - Sequential numbering required
- Global uniqueness needed - Auditing requires readable names
控制器扩展机制
1. override_doctype_class(完全替换)[所有版本]
# hooks.py
override_doctype_class = {
"Sales Order": "custom_app.overrides.CustomSalesOrder"
}
# custom_app/overrides.py
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate() # ALWAYS call super()
self.custom_validation()
警告:一个 DocType 类只能由一个应用覆盖。多个覆盖会发生冲突。
2. extend_doctype_class(mixin,非破坏性)[v16+]
# hooks.py
extend_doctype_class = {
"Address": ["custom_app.extensions.address.GeocodingMixin"],
"Contact": [
"custom_app.extensions.common.ValidationMixin",
"custom_app.extensions.contact.PhoneMixin"
]
}
# custom_app/extensions/address.py
from frappe.model.document import Document
class GeocodingMixin(Document):
@property
def full_address(self):
return f"{self.address_line1}, {self.city}, {self.country}"
def validate(self):
super().validate()
self.geocode_address()
在 v16+ 中,始终优先使用 extend_doctype_class,而不是 override_doctype_class。
多个应用可以安全地扩展同一个 DocType。
3. doc_events(钩接单个事件)[所有版本]
# hooks.py
doc_events = {
"Sales Order": {
"validate": "custom_app.events.validate_sales_order",
"on_submit": "custom_app.events.on_submit_sales_order"
},
"*": { # ALL DocTypes
"after_insert": "custom_app.events.log_creation"
}
}
# custom_app/events.py
def validate_sales_order(doc, method=None):
if doc.total > 100000:
doc.requires_approval = 1
如何选择
Need full class replacement? -> override_doctype_class [all versions]
Need to add methods/properties? -> extend_doctype_class [v16+]
Need to hook one or two events? -> doc_events [all versions]
Need to extend in v14/v15? -> override_doctype_class or doc_events
白名单方法
使用 @frappe.whitelist() 将控制器方法开放给客户端 JavaScript:
class SalesOrder(Document):
@frappe.whitelist()
def send_email(self, recipient):
"""Callable from JS: frm.call('send_email', {recipient: '...'})"""
frappe.sendmail(recipients=[recipient], message="Order confirmed")
return {"status": "sent"}
// Client-side call
frm.call('send_email', { recipient: '[email protected]' })
.then(r => frappe.msgprint(r.message.status));
规则:
- 始终添加
@frappe.whitelist()装饰器——否则,客户端无法调用该方法 - 方法必须定义在控制器类中(不能是独立方法)
- 权限检查会自动执行(用户必须拥有文档的读取权限)
可提交文档
is_submittable = 1 的文档遵循 docstatus 生命周期:
| docstatus | 状态 | 可编辑 | 状态转换 |
|---|---|---|---|
| 0 | 草稿 | 是 | -> 1(提交) |
| 1 | 已提交 | 仅限“允许提交后编辑”的字段 | -> 2(取消) |
| 2 | 已取消 | 否 | 无(修订会创建新的草稿) |
始终成对实现 on_submit 和 on_cancel。
始终在 on_cancel 中撤销 on_submit 所创建的内容。
继承模式
# Standard controller
from frappe.model.document import Document
class MyDoc(Document): pass
# Tree DocType (hierarchical)
from frappe.utils.nestedset import NestedSet
class Department(NestedSet):
nsm_parent_field = "parent_department"
# Virtual DocType (no database table)
class ExternalData(Document):
def load_from_db(self): ...
def db_insert(self, *args, **kwargs): ...
def db_update(self, *args, **kwargs): ...
@staticmethod
def get_list(args): ...
@staticmethod
def get_count(args): ...
类型注解 [v15+]
class Person(Document):
if TYPE_CHECKING:
from frappe.types import DF
first_name: DF.Data
last_name: DF.Data
birth_date: DF.Date
company: DF.Link
在 hooks.py 中启用自动生成:export_python_type_annotations = True
版本差异
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
| 类型注解 | 无 | 自动生成 | 有 |
before_discard / on_discard | 无 | 有 | 有 |
flags.notify_update | 无 | 有 | 有 |
extend_doctype_class | 无 | 无 | 有 |
| UUID 自动命名 | 无 | 无 | 有 |
| 旧式格式命名 | 有 | 有 | 已弃用 |
参考文件
| 文件 | 内容 |
|---|---|
| lifecycle-methods.md | 所有钩子及执行顺序图 |
| document-api-complete.md | 完整的 Document API:按类别列出的所有方法(CRUD、字段、数据库、权限、标志、子表、命名) |
| methods.md | Document 类方法签名 |
| events.md | 按顺序列出的所有文档事件 |
| examples.md | 完整可用的控制器示例 |
| anti-patterns.md | 常见错误及修正方法 |
| flags.md | 标志系统(doc.flags、frappe.flags) |
| hooks.md | 控制器与 hooks.py 的交互 |
| patterns.md | 常见控制器模式 |
| syntax.md | 控制器类语法参考 |
相关技能
frappe-syntax-serverscripts-- 服务器脚本(沙盒替代方案)frappe-syntax-hooks-- hooks.py 配置frappe-impl-controllers-- 实现工作流frappe-core-permissions-- 权限系统
评论
支持 Markdown