Back to list
OpenAEC-Foundation

erpnext-syntax-controllers

by OpenAEC-Foundation

28 deterministic Claude AI skills for flawless ERPNext/Frappe v14-16 development. Agent Skills standard compliant.

1🍴 1📅 Jan 23, 2026

SKILL.md


name: erpnext-syntax-controllers description: "Deterministic syntax for Frappe Document Controllers (Python server-side). Use when Claude needs to generate code for DocType controllers, lifecycle hooks (validate, on_update, on_submit, etc.), document methods, controller override, submittable documents, or when questions concern controller structure, naming conventions, autoname patterns, UUID naming (v16), or the flags system. Triggers: document controller, controller hook, validate, on_update, on_submit, autoname, naming series, UUID naming, flags system."

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

DocTypeClassFile
Sales OrderSalesOrderselling/doctype/sales_order/sales_order.py
Custom DocCustomDocmodule/doctype/custom_doc/custom_doc.py

Rule: DocType name → PascalCase (remove spaces) → snake_case filename


Most Used Hooks

HookWhenTypical Use
validateBefore every saveValidation, calculations
on_updateAfter every saveNotifications, linked docs
after_insertAfter new docCreation-only actions
on_submitAfter submitLedger entries, stock
on_cancelAfter cancelReverse ledger entries
on_trashBefore deleteCleanup related data
autonameOn namingCustom 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

OptionExampleResultVersion
field:fieldnamefield:customer_nameABC CompanyAll
naming_series:naming_series:SO-2024-00001All
format:PREFIX-{##}format:INV-{YYYY}-{####}INV-2024-0001All
hashhasha1b2c3d4e5All
PromptPromptUser enters nameAll
UUIDUUID01948d5f-...v16+
Custom methodController autoname()Any patternAll

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

PatternDescriptionExample
{#}Counter1, 2, 3
{##}Zero-padded counter01, 02, 03
{####}4-digit counter0001, 0002
{YYYY}Full year2024
{YY}2-digit year24
{MM}Month01-12
{DD}Day01-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:

docstatusStatusEditableCan go to
0Draft✅ Yes1 (Submit)
1Submitted❌ No2 (Cancel)
2Cancelled❌ 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

FileContents
lifecycle-methods.mdAll hooks, execution order, examples
methods.mdAll doc.* methods with signatures
flags.mdFlags system documentation
examples.mdComplete working controller examples
anti-patterns.mdCommon mistakes and corrections

Version Differences

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


  • erpnext-syntax-serverscripts – Server Scripts (sandbox alternative)
  • erpnext-syntax-hooks – hooks.py configuration
  • erpnext-impl-controllers – Implementation workflows

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon