mcpField
F
frappe-syntax-controllers

frappe-syntax-controllers

github.com/impertio-studio开发198111 次安装30 周前更新
代码仓库

关于 frappe-syntax-controllers

面向 Frappe 开发者的 DocType 控制器语法参考,涵盖生命周期、命名与扩展方式,并整理常见规则和错误模式。

主要功能

  1. 01控制器文件与类命名规范
  2. 02文档生命周期钩子顺序
  3. 03文档命名与 autoname 方法
  4. 04控制器扩展机制对比
  5. 05白名单方法与文档状态规则

适用场景

  1. 01实现 DocType 服务端逻辑
  2. 02选择合适的生命周期钩子
  3. 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.md
document-api-complete.md
events.md
examples.md
flags.md
hooks.md
lifecycle-methods.md
methods.md
patterns.md
syntax.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 OrderSalesOrderselling/doctype/sales_order/sales_order.py
My Custom DocMyCustomDocmodule/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:fieldnamefield:customer_nameABC CompanyAll
naming_series:naming_series:SO-2024-00001All
表达式PRE-.#####PRE-00001All
旧式格式INV-{YYYY}-{####}INV-2024-0001Deprecated v16
hash / randomhasha1b2c3d4e5All
PromptPrompt用户输入名称All
autoincrementautoincrement1、2、3All
UUIDUUID550e8400-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


版本差异

功能v14v15v16
类型注解无自动生成有
before_discard / on_discard无有有
flags.notify_update无有有
extend_doctype_class无无有
UUID 自动命名无无有
旧式格式命名有有已弃用

参考文件

文件内容
lifecycle-methods.md所有钩子及执行顺序图
document-api-complete.md完整的 Document API:按类别列出的所有方法(CRUD、字段、数据库、权限、标志、子表、命名)
methods.mdDocument 类方法签名
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