erpnext-syntax-controllers
npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-syntax-controllers --openclawERPNext Syntax: Document Controllers
Document Controllers are Python classes that implement the server-side logic of a DocType.
Quick Reference
Controller Basic Structure
import frappe
from frappe.model.document import Document
class SalesOrder(Document):
def validate(self):
"""Main validation - runs on every save."""
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."""
self.update_linked_docs()
Location and Naming
| DocType | Class | File |
|---|---|---|
| Sales Order | SalesOrder | selling/doctype/sales_order/sales_order.py |
| Custom Doc | CustomDoc | module/doctype/custom_doc/custom_doc.py |
Rule: DocType name → PascalCase (remove spaces) → snake_case filename
Most Used Hooks
| Hook | When | Typical Use |
|---|---|---|
validate | Before every save | Validation, calculations |
on_update | After every save | Notifications, linked docs |
after_insert | After new doc | Creation-only actions |
on_submit | After submit | Ledger entries, stock |
on_cancel | After cancel | Reverse ledger entries |
on_trash | Before delete | Cleanup related data |
autoname | On naming | Custom document name |
Complete list and execution order: See lifecycle-methods.md
Hook Selection Decision Tree
What do you want to do?
│
├─► Validate or calculate fields?
│ └─► validate
│
├─► Action after save (emails, linked docs)?
│ └─► on_update
│
├─► Only for NEW docs?
│ └─► after_insert
│
├─► On SUBMIT?
│ ├─► Check beforehand? → before_submit
│ └─► Action afterwards? → on_submit
│
├─► On CANCEL?
│ ├─► Check beforehand? → before_cancel
│ └─► Cleanup? → on_cancel
│
├─► Custom document name?
│ └─► autoname
│
└─► Cleanup before delete?
└─► on_trash
Critical Rules
1. Changes after on_update are NOT saved
# ❌ WRONG - change is lost
def on_update(self):
self.status = "Completed" # NOT saved
# ✅ CORRECT - use db_set
def on_update(self):
frappe.db.set_value(self.doctype, self.name, "status", "Completed")
2. No commits in controllers
# ❌ WRONG - Frappe handles commits
def on_update(self):
frappe.db.commit() # DON'T DO THIS
# ✅ CORRECT - no commit needed
def on_update(self):
self.update_related() # Frappe commits automatically
3. Always call super() when overriding
# ❌ WRONG - parent logic is skipped
def validate(self):
self.custom_check()
# ✅ CORRECT - parent logic is preserved
def validate(self):
super().validate()
self.custom_check()
4. Use flags for recursion prevention
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()
Document Naming (autoname)
Available Naming Options
| Option | Example | Result | Version |
|---|---|---|---|
field:fieldname | field:customer_name | ABC Company | All |
naming_series: | naming_series: | SO-2024-00001 | All |
format:PREFIX-{##} | format:INV-{YYYY}-{####} | INV-2024-0001 | All |
hash | hash | a1b2c3d4e5 | All |
Prompt | Prompt | User enters name | All |
UUID | UUID | 01948d5f-... | v16+ |
| Custom method | Controller autoname() | Any pattern | All |
UUID Naming (v16+)
New in v16: UUID-based naming for globally unique identifiers.
{
"doctype": "DocType",
"autoname": "UUID"
}
Benefits:
- Globally unique across systems
- Better data integrity and traceability
- Reduced database storage
- Faster bulk record creation
- Link fields store UUID in native format
Implementation:
# Frappe automatically generates UUID7
# In naming.py:
if meta.autoname == "UUID":
doc.name = str(uuid_utils.uuid7())
Validation:
# UUID names are validated on import
from uuid import UUID
try:
UUID(doc.name)
except ValueError:
frappe.throw(_("Invalid UUID: {}").format(doc.name))
Custom autoname Method
from frappe.model.naming import getseries
class Project(Document):
def autoname(self):
# Custom naming based on customer
prefix = f"P-{self.customer}-"
self.name = getseries(prefix, 3)
# Result: P-ACME-001, P-ACME-002, etc.
Format Patterns
| Pattern | Description | Example |
|---|---|---|
{#} | Counter | 1, 2, 3 |
{##} | Zero-padded counter | 01, 02, 03 |
{####} | 4-digit counter | 0001, 0002 |
{YYYY} | Full year | 2024 |
{YY} | 2-digit year | 24 |
{MM} | Month | 01-12 |
{DD} | Day | 01-31 |
{fieldname} | Field value | (value) |
Controller Override
Via hooks.py (override_doctype_class)
# hooks.py
override_doctype_class = {
"Sales Order": "custom_app.overrides.CustomSalesOrder"
}
# custom_app/overrides.py
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()
Via doc_events (hooks.py)
# hooks.py
doc_events = {
"Sales Order": {
"validate": "custom_app.events.validate_sales_order",
"on_submit": "custom_app.events.on_submit_sales_order"
}
}
# custom_app/events.py
def validate_sales_order(doc, method):
if doc.total > 100000:
doc.requires_approval = 1
Choice: override_doctype_class for full control, doc_events for individual hooks.
Submittable Documents
Documents with is_submittable = 1 have a docstatus lifecycle:
| docstatus | Status | Editable | Can go to |
|---|---|---|---|
| 0 | Draft | ✅ Yes | 1 (Submit) |
| 1 | Submitted | ❌ No | 2 (Cancel) |
| 2 | Cancelled | ❌ No | - |
class StockEntry(Document):
def on_submit(self):
"""After submit - create stock ledger entries."""
self.update_stock_ledger()
def on_cancel(self):
"""After cancel - reverse the entries."""
self.reverse_stock_ledger()
Virtual DocTypes
For external data sources (no database table):
class ExternalCustomer(Document):
@staticmethod
def get_list(args):
return external_api.get_customers(args.get("filters"))
@staticmethod
def get_count(args):
return external_api.count_customers(args.get("filters"))
@staticmethod
def get_stats(args):
return {}
Inheritance Patterns
Standard Controller
from frappe.model.document import Document
class MyDocType(Document):
pass
Tree DocType
from frappe.utils.nestedset import NestedSet
class Department(NestedSet):
pass
Extend Existing Controller
from erpnext.selling.doctype.sales_order.sales_order import SalesOrder
class CustomSalesOrder(SalesOrder):
def validate(self):
super().validate()
self.custom_validation()
Type Annotations (v15+)
class Person(Document):
if TYPE_CHECKING:
from frappe.types import DF
first_name: DF.Data
last_name: DF.Data
birth_date: DF.Date
Enable in hooks.py:
export_python_type_annotations = True
Reference Files
| File | Contents |
|---|---|
| lifecycle-methods.md | All hooks, execution order, examples |
| methods.md | All doc.* methods with signatures |
| flags.md | Flags system documentation |
| examples.md | Complete working controller examples |
| anti-patterns.md | Common mistakes and corrections |
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| Type annotations | ❌ | ✅ Auto-generated | ✅ |
before_discard hook | ❌ | ✅ | ✅ |
on_discard hook | ❌ | ✅ | ✅ |
flags.notify_update | ❌ | ✅ | ✅ |
| UUID autoname | ❌ | ❌ | ✅ |
| UUID in Link fields (native) | ❌ | ❌ | ✅ |
v16-Specific Notes
UUID Naming:
- Set
autoname = "UUID"in DocType definition - Uses
uuid7()for time-ordered UUIDs - Link fields store UUIDs in native format (not text)
- Improves performance for bulk operations
Choosing UUID vs Traditional Naming:
When to use UUID:
├── Cross-system data synchronization
├── Bulk record creation
├── Global uniqueness required
└── No human-readable name needed
When to use traditional naming:
├── User-facing document references (SO-00001)
├── Sequential numbering required
├── Auditing requires readable names
└── Integration with legacy systems
Anti-Patterns
❌ Direct field change after on_update
def on_update(self):
self.status = "Done" # Will be lost!
❌ frappe.db.commit() in controller
def validate(self):
frappe.db.commit() # Breaks transaction!
❌ Forgetting to call super()
def validate(self):
self.my_check() # Parent validate is skipped
→ See anti-patterns.md for complete list.
Related Skills
erpnext-syntax-serverscripts– Server Scripts (sandbox alternative)erpnext-syntax-hooks– hooks.py configurationerpnext-impl-controllers– Implementation workflows
Source
git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/syntax/erpnext-syntax-controllers/SKILL.mdView on GitHub Overview
This skill provides a deterministic Python-based syntax for ERPNext Document Controllers. It covers lifecycle hooks (validate, on_update, on_submit, etc.), document methods, controller overrides, and naming/UUID patterns, with emphasis on the flags system. It helps you generate robust, repeatable controller structures.
How This Skill Works
Document Controllers are Python classes that extend Document and implement server-side logic for a DocType. Frappe calls hooks like validate, on_update, on_submit at the appropriate lifecycle events, enabling custom behavior. Follow practices such as avoiding manual commits, using frappe.db.set_value to persist changes, and using flags to prevent recursion.
When to Use It
- When you need a DocType controller with lifecycle hooks (validate, on_update, on_submit)
- When adding custom document methods or overriding default behavior
- When implementing autoname, naming_series, or UUID naming (v16)
- When questions concern controller structure, naming conventions, or the flags system
- When building submittable documents that require post-submit actions
Quick Start
- Step 1: Identify required hooks (validate, on_update, on_submit) for your DocType
- Step 2: Create a Python class that extends Document and implement the hooks
- Step 3: Place the file under the DocType path, then test in the Frappe bench and fix issues
Best Practices
- Avoid making commits; rely on Frappe’s automatic commit and use db_set/update patterns
- Always call super() when overriding lifecycle methods
- Use flags to prevent recursive updates across linked docs
- Follow the DocType naming rule: PascalCase class name and snake_case file path
- Test hooks in a safe environment before deploying to production
Example Use Cases
- validate hook enforcing required items and calculating totals
- on_update to update linked documents after a save
- autoname using a naming_series for predictable doc names
- using flags in on_update to avoid recursive saves
- on_submit to create ledger entries or trigger downstream processes