$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-errors-controllersUse when debugging or preventing errors in Frappe Document Controllers. Prevents autoname failures, validate loops, on_submit without is_submittable, wrong lifecycle hook choice, get_list permission errors, NestedSet errors, extend_doctype_class conflicts, missing super() calls,
| 1 | # Controller Errors — Diagnosis and Resolution |
| 2 | |
| 3 | Cross-refs: `frappe-syntax-controllers` (syntax), `frappe-impl-controllers` (workflows), `frappe-errors-serverscripts` (server scripts). |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Error Diagnosis by Lifecycle Phase |
| 8 | |
| 9 | ``` |
| 10 | CONTROLLER ERROR |
| 11 | │ |
| 12 | ├─► NAMING PHASE (autoname / before_naming) |
| 13 | │ ├─► NamingSeries not set → Add naming_series field or autoname property |
| 14 | │ ├─► DuplicateEntryError → Name collision, check uniqueness |
| 15 | │ └─► "name cannot be set directly" → Use autoname method, not self.name = x |
| 16 | │ |
| 17 | ├─► VALIDATION PHASE (before_validate / validate / before_save) |
| 18 | │ ├─► Infinite recursion → doc.save() called inside validate |
| 19 | │ ├─► Validation skipped → Missing super().validate() in override |
| 20 | │ └─► Wrong error timing → Use validate, not on_update, to block save |
| 21 | │ |
| 22 | ├─► SAVE PHASE (before_save / on_update / after_insert) |
| 23 | │ ├─► Changes lost in on_update → Use db_set(), not self.field = x |
| 24 | │ ├─► Infinite loop → self.save() in on_update triggers on_update again |
| 25 | │ └─► Transaction broken → frappe.db.commit() in controller (DON'T) |
| 26 | │ |
| 27 | ├─► SUBMIT PHASE (before_submit / on_submit) |
| 28 | │ ├─► "Not allowed to submit" → DocType missing is_submittable = 1 |
| 29 | │ ├─► Partial state → Validation in on_submit (too late, already submitted) |
| 30 | │ └─► Stock/GL failures → Entries fail but docstatus already = 1 |
| 31 | │ |
| 32 | ├─► CANCEL PHASE (before_cancel / on_cancel) |
| 33 | │ ├─► "Cannot cancel: linked docs" → Check and handle linked documents |
| 34 | │ └─► Partial cleanup → One reversal fails, rest skipped |
| 35 | │ |
| 36 | └─► PERMISSION PHASE (has_permission / get_list) |
| 37 | ├─► "Not permitted" → has_permission returns None (should be True/False) |
| 38 | ├─► get_list returns nothing → permission_query_conditions SQL error |
| 39 | └─► SQL injection → User input in conditions without escape |
| 40 | ``` |
| 41 | |
| 42 | --- |
| 43 | |
| 44 | ## Error Message → Cause → Fix Table |
| 45 | |
| 46 | | Error Message | Cause | Fix | |
| 47 | |---------------|-------|-----| |
| 48 | | `NamingSeries is not set` | DocType uses naming_series but field is missing | Add `naming_series` field to DocType or set `autoname` in controller | |
| 49 | | `DuplicateEntryError` | `autoname` generated non-unique name | Use `naming_series` with counter, or add hash suffix | |
| 50 | | `Maximum recursion depth exceeded` | `self.save()` called in validate/on_update | NEVER call `self.save()` in hooks; use `self.db_set()` in on_update | |
| 51 | | `Not allowed to submit` | DocType lacks `is_submittable = 1` | Enable "Is Submittable" in DocType settings | |
| 52 | | `Cannot cancel: linked docs exist` | Submitted linked documents block cancellation | Cancel linked docs first, or use `before_cancel` to check | |
| 53 | | `AttributeError: super()` | Missing `super()` call in overridden hook | ALWAYS call `super().method_name()` first in overrides | |
| 54 | | `Value missing for: field` | Controller validate skipped parent logic | Ensure `super().validate()` is called | |
| 55 | | `frappe.db.commit() breaks transactions` | Manual commit in controller hook | NEVER call `frappe.db.commit()` in controllers | |
| 56 | | `Changes lost in on_update` | Set `self.field = x` instead of `self.db_set()` | Use `self.db_set("field", value)` after save hooks | |
| 57 | | `NestedSet: root cannot be child` | Parent set to itself or circular reference | Validate parent != self in validate, check `lft`/`rgt` | |
| 58 | | `extend_doctype_class conflict` [v16+] | Multiple apps extend same class with conflicting methods | Use MRO-aware design, check method resolution order | |
| 59 | | `has_permission returns wrong result` | Function returns None instead of True/False | ALWAYS return explicit True or False | |
| 60 | | `permission_query_conditions SQL error` | Malformed WHERE clause fragment | Test conditions string independently, use `frappe.db.escape()` | |
| 61 | |
| 62 | --- |
| 63 | |
| 64 | ## Critical Error Patterns |
| 65 | |
| 66 | ### 1. Autoname Failures |
| 67 | |
| 68 | ```python |
| 69 | # ❌ WRONG — Setting name directly fails |
| 70 | class CustomDoc(Document): |
| 71 | def autoname(self): |
| 72 | self.name = f"DOC-{self.customer}" # May cause DuplicateEntryError |
| 73 | |
| 74 | # ✅ CORRECT — Use naming utilities |
| 75 | class CustomDoc(Document): |
| 76 | def autoname(self): |
| 77 | # Option 1: Naming series |
| 78 | from frappe.model.naming import set_name_by_naming_series |
| 79 | set_name_by_naming_series(self) |
| 80 | |
| 81 | # Option 2: Safe format with counter |
| 82 | self.name = frappe.model.n |