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
| Approach | Downside |
|---|---|
| horilla_crm/leads/models.py (edited directly) | Lost on upstream merge |
| JSON only in additional_info | No DB constraints, poor filtering |
| Parallel “extension” table + GFK | Join 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:
- Models: _inherit_model + InjectField migrations
- Forms: _inherit_form + FormExtension
- Lists: _inherit_list + ListExtension
- Cards / filters / nav / kanban / detail: matching *Extension classes
- Call bootstrap_extensions() from horilla/urls/project.py (already wired in stock CRM) so startup composition runs after target apps load
- 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_model | Use additional_info JSON | Use separate model + FK |
|---|---|---|
| Filter/sort in list views | Scratch flags, API cache | Complex sub-objects with own lifecycle |
| Reporting / exports | Non-critical metadata | Many-to-many extension tables |
| Validation / DB constraints | Prototyping |
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.