关于 frappe-errors-controllers
面向 Frappe 开发者的控制器错误排查指南,按生命周期阶段和错误信息说明原因与修复方式,并整理钩子、事务及权限处理中的常见问题。
主要功能
- 01按生命周期阶段诊断错误
- 02错误信息、原因与修复对照
- 03命名与验证故障排查
- 04钩子覆盖及父类调用规则
- 05事务回滚时机说明
- 06权限查询与递归防护建议
适用场景
- 01排查控制器生命周期错误
- 02修复命名和提交失败
- 03避免钩子递归及事务问题
- 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.mdexamples.mdpatterns.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 set | DocType 使用了 naming_series,但缺少相应字段 | 在 DocType 中添加 naming_series 字段,或在控制器中设置 autoname |
DuplicateEntryError | autoname 生成了非唯一名称 | 使用带计数器的 naming_series,或添加哈希后缀 |
Maximum recursion depth exceeded | 在 validate/on_update 中调用了 self.save() | 切勿在钩子中调用 self.save();在 on_update 中使用 self.db_set() |
Not allowed to submit | DocType 未设置 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 error | WHERE 子句片段格式错误 | 独立测试 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_submit | docstatus = 1——显示错误,但文档已提交 |
on_cancel | docstatus = 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"
)
务必遵守/切勿违反的规则
始终
- 在重写的钩子中调用
super().method()— 保留父类逻辑 - 在
before_submit中验证,而不是在on_submit中 — 这是最后一个可以安全中止的时机 - 在
on_update中使用self.db_set()— 直接执行self.field = x会被丢弃 - 使用
self.flags防止递归 — 避免循环触发钩子 - 在
on_cancel中隔离清理操作 — 不要让一个失败导致全部操作停止 - 在权限查询中使用
frappe.db.escape()— 防止 SQL 注入 - 从
has_permission显式返回 True/False — 返回 None 会回退到默认行为 - 对意外异常使用
frappe.log_error()— 绝不要默默吞掉异常 - 对所有面向用户的错误消息使用
_()包装 — 启用翻译
绝不
- 绝不要在
validate/on_update中调用self.save()— 会导致无限递归 - 绝不要在控制器中调用
frappe.db.commit()— 事务由框架管理 - 绝不要在
on_submit中进行阻断式验证 — 文档已经提交 - 绝不要在重写的方法中跳过
super()— 会破坏父类逻辑 - 绝不要从
has_permission返回 None — 会导致不可预测的结果 - 绝不要使用裸
except: pass吞掉异常 — 始终记录错误 - 当
extend_doctype_class可用时,绝不要使用override_doctype_class[v16+] - 绝不要在
validate中执行繁重操作 — 在on_update中使用frappe.enqueue()
参考文件
| 文件 | 内容 |
|---|---|
references/examples.md | 包含诊断信息的真实控制器错误场景 |
references/anti-patterns.md | 常见控制器错误及修正方法 |
references/patterns.md | 按生命周期钩子分类的防御式错误处理模式 |
评论
支持 Markdown