Horilla CRM’s extension framework allows you to add fields and UI to existing models without forking core. Extension apps use _inherit_model on models and matching _inherit_form, _inherit_list, and related hooks for forms, lists, and views.

This post is Part 6 of 28 in the Horilla CRM Technical Blog series.

What is the _inherit_model extension framework?

The _inherit_model extension framework enables you to:

  • Model _inherit_model — add fields to Lead, Opportunity, etc.
  • Form, list, card, filter, nav, kanban, detail extensions
  • bootstrap_extensions() loads extension apps at startup
  • Separate migrations in your extension app

The problem with naive approaches

ApproachDownside
horilla_crm/leads/models.py (edited directly)Lost on upstream merge
JSON only in additional_infoNo DB constraints, poor filtering
Parallel “extension” table + GFKJoin pain, no generic view support

Horilla injects fields onto the existing model class and table via a custom metaclass and migration autodetector.

Quick start

1. Create an extension app

python manage.py start_horilla_app my_lead_extensions

2. Define the extension model

# my_lead_extensions/models.py
from horilla.db import models
from horilla.contrib.core.models import HorillaCoreModel
 
 
class LeadExtension(HorillaCoreModel):
    _inherit_model = "leads.Lead"
 
    industry_code = models.CharField(max_length=20, null=True, blank=True)
    compliance_tier = models.CharField(max_length=10, null=True, blank=True)

Rules:

  • Base class must be HorillaCoreModel
  • _inherit_model format: “app_label.ModelName” (e.g. “leads.Lead”)
  • Do not create a separate table for business fields — they merge onto leads_lead

3. Configure AppLauncher

# my_lead_extensions/apps.py
from horilla.apps import AppLauncher
from horilla.utils.translation import gettext_lazy as _
 
 
class MyLeadExtensionsConfig(AppLauncher):
    name = "my_lead_extensions"
    verbose_name = _("Lead Extensions")
    auto_import_modules = ["models", "forms", "lists", "filters", "navbars"]

4. Install in client settings

# local_settings.py — do not edit horilla/settings/base.py or horilla_apps.py
INSTALLED_APPS += ["my_lead_extensions"]

List the extension after horilla_crm.* when possible. Horilla’s bootstrap_extensions() and per-request list resolution still compose UI extensions even if CRM apps register URLs first.

5. Migrate from your app only

python manage.py makemigrations my_lead_extensions
python manage.py migrate

Migrations should contain InjectField operations under my_lead_extensions/migrations/ — not new migrations inside horilla_crm/leads/.

How it works (conceptual)

flowchart LR
    A[Lead model in leads app] --> B[ExtensionModelBase metaclass]
    C[LeadExtension with _inherit_model] --> B
    B --> D[Merged Lead Python class]
    D --> E[Single DB table leads_lead]
    C --> F[InjectField migrations in extension app]

At runtime, Lead appears to have industry_code. Generic views, filters, and forms that can use the field if registered and permitted.

Migrations: pitfalls and fixes

NOT NULL on existing rows

Adding a non-nullable column without a default or null=True fails on populated tables. Prefer:

industry_code = models.CharField(max_length=20, null=True, blank=True)
# backfill in data migration, then ALTER to NOT NULL if needed

Never delete via core app migrations

If Django generates horilla_crm/leads/migrations/000X_remove_lead_industry_code.py, delete it and regenerate under your extension app. Core RemoveField breaks the injection state.

AlterField ownership

Field renames and verbose_name changes on injected fields should produce AlterInjectedField in the extension app — Horilla’s autodetector rewrites operations when the field is in INJECTION_MAP.

Uninstall

python manage.py migrate my_lead_extensions zero

Then remove the app from INSTALLED_APPS.

Using extended fields in UI

After migration, wire forms and lists in the same extension app (see my_lead_extensions/):

# forms.py
from horilla.extension.forms import FormExtension
 
class LeadSingleFormExtension(FormExtension):
    _inherit_form = "horilla_crm.leads.forms.LeadSingleForm"
    field_order_insert = [("industry", "industry_code")]
# lists.py
from horilla.extension.list import ListExtension
 
class LeadListExtension(ListExtension):
    _inherit_list = "horilla_crm.leads.views.core.LeadListView"
    columns_insert = [("industry", "industry_code")]
    bulk_update_fields_append = ["industry_code"]
# filters.py
from horilla.extension.filter import FilterExtension
 
class LeadFilterExtension(FilterExtension):
    _inherit_filter = "horilla_crm.leads.filters.LeadFilter"
    exclude_append = ["industry_code"]  # filter panel field dropdown
# navbars.py
from horilla.extension.nav import NavExtension
 
class LeadNavbarExtension(NavExtension):
    _inherit_nav = "horilla_crm.leads.views.core.LeadNavbar"
    column_selector_exclude_fields_append = ["industry_code"]
# cards.py
from horilla.extension.card import CardExtension
 
class LeadCardExtension(CardExtension):
    _inherit_card = "horilla_crm.leads.views.core.LeadCardView"
    columns_insert = [("industry", "industry_code")]
# kanbans.py / details.py — same pattern
from horilla.extension.kanban import KanbanExtension
from horilla.extension.detail import DetailExtension

Checklist:

  1. Models: _inherit_model + InjectField migrations
  2. Forms: _inherit_form + FormExtension
  3. Lists: _inherit_list + ListExtension
  4. Cards / filters / nav / kanban / detail: matching *Extension classes
  5. Call bootstrap_extensions() from horilla/urls/project.py (already wired in stock CRM) so startup composition runs after target apps load
  6. FieldPermission if sensitive (Part 4)

Do not edit horilla_crm form/list/view classes in core. Per-request resolution and extension caches avoid strict INSTALLED_APPS ordering for most view layers.

clean() behavior

Do not call super().clean() in extension classes; Horilla runs the target model’s clean() first, then extension validators.

When to use _inherit_model vs alternatives

Use _inherit_modelUse additional_info JSONUse separate model + FK
Filter/sort in list viewsScratch flags, API cacheComplex sub-objects with own lifecycle
Reporting / exportsNon-critical metadataMany-to-many extension tables
Validation / DB constraintsPrototyping

Benefits of Model Extensions in Horilla CRM

  • Customize CRM without losing upstream updates
  • Clean separation between core and customer-specific code
  • Extension apps follow the same AppLauncher conventions
  • UI extensions appear in the same list/detail screens

Use extension apps and _inherit_model (plus _inherit_form, _inherit_list, and related hooks) when you need extra fields or UI on core models. Keep migrations in your app and register through AppLauncher like any other module.

Continue the series

Previous: Part 5 — Why Horilla CRM Uses HTMX Instead of React for a Fast and Server-Rendered UI

Next: Part 7 — A Complete Guide to CRM Automations, Approvals, and Workflows in Horilla CRM

More posts are at Horilla Blogs; share feedback on GitHub.

Share this article