Custom apps allow you to extend ERPNext without modifying the core codebase. This approach ensures your customizations remain maintainable across ERPNext upgrades and can be version-controlled independently.
Understanding Custom Apps
A custom app in the Frappe ecosystem is a standalone Python package that can:
- Add new doctypes and modules
- Override or extend existing functionality
- Implement custom business logic
- Provide additional API endpoints
- Add custom reports and dashboards
Creating a New App
Use Bench to scaffold a new app:
App Title (e.g., “Custom App”)
App Description
App Publisher
App Email
App License
Step 2: Review the App Structure
The generated app contains:
custom_app/
├── custom_app/
│ ├── __init__.py
│ ├── hooks.py # App configuration and hooks
│ ├── modules.txt # List of modules
│ └── config/ # Desk configuration
├── setup.py # Python package setup
├── requirements.txt # Python dependencies
└── README.md
Install the app on your site:
bench --site sitename install-app custom_app
Configuring hooks.py
The hooks.py file is the heart of your app’s integration with ERPNext. Here’s a practical example:
app_name = "custom_app"
app_title = "Custom App"
app_publisher = "Your Company"
app_description = "Custom business logic for ERPNext"
app_icon = "fa fa-star"
app_color = "blue"
app_email = "[email protected]"
app_license = "MIT"
# Document Events
doc_events = {
"Sales Invoice": {
"before_save": "custom_app.utils.sales_invoice.validate_custom_fields",
"on_submit": "custom_app.utils.sales_invoice.update_external_system",
},
"Customer": {
"after_insert": "custom_app.utils.customer.sync_to_crm",
}
}
# Scheduled Events
scheduler_events = {
"daily": [
"custom_app.tasks.daily_sync"
],
"hourly": [
"custom_app.tasks.update_inventory"
]
}
# Override Whitelisted Methods
override_whitelisted_methods = {
"erpnext.selling.page.point_of_sale.point_of_sale.get_items": "custom_app.overrides.pos.get_items"
}
# Custom Jinja Filters
jinja = {
"methods": [
"custom_app.utils.format_custom_field",
]
}
Working with ERPNext
Extending ERPNext Controllers
Create custom logic that runs alongside ERPNext documents:
# custom_app/utils/sales_invoice.py
import frappe
from frappe import _
def validate_custom_fields(doc, method):
"""Custom validation for Sales Invoice"""
if doc.custom_field_1 and not doc.custom_field_2:
frappe.throw(_("Custom Field 2 is required when Custom Field 1 is set"))
def on_submit(doc, method):
"""Post-submission actions"""
# Send data to external API
sync_to_external_system(doc)
def sync_to_external_system(doc):
"""Send invoice data to external system"""
import requests
api_url = frappe.db.get_single_value("Custom App Settings", "api_endpoint")
response = requests.post(api_url, json=doc.as_dict())
if response.status_code != 200:
frappe.log_error(f"Sync failed: {response.text}", "External Sync Error")
Adding Custom API Endpoints
# custom_app/api.py
import frappe
@frappe.whitelist()
def get_customer_analytics(customer):
"""Custom API endpoint for customer analytics"""
return frappe.db.sql("""
SELECT
MONTHNAME(posting_date) as month,
SUM(grand_total) as total_sales,
COUNT(name) as invoice_count
FROM `tabSales Invoice`
WHERE customer = %s
AND docstatus = 1
GROUP BY MONTH(posting_date)
ORDER BY posting_date DESC
""", customer, as_dict=True)
@frappe.whitelist(allow_guest=True)
def public_product_catalog():
"""Public API for product catalog"""
return frappe.get_all(
"Item",
filters={"show_in_website": 1},
fields=["name", "item_name", "description", "standard_rate"]
)
Installing Apps from Git
bench get-app https://github.com/username/custom_app.git
Install from a specific branch
bench get-app https://github.com/username/custom_app.git --branch develop
bench --site sitename install-app custom_app
The bench get-app command clones the repository into the apps directory and installs Python dependencies. Use bench install-app to enable it on specific sites.
Best Practices
1. Use Proper Naming Conventions
- App names should be lowercase with underscores
- Module names should follow ERPNext conventions
- Prefix custom fields with your app name:
custom_app_field_name
2. Version Control Custom Fields
Export custom fields as fixtures (see Fixtures documentation) to version control them:
# hooks.py
fixtures = [
{
"dt": "Custom Field",
"filters": [["name", "in", [
"Sales Invoice-custom_app_reference",
"Customer-custom_app_external_id"
]]]
}
]
3. Handle Dependencies Properly
List ERPNext as a dependency in hooks.py:
required_apps = ["erpnext"]
4. Write Tests
Create unit tests for custom functionality:
# custom_app/tests/test_sales_invoice.py
import frappe
from frappe.tests import IntegrationTestCase
class TestSalesInvoiceCustom(IntegrationTestCase):
def test_custom_validation(self):
invoice = frappe.get_doc({
"doctype": "Sales Invoice",
"customer": "_Test Customer",
"custom_app_field": "test_value"
})
invoice.insert()
self.assertEqual(invoice.custom_app_field, "test_value")
5. Document Your App
Maintain clear documentation:
- README.md with installation instructions
- API documentation for custom endpoints
- Configuration guide for settings
Common Use Cases
Custom Industry Logic
# Manufacturing industry customization
doc_events = {
"Work Order": {
"before_submit": "custom_manufacturing.quality_check.validate_qc",
"on_cancel": "custom_manufacturing.inventory.reverse_allocations"
}
}
Third-Party Integrations
# E-commerce integration
doc_events = {
"Item": {
"on_update": "custom_ecommerce.sync.update_shopify_product"
},
"Sales Order": {
"on_submit": "custom_ecommerce.fulfillment.create_shipment"
}
}
Custom Reports and Dashboards
Add custom reports in hooks.py:
app_include_js = "custom_app.bundle.js"
# Custom reports will be auto-discovered if placed in:
# custom_app/custom_app/report/
Deployment Considerations
Always test custom apps in a staging environment before deploying to production. Use Bench commands to manage app updates:# Update app from git
bench update --apps custom_app
# Pull latest changes without updating dependencies
bench update --pull --apps custom_app
Migration and Patches
Create database migrations for schema changes:
# custom_app/patches.txt
custom_app.patches.v1_0.add_custom_fields
custom_app.patches.v1_1.migrate_old_data
# custom_app/patches/v1_0/add_custom_fields.py
import frappe
def execute():
"""Add custom fields to existing documents"""
if not frappe.db.exists("Custom Field", "Customer-custom_app_tier"):
frappe.get_doc({
"doctype": "Custom Field",
"dt": "Customer",
"fieldname": "custom_app_tier",
"label": "Customer Tier",
"fieldtype": "Select",
"options": "Bronze\nSilver\nGold\nPlatinum"
}).insert()