Frappe_Claude_Skill_Package frappe-errors-controllers

install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/errors/frappe-errors-controllers" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-errors-controllers && rm -rf "$T"
manifest: skills/source/errors/frappe-errors-controllers/SKILL.md
source content

Controller Errors — Diagnosis and Resolution

Cross-refs:

frappe-syntax-controllers
(syntax),
frappe-impl-controllers
(workflows),
frappe-errors-serverscripts
(server scripts).


Error Diagnosis by Lifecycle Phase

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

Error Message → Cause → Fix Table

Error MessageCauseFix
NamingSeries is not set
DocType uses naming_series but field is missingAdd
naming_series
field to DocType or set
autoname
in controller
DuplicateEntryError
autoname
generated non-unique name
Use
naming_series
with counter, or add hash suffix
Maximum recursion depth exceeded
self.save()
called in validate/on_update
NEVER call
self.save()
in hooks; use
self.db_set()
in on_update
Not allowed to submit
DocType lacks
is_submittable = 1
Enable "Is Submittable" in DocType settings
Cannot cancel: linked docs exist
Submitted linked documents block cancellationCancel linked docs first, or use
before_cancel
to check
AttributeError: super()
Missing
super()
call in overridden hook
ALWAYS call
super().method_name()
first in overrides
Value missing for: field
Controller validate skipped parent logicEnsure
super().validate()
is called
frappe.db.commit() breaks transactions
Manual commit in controller hookNEVER call
frappe.db.commit()
in controllers
Changes lost in on_update
Set
self.field = x
instead of
self.db_set()
Use
self.db_set("field", value)
after save hooks
NestedSet: root cannot be child
Parent set to itself or circular referenceValidate parent != self in validate, check
lft
/
rgt
extend_doctype_class conflict
[v16+]
Multiple apps extend same class with conflicting methodsUse MRO-aware design, check method resolution order
has_permission returns wrong result
Function returns None instead of True/FalseALWAYS return explicit True or False
permission_query_conditions SQL error
Malformed WHERE clause fragmentTest conditions string independently, use
frappe.db.escape()

Critical Error Patterns

1. Autoname Failures

# ❌ 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 options:

naming_series
,
field:fieldname
,
format:PREFIX-{fieldname}-.####
,
hash
,
Prompt
, or custom
autoname()
method.

2. Validate Loop: self.save() in Hooks

# ❌ 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. on_submit Without is_submittable

# ❌ 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 Lifecycle Hook: Error Timing

# ❌ 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

Transaction Rollback Rules by Hook:

Hook
frappe.throw()
Effect
validate
/
before_save
Full rollback — document NOT saved
before_submit
Full rollback — stays Draft
before_cancel
Full rollback — stays Submitted
on_update
/
after_insert
Document IS saved — error shown but doc persists
on_submit
docstatus = 1 — error shown but ALREADY submitted
on_cancel
docstatus = 2 — error shown but ALREADY cancelled

5. Missing super() in Overrides

# ❌ 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()

Resolution order:

class ExtendedSalesOrder(Mixin2, Mixin1, OriginalSalesOrder)
— last mixin listed has highest priority.

7. Flags for Recursion Guard

# ❌ 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 Permission Errors

# ❌ 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))

Note:

permission_query_conditions
affects
frappe.db.get_list()
only, NOT
frappe.db.get_all()
.

9. NestedSet Errors

# ❌ 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: Isolate Cleanup Operations

# ❌ 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"
        )

ALWAYS / NEVER Rules

ALWAYS

  1. Call
    super().method()
    in overridden hooks
    — Preserve parent logic
  2. Validate in
    before_submit
    not
    on_submit
    — Last clean abort point
  3. Use
    self.db_set()
    in
    on_update
    — Direct
    self.field = x
    is lost
  4. Use
    self.flags
    for recursion guards
    — Prevent circular hook triggers
  5. Isolate cleanup operations in
    on_cancel
    — Don't let one failure stop all
  6. Use
    frappe.db.escape()
    in permission queries
    — Prevent SQL injection
  7. Return explicit True/False from
    has_permission
    — None falls back to default
  8. Use
    frappe.log_error()
    for unexpected exceptions
    — Never swallow silently
  9. Use
    _()
    wrapper for all user-facing error messages
    — Enable translation

NEVER

  1. NEVER call
    self.save()
    in validate/on_update
    — Causes infinite recursion
  2. NEVER call
    frappe.db.commit()
    in controllers
    — Framework manages transactions
  3. NEVER put blocking validation in
    on_submit
    — Document already submitted
  4. NEVER skip
    super()
    in overridden methods
    — Breaks parent class logic
  5. NEVER return None from
    has_permission
    — Returns unpredictable results
  6. NEVER swallow exceptions with bare
    except: pass
    — Always log errors
  7. NEVER use
    override_doctype_class
    when
    extend_doctype_class
    works
    [v16+]
  8. NEVER put heavy operations in
    validate
    — Use
    frappe.enqueue()
    from
    on_update

Reference Files

FileContents
references/examples.md
Real controller error scenarios with diagnosis
references/anti-patterns.md
Common controller mistakes with fixes
references/patterns.md
Defensive error handling patterns by lifecycle hook