erpnext-syntax-controllers

ERPNext Syntax: Document Controllers

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "erpnext-syntax-controllers" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-syntax-controllers

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

  1. 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

  1. 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()

  1. 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 configuration

  • erpnext-impl-controllers – Implementation workflows

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

erpnext-code-interpreter

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-jinja

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-impl-controllers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

erpnext-syntax-customapp

No summary provided by upstream source.

Repository SourceNeeds Review