How to Implement Custom Document Validation in ERPNext Hooks: A Complete Guide

You implement custom document validation in ERPNext by defining a doc_events hook in your custom app's hooks.py file, mapping DocTypes and events (like validate) to Python functions that receive the doc object and method name, allowing you to inject validation logic without modifying core ERPNext files.

ERPNext's hook architecture provides a clean extension point for enforcing business rules across any DocType. By leveraging the doc_events dictionary defined in the core erpnext/hooks.py, you can attach custom Python functions to document lifecycle events without touching the framework's source code. This guide demonstrates how to implement custom document validation in ERPNext hooks using the exact patterns found in the frappe/erpnext repository.

How the doc_events Hook Works

The entry point for document-level hooks resides in erpnext/hooks.py at lines 51-57, where ERPNext defines a global doc_events dictionary that maps DocTypes to event handlers. During startup, the framework reads this configuration via frappe.get_hooks("doc_events") and merges it with hooks from all installed custom apps.


# erpnext/hooks.py – doc_events definition (source: lines 51-57)

doc_events = {
    "*": {
        "validate": [
            "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
            "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job",
        ],
    },
}

When a document triggers an event like validate or before_save, ERPNext iterates over the corresponding list and calls each dotted-path method, passing the current doc object as the first argument and the event method name as the second. You can target specific DocTypes by name or use the "*" wildcard to apply logic globally.

Step-by-Step Implementation

Create the Validation Function

Place your validation logic in any Python module importable by your custom app. The function must accept exactly two parameters: doc (the document instance) and method (the event string).


# my_custom_app/validation.py

import frappe
from frappe import _

def validate_sales_order(doc, method):
    """Abort save if total is negative."""
    if doc.total < 0:
        frappe.throw(_("Total amount cannot be negative"))

Register the Hook in hooks.py

Expose the function by adding an entry to your app's hooks.py. The key is the DocType name, the sub-key is the event, and the value is the dotted Python path to your function.


# my_custom_app/hooks.py

doc_events = {
    "Sales Order": {
        "validate": "my_custom_app.validation.validate_sales_order"
    }
}

Deploy Your Custom App

Install the app into your site or add it to sites/apps.txt. Once deployed, ERPNext automatically merges your doc_events with the core dictionary at startup. Every subsequent save operation on the target DocType executes your validation function.

Practical Validation Examples

Mandatory Field Validation by Item Group

Enforce business rules conditionally based on field values. This example requires an SKU for all items categorized under "Electronics".


# my_custom_app/validation.py

def validate_custom_item(doc, method):
    if doc.item_group == "Electronics" and not doc.sku:
        frappe.throw(_("SKU is required for electronic items"))

# my_custom_app/hooks.py

doc_events = {
    "Item": {
        "validate": "my_custom_app.validation.validate_custom_item"
    }
}

Cross-Document Credit Limit Checks

Validation hooks can query related documents. This example blocks Sales Invoice submission if the grand total exceeds the customer's configured credit limit.


# my_custom_app/validation.py

def validate_sales_invoice_credit(doc, method):
    customer = frappe.get_doc("Customer", doc.customer)
    if doc.grand_total > customer.credit_limit:
        frappe.throw(
            _("Invoice total exceeds customer's credit limit of {0}").format(customer.credit_limit)
        )

# my_custom_app/hooks.py

doc_events = {
    "Sales Invoice": {
        "validate": "my_custom_app.validation.validate_sales_invoice_credit"
    }
}

Preventing Critical Document Deletion

Use the before_delete event to block removal of sensitive records. This triggers before the framework removes the document from the database.


# my_custom_app/validation.py

def prevent_deletion_of_critical_doc(doc, method):
    if doc.is_critical:
        frappe.throw(_("Critical documents cannot be deleted"))

# my_custom_app/hooks.py

doc_events = {
    "My Critical Doctype": {
        "before_delete": "my_custom_app.validation.prevent_deletion_of_critical_doc"
    }
}

Technical Architecture Behind the Hooks

ERPNext reads the doc_events dictionary at boot time using frappe.get_hooks("doc_events"), which aggregates entries from all installed apps. When a document event fires, the framework invokes frappe.call() on each registered method, supplying the current document instance. Because this architecture relies on dotted-path strings rather than direct imports, your custom app remains decoupled from core ERPNext code, ensuring seamless upgrades and clean separation of concerns.

Summary

  • Define validation logic in Python functions accepting doc and method parameters.
  • Register functions in your app's hooks.py under the doc_events dictionary, mapping DocTypes and events (e.g., validate, before_delete).
  • Abort transactions by calling frappe.throw() inside your validation function.
  • Target specific DocTypes by name or all DocTypes using the "*" wildcard.
  • Maintain core code integrity by keeping custom logic in separate apps that merge at runtime.

Frequently Asked Questions

What parameters must a document validation function accept?

Your function must accept at least two arguments: doc (the current document object) and method (the string name of the event, such as "validate"). You may include additional keyword arguments, but these two are required for the framework to execute the hook correctly.

How do I target all DocTypes with a single validation hook?

Use the "*" wildcard as the DocType key in your doc_events dictionary. This applies the validation to every document in the system. ERPNext uses this pattern internally at line 52 of erpnext/hooks.py to run global checks like service level agreement validation.

Can I prevent a document from being saved using a validation hook?

Yes. Raise a frappe.throw() exception with a descriptive message inside your validation function. This aborts the database transaction and displays the error to the user, preventing the save operation from completing.

Where does ERPNext read the doc_events configuration from?

The framework reads doc_events from the hooks.py file of every installed app at server startup. It specifically looks for the doc_events dictionary and merges all entries, as seen in the source code at erpnext/hooks.py lines 51-57, creating a unified mapping of DocTypes to their event handlers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →