Skip to main content
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

1
Step 1: Generate the App
2
Use Bench to scaffold a new app:
3
bench new-app custom_app
4
You’ll be prompted for:
5
  • App Title (e.g., “Custom App”)
  • App Description
  • App Publisher
  • App Email
  • App License
  • 6
    Step 2: Review the App Structure
    7
    The generated app contains:
    8
    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
    
    9
    Step 3: Install the App
    10
    Install the app on your site:
    11
    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

    1
    Install from GitHub
    2
    bench get-app https://github.com/username/custom_app.git
    
    3
    Install from a specific branch
    4
    bench get-app https://github.com/username/custom_app.git --branch develop
    
    5
    Install on a site
    6
    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()
    

    Build docs developers (and LLMs) love