ERPNext Regional Override System: How to Implement Country-Specific Customizations
The ERPNext regional override system lets developers replace or extend core functionality for specific countries using the @erpnext.allow_regional decorator and the regional_overrides hook, without modifying core source code.
The ERPNext regional override system provides a robust mechanism for tailoring ERP functionality to local tax laws, accounting standards, and business regulations. As implemented in the frappe/erpnext repository, this architecture allows developers to inject country-specific logic while keeping the core codebase clean and upgradable. Understanding how to leverage get_region(), the allow_regional decorator, and the hooks system is essential for building compliant multi-national ERP deployments.
Core Components of the Regional Override System
The architecture consists of three interconnected pieces that work together to route function calls to country-specific implementations.
get_region() Function
Located in erpnext/__init__.py at lines 20-32, the get_region() function determines the active country by checking three sources in order of priority:
frappe.flags.country(typically set by the UI)- The country associated with the current user's company via
frappe.local.flags.company - The global system setting
frappe.get_system_settings("country")
The allow_regional Decorator
Found in erpnext/__init__.py at lines 35-53, this decorator wraps functions to enable dynamic dispatch. At runtime, it intercepts calls to check whether the current country has a registered override in frappe.get_hooks("regional_overrides"). If a match exists, it executes the override; otherwise, it falls back to the original implementation.
regional_overrides Hook
Defined in erpnext/hooks.py at lines 20-33, this dictionary maps country names to dictionaries of original function path → overriding function path. The system prioritizes overrides from the last installed app (lines 45-52), allowing layered customizations where later apps take precedence over earlier ones.
How the Regional Override System Works
When a function decorated with @erpnext.allow_regional is called, ERPNext executes the following resolution process:
- Determine the country –
get_region()resolves the active country using the hierarchy of flags and settings. - Lookup overrides – The decorator queries
frappe.get_hooks("regional_overrides")and retrieves the sub-dictionary for the resolved country. - Match the function – The fully-qualified path of the decorated function (e.g.,
erpnext.controllers.taxes_and_totals.update_itemised_tax_data) serves as the lookup key. - Execute the override – If a matching key exists, the call forwards to the dotted path specified in the hook value; otherwise, the original implementation runs.
- Apply app priority – When multiple apps provide overrides for the same country, the one installed last wins, enabling progressive customization layers.
Where Regional Code Lives in ERPNext
All country-specific logic resides under erpnext/regional/<country>/. For example, the United Arab Emirates implementation at erpnext/regional/united_arab_emirates/utils.py defines utilities such as update_itemised_tax_data and make_regional_gl_entries. The regional_overrides hook in erpnext/hooks.py maps these to their corresponding core function paths:
"United Arab Emirates": {
"erpnext.controllers.taxes_and_totals.update_itemised_tax_data":
"erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data",
"erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries":
"erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries",
}
Step-by-Step Implementation Guide
Follow these steps to implement country-specific customizations using the regional override system:
-
Create a new app (optional) – Scaffold a custom Frappe app to isolate your customizations from core ERPNext code.
-
Add a regional module – Inside your app, create
regional/<your_country>/with an__init__.pyand any helper modules. -
Write the override function – Implement the new behavior with the same signature as the original function you intend to replace.
-
Expose the function via a hook – In your app's
hooks.py, add toregional_overrides:regional_overrides = { "My Country": { "erpnext.controllers.accounts_controller.validate_regional": "my_customizations.regional.my_country.utils.validate_regional" } } -
Verify the original function is decorated – Ensure the core function is already decorated with
@erpnext.allow_regional. Most ERPNext core functions already are; if writing new core functions, add the decorator yourself. -
Install the app – Install your custom app after ERPNext (or after any other app that may provide competing overrides) to ensure your implementation takes precedence.
-
Write tests – Create unit tests following the pattern in
erpnext/tests/test_regional.py, settingfrappe.flags.countryto your target country and asserting that the overridden function executes.
Practical Code Examples
Decorating a Core Function
Most ERPNext core functions already include the decorator:
# In erpnext/controllers/accounts_controller.py (or similar core module)
import erpnext
@erpnext.allow_regional
def validate_regional(self):
"""Default validation logic."""
pass
Implementing a Country-Specific Override
Create your replacement logic in your custom app:
# my_customizations/regional/my_country/utils.py
import frappe
def validate_regional(self):
"""Custom validation logic for My Country."""
if self.tax_id and not self.tax_id.startswith("MC"):
frappe.throw("Tax ID must start with 'MC' for My Country regulations")
Registering the Override in hooks.py
Connect your function to the core system:
# my_customizations/hooks.py
regional_overrides = {
"My Country": {
"erpnext.controllers.accounts_controller.validate_regional":
"my_customizations.regional.my_country.utils.validate_regional"
}
}
Testing the Regional Override
Verify your implementation using ERPNext's testing utilities:
# my_customizations/tests/test_my_country.py
import frappe
import erpnext
from erpnext.tests.utils import ERPNextTestSuite
def dummy_core_function():
return "core"
class TestMyCountryOverrides(ERPNextTestSuite):
def test_override_execution(self):
frappe.flags.country = "My Country"
# If "My Country" has a registered override for dummy_core_function,
# this will return the custom value instead of "core"
result = dummy_core_function()
self.assertEqual(result, "custom")
Summary
- The regional override system enables country-specific customizations without core code modification.
- Three core components power the system:
get_region()inerpnext/__init__.py, the@erpnext.allow_regionaldecorator, and theregional_overrideshook dictionary. - The system resolves the active country from flags, company settings, or global settings, then routes calls to the appropriate regional implementation.
- Later installed apps take precedence, allowing layered customization stacks.
- Place regional code in
regional/<country>/directories and register overrides in your app'shooks.py. - Test regional logic using patterns from
erpnext/tests/test_regional.pyby settingfrappe.flags.country.
Frequently Asked Questions
How does ERPNext determine which country's override to use?
ERPNext calls get_region() from erpnext/__init__.py (lines 20-32), which checks three sources in sequence: first frappe.flags.country (often set during document processing or tests), then the country associated with the current user's company via frappe.local.flags.company, and finally the global system setting. This determined country is used as the lookup key in the regional_overrides hook.
Can multiple apps override the same function for the same country?
Yes. When multiple apps register overrides for identical function paths within the same country, the system prioritizes the last installed app. This behavior is defined in erpnext/__init__.py at lines 45-52, where the code iterates through installed apps and the final match wins. Install your customization app after ERPNext and any other dependencies to ensure your override takes effect.
Do I need to modify ERPNext core files to implement regional customizations?
No. The system is designed specifically to avoid core modifications. You create a separate Frappe app containing your regional code in a regional/<country>/ directory, register the mappings in your app's hooks.py using the regional_overrides dictionary, and install the app alongside ERPNext. The @erpnext.allow_regional decorator on core functions (already present in ERPNext's codebase) handles the routing automatically.
What happens if a country has no override registered for a decorated function?
If get_region() returns a country that has no entry in frappe.get_hooks("regional_overrides"), or if the entry exists but does not contain a key matching the decorated function's fully-qualified path, the allow_regional decorator simply executes the original function. This fallback behavior ensures that core functionality remains intact for countries without specific customizations.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →