erpnext-syntax-controllers
27
总安装量
8
周安装量
#13733
全站排名
安装命令
npx skills add https://github.com/openaec-foundation/erpnext_anthropic_claude_development_skill_package --skill erpnext-syntax-controllers
Agent 安装分布
claude-code
7
opencode
5
github-copilot
5
codex
5
amp
5
Skill 文档
ERPNext 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