mcpField
F
frappe-errors-controllers

frappe-errors-controllers

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

关于 frappe-errors-controllers

面向 Frappe 开发者的控制器错误排查指南,按生命周期阶段和错误信息说明原因与修复方式,并整理钩子、事务及权限处理中的常见问题。

主要功能

  1. 01按生命周期阶段诊断错误
  2. 02错误信息、原因与修复对照
  3. 03命名与验证故障排查
  4. 04钩子覆盖及父类调用规则
  5. 05事务回滚时机说明
  6. 06权限查询与递归防护建议

适用场景

  1. 01排查控制器生命周期错误
  2. 02修复命名和提交失败
  3. 03避免钩子递归及事务问题
  4. 04检查权限查询与取消清理逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-errors-controllers

安装后在 Claude Code 中输入/frappe-errors-controllers确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
anti-patterns.md
examples.md
patterns.md

详细介绍

控制器错误——诊断与解决

交叉引用:frappe-syntax-controllers(语法)、frappe-impl-controllers(工作流)、frappe-errors-serverscripts(服务器脚本)。


按生命周期阶段诊断错误

CONTROLLER ERROR
│
├─► NAMING PHASE (autoname / before_naming)
│   ├─► NamingSeries not set → Add naming_series field or autoname property
│   ├─► DuplicateEntryError → Name collision, check uniqueness
│   └─► "name cannot be set directly" → Use autoname method, not self.name = x
│
├─► VALIDATION PHASE (before_validate / validate / before_save)
│   ├─► Infinite recursion → doc.save() called inside validate
│   ├─► Validation skipped → Missing super().validate() in override
│   └─► Wrong error timing → Use validate, not on_update, to block save
│
├─► SAVE PHASE (before_save / on_update / after_insert)
│   ├─► Changes lost in on_update → Use db_set(), not self.field = x
│   ├─► Infinite loop → self.save() in on_update triggers on_update again
│   └─► Transaction broken → frappe.db.commit() in controller (DON'T)
│
├─► SUBMIT PHASE (before_submit / on_submit)
│   ├─► "Not allowed to submit" → DocType missing is_submittable = 1
│   ├─► Partial state → Validation in on_submit (too late, already submitted)
│   └─► Stock/GL failures → Entries fail but docstatus already = 1
│
├─► CANCEL PHASE (before_cancel / on_cancel)
│   ├─► "Cannot cancel: linked docs" → Check and handle linked documents
│   └─► Partial cleanup → One reversal fails, rest skipped
│
└─► PERMISSION PHASE (has_permission / get_list)
    ├─► "Not permitted" → has_permission returns None (should be True/False)
    ├─► get_list returns nothing → permission_query_conditions SQL error
    └─► SQL injection → User input in conditions without escape

错误消息 → 原因 → 修复方法表

错误消息原因修复方法
NamingSeries is not setDocType 使用了 naming_series,但缺少相应字段在 DocType 中添加 naming_series 字段,或在控制器中设置 autoname
DuplicateEntryErrorautoname 生成了非唯一名称使用带计数器的 naming_series,或添加哈希后缀
Maximum recursion depth exceeded在 validate/on_update 中调用了 self.save()切勿在钩子中调用 self.save();在 on_update 中使用 self.db_set()
Not allowed to submitDocType 未设置 is_submittable = 1在 DocType 设置中启用“Is Submittable”
Cannot cancel: linked docs exist已提交的关联文档阻止取消先取消关联文档,或使用 before_cancel 进行检查
AttributeError: super()重写的钩子中缺少 super() 调用重写方法时,务必先调用 super().method_name()
Value missing for: field控制器的 validate 跳过了父类逻辑确保调用了 super().validate()
frappe.db.commit() breaks transactions在控制器钩子中手动提交切勿在控制器中调用 frappe.db.commit()
Changes lost in on_update使用 self.field = x 而非 self.db_set()在保存钩子之后使用 self.db_set("field", value)
NestedSet: root cannot be child父级被设为自身,或存在循环引用在 validate 中验证 parent != self,并检查 lft/rgt
extend_doctype_class conflict [v16+]多个应用扩展了同一个类,且方法冲突采用兼顾 MRO 的设计,并检查方法解析顺序
has_permission returns wrong result函数返回了 None,而非 True/False务必显式返回 True 或 False
permission_query_conditions SQL errorWHERE 子句片段格式错误独立测试 conditions 字符串,并使用 frappe.db.escape()

关键错误模式

1. Autoname 失败
# ❌ WRONG — Setting name directly fails
class CustomDoc(Document):
    def autoname(self):
        self.name = f"DOC-{self.customer}"  # May cause DuplicateEntryError

# ✅ CORRECT — Use naming utilities
class CustomDoc(Document):
    def autoname(self):
        # Option 1: Naming series
        from frappe.model.naming import set_name_by_naming_series
        set_name_by_naming_series(self)

        # Option 2: Safe format with counter
        self.name = frappe.model.naming.make_autoname(
            f"DOC-.{self.customer}.-.####"
        )

        # Option 3: Hash for guaranteed uniqueness
        # Set autoname = "hash" in DocType JSON instead

Autoname 选项:naming_series、field:fieldname、format:PREFIX-{fieldname}-.####、hash、Prompt 或自定义 autoname() 方法。

2. Validate 循环:在钩子中调用 self.save()
# ❌ WRONG — Infinite recursion
class SalesOrder(Document):
    def validate(self):
        self.calculate_totals()
        self.save()  # Triggers validate again → infinite loop!

    def on_update(self):
        self.status = "Updated"
        self.save()  # Triggers on_update again → infinite loop!

# ✅ CORRECT — Framework handles save; use db_set after save
class SalesOrder(Document):
    def validate(self):
        self.calculate_totals()
        # No save() — framework saves after validate completes

    def on_update(self):
        self.db_set("status", "Updated")  # Direct DB write, no trigger
3. 未设置 is_submittable 却使用 on_submit
# ❌ ERROR — "Not allowed to submit"
class MyDoc(Document):
    def on_submit(self):
        self.create_entries()
# This fails if DocType JSON lacks: "is_submittable": 1

# ✅ FIX — Enable in DocType definition
# In my_doc.json:
# { "is_submittable": 1 }
# Then before_submit and on_submit hooks work
4. 生命周期钩子错误:错误触发时机
# ❌ WRONG — Validation in on_submit (document already submitted!)
class SalesOrder(Document):
    def on_submit(self):
        if not self.has_stock():
            frappe.throw(_("Insufficient stock"))  # docstatus already = 1!

# ✅ CORRECT — ALWAYS validate in before_submit
class SalesOrder(Document):
    def before_submit(self):
        if not self.has_stock():
            frappe.throw(_("Insufficient stock"))  # Clean abort, stays Draft

    def on_submit(self):
        self.create_stock_entries()  # Only post-submit actions here

各钩子的事务回滚规则:

钩子frappe.throw() 的影响
validate / before_save完全回滚——文档未保存
before_submit完全回滚——保持为 Draft
before_cancel完全回滚——保持为 Submitted
on_update / after_insert文档已保存——显示错误,但文档仍保留
on_submitdocstatus = 1——显示错误,但文档已提交
on_canceldocstatus = 2——显示错误,但文档已取消
5. 重写方法时缺少 super()
# ❌ WRONG — Parent validation completely skipped
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder

class CustomSalesOrder(SalesOrder):
    def validate(self):
        # Parent validate() never runs! All ERPNext validations bypassed!
        self.custom_check()

# ✅ CORRECT — ALWAYS call super() first
class CustomSalesOrder(SalesOrder):
    def validate(self):
        super().validate()  # Run all parent validations first
        self.custom_check()  # Then add custom logic
6. extend_doctype_class [v16+]
# In hooks.py — v16+ preferred approach
extend_doctype_class = {
    "Sales Order": ["myapp.overrides.sales_order.SalesOrderMixin"]
}

# myapp/overrides/sales_order.py
class SalesOrderMixin:
    """Mixin class — extends, does not replace."""
    def validate(self):
        super().validate()  # ALWAYS call super — runs original + other mixins
        self.custom_validation()

解析顺序:class ExtendedSalesOrder(Mixin2, Mixin1, OriginalSalesOrder)——最后列出的 mixin 优先级最高。

7. 用于递归保护的标志
# ❌ WRONG — on_update of linked doc triggers this doc's on_update
class SalesOrder(Document):
    def on_update(self):
        self.update_quotation()  # Quotation.on_update triggers back here

# ✅ CORRECT — Use flags to prevent recursion
class SalesOrder(Document):
    def on_update(self):
        if self.flags.get("skip_linked_update"):
            return
        self.flags.skip_linked_update = True
        self.update_quotation()

    def update_quotation(self):
        if self.quotation:
            q = frappe.get_doc("Quotation", self.quotation)
            q.flags.skip_linked_update = True  # Prevent back-trigger
            q.db_set("status", "Ordered")
8. get_list 权限错误
# ❌ WRONG — permission_query_conditions returns None (fallback to no filter)
def get_permission_query(user):
    pass  # Returns None — shows ALL records!

# ❌ WRONG — SQL injection
def get_permission_query(user):
    dept = frappe.db.get_value("User", user, "department")
    return f"department = '{dept}'"  # INJECTION RISK

# ✅ CORRECT — Explicit conditions with escape
def get_permission_query(user):
    if "System Manager" in frappe.get_roles(user):
        return ""  # No filter — full access
    dept = frappe.db.get_value("User", user, "department")
    if dept:
        return f"department = {frappe.db.escape(dept)}"
    return "owner = {0}".format(frappe.db.escape(user))

注意:permission_query_conditions 仅影响 frappe.db.get_list(),不影响 frappe.db.get_all()。

9. NestedSet 错误
# ❌ WRONG — Circular reference causes lft/rgt corruption
class Territory(NestedSet):
    def validate(self):
        # No parent validation!
        pass

# ✅ CORRECT — Validate parent chain
class Territory(NestedSet):
    def validate(self):
        super().validate()
        if self.parent_territory == self.name:
            frappe.throw(_("Territory cannot be its own parent"))
        # NestedSet.validate() checks circular refs automatically
        # but explicit check gives better error message

on_cancel:隔离清理操作

# ❌ WRONG — First failure stops all cleanup
def on_cancel(self):
    self.reverse_stock()     # If this fails...
    self.reverse_gl()        # ...this never runs
    self.update_linked()     # ...neither does this

# ✅ CORRECT — Isolate each reversal
def on_cancel(self):
    errors = []
    for operation, label in [
        (self.reverse_stock, "Stock reversal"),
        (self.reverse_gl, "GL reversal"),
        (self.update_linked, "Linked docs"),
    ]:
        try:
            operation()
        except Exception as e:
            errors.append(f"{label}: {str(e)}")
            frappe.log_error(frappe.get_traceback(), f"{label} Error")

    if errors:
        frappe.msgprint(
            _("Cancelled with errors:<br>{0}").format("<br>".join(errors)),
            indicator="orange"
        )

务必遵守/切勿违反的规则

始终
  1. 在重写的钩子中调用 super().method() — 保留父类逻辑
  2. 在 before_submit 中验证,而不是在 on_submit 中 — 这是最后一个可以安全中止的时机
  3. 在 on_update 中使用 self.db_set() — 直接执行 self.field = x 会被丢弃
  4. 使用 self.flags 防止递归 — 避免循环触发钩子
  5. 在 on_cancel 中隔离清理操作 — 不要让一个失败导致全部操作停止
  6. 在权限查询中使用 frappe.db.escape() — 防止 SQL 注入
  7. 从 has_permission 显式返回 True/False — 返回 None 会回退到默认行为
  8. 对意外异常使用 frappe.log_error() — 绝不要默默吞掉异常
  9. 对所有面向用户的错误消息使用 _() 包装 — 启用翻译
绝不
  1. 绝不要在 validate/on_update 中调用 self.save() — 会导致无限递归
  2. 绝不要在控制器中调用 frappe.db.commit() — 事务由框架管理
  3. 绝不要在 on_submit 中进行阻断式验证 — 文档已经提交
  4. 绝不要在重写的方法中跳过 super() — 会破坏父类逻辑
  5. 绝不要从 has_permission 返回 None — 会导致不可预测的结果
  6. 绝不要使用裸 except: pass 吞掉异常 — 始终记录错误
  7. 当 extend_doctype_class 可用时,绝不要使用 override_doctype_class [v16+]
  8. 绝不要在 validate 中执行繁重操作 — 在 on_update 中使用 frappe.enqueue()

参考文件

文件内容
references/examples.md包含诊断信息的真实控制器错误场景
references/anti-patterns.md常见控制器错误及修正方法
references/patterns.md按生命周期钩子分类的防御式错误处理模式

评论

支持 Markdown