Skill125 estrellas del repoactualizado 2mo ago
frappe-syntax-controllers
>
Instalar en Claude Code
Copiargit clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-syntax-controllers && cp -r /tmp/frappe-syntax-controllers/skills/source/syntax/frappe-syntax-controllers ~/.claude/skills/frappe-syntax-controllersDespués abre una sesión nueva de Claude Code; el skill carga automáticamente.
Definición
SKILL.md
# Frappe Syntax: Document Controllers
Document Controllers are Python classes that define all server-side logic for a DocType.
EVERY DocType has exactly one controller file. The controller class extends `frappe.model.document.Document`.
## Quick Reference
```python
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}
```
### File Location and Naming
| DocType Name | Class Name | File Path |
|---|---|---|
| Sales Order | `SalesOrder` | `selling/doctype/sales_order/sales_order.py` |
| My Custom Doc | `MyCustomDoc` | `module/doctype/my_custom_doc/my_custom_doc.py` |
**Rule**: DocType name -> PascalCase class -> snake_case filename. ALWAYS match exactly.
---
## Lifecycle Hook Execution Order
### INSERT (new document)
```
before_insert -> before_naming -> autoname -> before_validate -> validate
-> before_save -> [db_insert] -> after_insert -> on_update -> on_change
```
### SAVE (existing document)
```
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
```
### UPDATE AFTER SUBMIT
```
before_update_after_submit -> [db_update]
-> on_update_after_submit -> on_change
```
### DELETE
```
on_trash -> [db_delete] -> after_delete
```
### DISCARD [v15+]
```
before_discard -> [db_set docstatus=2] -> on_discard
```
**Complete hook reference with parameters**: See [lifecycle-methods.md](references/lifecycle-methods.md)
---
## Hook Selection Decision Tree
```
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)
```
---
## Critical Rules
### 1. Changes after on_update are NOT saved
```python
# 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. NEVER call frappe.db.commit() in controllers
```python
# 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. ALWAYS call super() when overriding
```python
# 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. Use flags for recursion prevention
```python
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. NEVER put validation logic in on_update
```python
# 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
```
---
## Document Naming (autoname)
| Method | Example | Result | Version |
|---|---|---|---|
| `field:fieldname` | `field:customer_name` | `ABC Company` | All |
| `naming_series:` | `naming_series:` | `SO-2024-00001` | All |
| Expression | `PRE-.#####` | `PRE-00001` | All |
| Old-style format | `INV-{YYYY}-{####}` | `INV-2024-0001` | Deprecated v16 |
| `hash` / `random` | `hash` | `a1b2c3d4e5` | All |
| `Prompt` | `Prompt` | User enters name | All |
| `autoincrement` | `autoincrement` | `1`, `2`, `3` | All |
| **`UUID`** | `UUID` | `550e8400-e29b-...` | **v16+** |
| Custom method | `autoname()` in controller | Any pattern | All |
### Custom autoname Method
```python
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 Naming [v16+]
Set `autoname = "UUID"` in DocType definit