Get the FREE Ultimate OpenClaw Setup Guide →

erpnext-impl-hooks

npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-impl-hooks --openclaw
Files (1)
SKILL.md
13.2 KB

ERPNext Hooks - Implementation

This skill helps you determine HOW to implement hooks.py configurations. For exact syntax, see erpnext-syntax-hooks.

Version: v14/v15/v16 compatible (with V16-specific features noted)

Main Decision: What Are You Trying to Do?

┌─────────────────────────────────────────────────────────────────────────┐
│ WHAT DO YOU WANT TO ACHIEVE?                                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│ ► React to document events on OTHER apps' DocTypes?                     │
│   └── doc_events in hooks.py                                            │
│                                                                         │
│ ► Run code periodically (hourly, daily, custom schedule)?               │
│   └── scheduler_events                                                  │
│                                                                         │
│ ► Modify behavior of existing DocType controller?                       │
│   ├── V16+: extend_doctype_class (RECOMMENDED - multiple apps work)     │
│   └── V14/V15: override_doctype_class (last app wins)                   │
│                                                                         │
│ ► Modify existing API endpoint behavior?                                │
│   └── override_whitelisted_methods                                      │
│                                                                         │
│ ► Add custom permission logic?                                          │
│   ├── List filtering: permission_query_conditions                       │
│   └── Document-level: has_permission                                    │
│                                                                         │
│ ► Send data to client on page load?                                     │
│   └── extend_bootinfo                                                   │
│                                                                         │
│ ► Export/import configuration between sites?                            │
│   └── fixtures                                                          │
│                                                                         │
│ ► Add JS/CSS to desk or portal?                                         │
│   ├── Desk: app_include_js/css                                          │
│   ├── Portal: web_include_js/css                                        │
│   └── Specific form: doctype_js                                         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Decision Tree: doc_events vs Controller Methods

WHERE IS THE DOCTYPE?
│
├─► DocType is in YOUR custom app?
│   └─► Use controller methods (doctype/xxx/xxx.py)
│       - Direct control over lifecycle
│       - Cleaner code organization
│
├─► DocType is in ANOTHER app (ERPNext, Frappe)?
│   └─► Use doc_events in hooks.py
│       - Only way to hook external DocTypes
│       - Can register multiple handlers
│
└─► Need to hook ALL DocTypes (logging, audit)?
    └─► Use doc_events with wildcard "*"

Rule: Controller methods for YOUR DocTypes, doc_events for OTHER apps' DocTypes.


Decision Tree: Which doc_event?

WHAT DO YOU NEED TO DO?
│
├─► Validate data or calculate fields?
│   ├─► Before any save → validate
│   └─► Only on new documents → before_insert
│
├─► React after document is saved?
│   ├─► Only first save → after_insert
│   ├─► Every save → on_update
│   └─► ANY change (including db_set) → on_change
│
├─► Handle submittable documents?
│   ├─► Before submit → before_submit
│   ├─► After submit → on_submit (ledger entries here)
│   ├─► Before cancel → before_cancel
│   └─► After cancel → on_cancel (reverse entries here)
│
├─► Handle document deletion?
│   ├─► Before delete (can prevent) → on_trash
│   └─► After delete (cleanup) → after_delete
│
└─► Handle document rename?
    ├─► Before rename → before_rename
    └─► After rename → after_rename

Decision Tree: Scheduler Event Type

HOW LONG DOES YOUR TASK RUN?
│
├─► < 5 minutes
│   │
│   │ HOW OFTEN?
│   ├─► Every ~60 seconds → all
│   ├─► Every hour → hourly
│   ├─► Every day → daily
│   ├─► Every week → weekly
│   ├─► Every month → monthly
│   └─► Specific time → cron
│
└─► > 5 minutes (up to 25 minutes)
    │
    │ HOW OFTEN?
    ├─► Every hour → hourly_long
    ├─► Every day → daily_long
    ├─► Every week → weekly_long
    └─► Every month → monthly_long

⚠️ Tasks > 25 minutes: Split into chunks or use background jobs

Decision Tree: Override vs Extend (V16)

FRAPPE VERSION?
│
├─► V16+
│   │
│   │ WHAT DO YOU NEED?
│   ├─► Add methods/properties to DocType?
│   │   └─► extend_doctype_class (RECOMMENDED)
│   │       - Multiple apps can extend same DocType
│   │       - Safer, less breakage on updates
│   │
│   └─► Completely replace controller logic?
│       └─► override_doctype_class (use sparingly)
│
└─► V14/V15
    └─► override_doctype_class (only option)
        ⚠️ Last installed app wins!
        ⚠️ Always call super() in methods!

Implementation Workflow: doc_events

Step 1: Add to hooks.py

# myapp/hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.sales_invoice.validate",
        "on_submit": "myapp.events.sales_invoice.on_submit"
    }
}

Step 2: Create handler module

# myapp/events/sales_invoice.py
import frappe

def validate(doc, method=None):
    """
    Args:
        doc: The document object
        method: Event name ("validate")
    
    Changes to doc ARE saved (before save event)
    """
    if doc.grand_total < 0:
        frappe.throw("Total cannot be negative")
    
    # Calculate custom field
    doc.custom_margin = doc.grand_total - doc.total_cost

def on_submit(doc, method=None):
    """
    After submit - document already saved
    Use frappe.db.set_value for additional changes
    """
    create_external_record(doc)

Step 3: Deploy

bench --site sitename migrate

Implementation Workflow: scheduler_events

Step 1: Add to hooks.py

# myapp/hooks.py
scheduler_events = {
    "daily": ["myapp.tasks.daily_cleanup"],
    "daily_long": ["myapp.tasks.heavy_processing"],
    "cron": {
        "0 9 * * 1-5": ["myapp.tasks.weekday_report"]
    }
}

Step 2: Create task module

# myapp/tasks.py
import frappe

def daily_cleanup():
    """NO arguments - scheduler calls with no args"""
    old_logs = frappe.get_all(
        "Error Log",
        filters={"creation": ["<", frappe.utils.add_days(None, -30)]},
        pluck="name"
    )
    for name in old_logs:
        frappe.delete_doc("Error Log", name)

def heavy_processing():
    """Long task - use _long variant in hooks"""
    for batch in get_batches():
        process_batch(batch)
        frappe.db.commit()  # Commit per batch for long tasks

Step 3: Deploy and verify

bench --site sitename migrate
bench --site sitename scheduler enable
bench --site sitename scheduler status

Implementation Workflow: extend_doctype_class (V16+)

Step 1: Add to hooks.py

# myapp/hooks.py
extend_doctype_class = {
    "Sales Invoice": ["myapp.extensions.SalesInvoiceMixin"]
}

Step 2: Create mixin class

# myapp/extensions.py
import frappe
from frappe.model.document import Document

class SalesInvoiceMixin(Document):
    """Mixin that extends Sales Invoice"""
    
    @property
    def profit_margin(self):
        """Add computed property"""
        if self.grand_total:
            return ((self.grand_total - self.total_cost) / self.grand_total) * 100
        return 0
    
    def validate(self):
        """Extend validation - ALWAYS call super()"""
        super().validate()
        self.validate_margin()
    
    def validate_margin(self):
        """Custom validation logic"""
        if self.profit_margin < 10:
            frappe.msgprint("Warning: Low margin invoice")

Step 3: Deploy

bench --site sitename migrate

Implementation Workflow: Permission Hooks

Step 1: Add to hooks.py

# myapp/hooks.py
permission_query_conditions = {
    "Sales Invoice": "myapp.permissions.si_query"
}
has_permission = {
    "Sales Invoice": "myapp.permissions.si_permission"
}

Step 2: Create permission handlers

# myapp/permissions.py
import frappe

def si_query(user):
    """
    Returns SQL WHERE clause for list filtering.
    ONLY works with get_list, NOT get_all!
    """
    if not user:
        user = frappe.session.user
    
    if "Sales Manager" in frappe.get_roles(user):
        return ""  # No filter - see all
    
    # Regular users see only their own
    return f"`tabSales Invoice`.owner = {frappe.db.escape(user)}"

def si_permission(doc, user=None, permission_type=None):
    """
    Document-level permission check.
    Return: True (allow), False (deny), None (use default)
    
    NOTE: Can only DENY, not grant additional permissions!
    """
    if permission_type == "write" and doc.status == "Closed":
        return False  # Deny write on closed invoices
    
    return None  # Use default permission system

Quick Reference: Handler Signatures

HookSignature
doc_eventsdef handler(doc, method=None):
rename eventsdef handler(doc, method, old, new, merge):
scheduler_eventsdef handler(): (no args)
extend_bootinfodef handler(bootinfo):
permission_querydef handler(user): → returns SQL string
has_permissiondef handler(doc, user=None, permission_type=None): → True/False/None
override methodsMust match original signature exactly

Critical Rules

1. Never commit in doc_events

# ❌ WRONG - breaks transaction
def on_update(doc, method=None):
    frappe.db.commit()

# ✅ CORRECT - Frappe commits automatically
def on_update(doc, method=None):
    update_related(doc)

2. Use db_set_value after on_update

# ❌ WRONG - change is lost
def on_update(doc, method=None):
    doc.status = "Processed"

# ✅ CORRECT
def on_update(doc, method=None):
    frappe.db.set_value(doc.doctype, doc.name, "status", "Processed")

3. Always call super() in overrides

# ❌ WRONG - breaks core functionality
class CustomInvoice(SalesInvoice):
    def validate(self):
        self.my_validation()

# ✅ CORRECT
class CustomInvoice(SalesInvoice):
    def validate(self):
        super().validate()  # FIRST!
        self.my_validation()

4. Always migrate after hooks changes

# Required after ANY hooks.py change
bench --site sitename migrate

5. permission_query only works with get_list

# ❌ NOT filtered by permission_query_conditions
frappe.db.get_all("Sales Invoice", filters={})

# ✅ Filtered by permission_query_conditions
frappe.db.get_list("Sales Invoice", filters={})

Version Differences

FeatureV14V15V16
doc_events
scheduler_events
override_doctype_class
extend_doctype_class
permission hooks
Scheduler tick4 min4 min60 sec

Reference Files

FileContents
decision-tree.mdComplete hook selection flowcharts
workflows.mdStep-by-step implementation patterns
examples.mdWorking code examples
anti-patterns.mdCommon mistakes and solutions

Source

git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/impl/erpnext-impl-hooks/SKILL.mdView on GitHub

Overview

This skill helps you decide how to implement hooks.py configurations in ERPNext. It covers doc_events, scheduler_events, override vs extend for Doctype controllers, permission hooks, extend_bootinfo, fixtures, and asset includes, with V16 extend_doctype_class notes. It clarifies when to use each hook and how to structure decisions across apps.

How This Skill Works

Use the decision trees in the skill to determine the appropriate hook for a given goal (e.g., doc_events for external DocTypes, scheduler_events for periodic tasks). For Doctype behavior, prefer extend_doctype_class in V16 and understand override_doctype_class in V14/V15. The guidance also covers permission hooks, boot info extension, fixture exports, and asset includes to configure client assets. The approach emphasizes targeting the right scope (your app vs external apps) and testing changes in staging.

When to Use It

  • Hook external DocTypes (your app interacts with other apps) using doc_events in hooks.py
  • Run code on a schedule (hourly, daily, or custom) with scheduler_events
  • Modify Doctype behavior: extend_doctype_class (V16 recommended) or override_doctype_class (V14/V15) depending on app scope
  • Customize API behavior with override_whitelisted_methods and permission hooks
  • Share or move configurations between sites using fixtures and extend_bootinfo to pass data to the client

Quick Start

  1. Step 1: Define the business goal (e.g., react to external DocType events or schedule a task)
  2. Step 2: Pick the appropriate hook (doc_events, scheduler_events, extend_doctype_class, etc.) and implement in hooks.py
  3. Step 3: Test in a dev/staging site, then iterate and deploy with fixtures for site-to-site consistency

Best Practices

  • Prefer extend_doctype_class (V16) for cross-app Doctype modifications; use override_doctype_class only if extension is not feasible
  • Use doc_events for external DocTypes; reserve controller methods for YOUR DocTypes
  • Document decisions and keep hooks.py organized by purpose and scope
  • Test hooks in a safe staging environment before production rollout
  • Use fixtures to migrate configurations and extend_bootinfo to prefill client data where needed

Example Use Cases

  • Hook into an external app's DocType changes with doc_events to log edits or enforce custom validation
  • Schedule a nightly data archival task via scheduler_events
  • Extend the Doctype class in V16 to add cross-app lifecycle hooks without overriding core behavior
  • Add a has_permission hook to restrict access to a sensitive DocType based on user roles
  • Export hooks configuration with fixtures and leverage extend_bootinfo to send initial data to the client on load

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers