erpnext-impl-hooks

ERPNext Hooks - Implementation

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-impl-hooks" with this command: npx skills add openaec-foundation/erpnext_anthropic_claude_development_skill_package/openaec-foundation-erpnext-anthropic-claude-development-skill-package-erpnext-impl-hooks

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 &#x3C; 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 &#x3C; 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

Hook Signature

doc_events def handler(doc, method=None):

rename events def handler(doc, method, old, new, merge):

scheduler_events def handler(): (no args)

extend_bootinfo def handler(bootinfo):

permission_query def handler(user): → returns SQL string

has_permission def handler(doc, user=None, permission_type=None): → True/False/None

override methods Must 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)

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

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

  1. Always migrate after hooks changes

Required after ANY hooks.py change

bench --site sitename migrate

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

Feature V14 V15 V16

doc_events ✅ ✅ ✅

scheduler_events ✅ ✅ ✅

override_doctype_class ✅ ✅ ✅

extend_doctype_class ❌ ❌ ✅

permission hooks ✅ ✅ ✅

Scheduler tick 4 min 4 min 60 sec

Reference Files

File Contents

decision-tree.md Complete hook selection flowcharts

workflows.md Step-by-step implementation patterns

examples.md Working code examples

anti-patterns.md Common mistakes and solutions

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