When a CRM starts filling up with leads, contacts, accounts, and opportunities, finding the right record quickly becomes important. Users don’t want to scroll through hundreds of records just to find opportunities in a particular stage or leads created within a certain date range. This is where filters become essential.
In Horilla CRM, you don’t have to build filtering logic separately for every model. The framework provides a reusable HorillaFilterSet that handles the common filtering requirements and connects directly with the generic list-view UI.
So, if you’re adding a new model to Horilla CRM and want it to have the same filtering experience as the existing modules, you mainly need to define a filter class and tell Horilla which fields should be searchable and filterable.
Let’s look at how it works.
Why use HorillaFilterSet instead of django-filter directly?
Horilla CRM is built on top of django-filter, but it adds its own filtering layer through HorillaFilterSet, located at:
horilla/contrib/generics/filters.py
The reason for this extra layer is simple: Horilla’s filter UI needs more than the standard django-filter behavior.
Type-aware operators
Different types of fields need different filtering options.
For example, a text field might support:
- Contains
- Starts with
- Equals
- Is empty
While a number or date field can have options such as:
- Equals
- Greater than
- Less than
- Between
- Is empty
Date fields can also use shortcuts such as Today, Yesterday, This Week, and This Month.
These operators are handled centrally through the shared OPERATOR_CHOICES configuration, which means different modules can provide a consistent filtering experience without duplicating the same logic.
Multiple filter conditions
Horilla’s filter panel isn’t limited to one condition at a time.
A user can build something like:
Stage = Prospecting AND Amount > 10,000
or:
Stage = Prospecting OR Stage = Negotiation
The generic filter UI sends these conditions to HorillaFilterSet, which processes them inside filter_queryset() and applies the requested AND/OR logic.
Built-in search
HorillaFilterSet also provides a general search field.
You simply specify which model fields should be searched using search_fields in the Meta class.
This keeps the search configuration close to the filter definition instead of requiring a separate search implementation for every model.
A real example: Filtering Leads
Horilla CRM already uses this approach for its Lead model. The filter is defined in:
horilla_crm/leads/filters.py
from horilla.contrib.core.mixins import OwnerFiltersetMixin
from horilla.contrib.generics.filters import HorillaFilterSet
from .models import Lead
class LeadFilter(OwnerFiltersetMixin, HorillaFilterSet):
"""Lead Filter"""
class Meta:
model = Lead
fields = "__all__"
exclude = ["additional_info", "is_convert", "message_id"]
search_fields = ["first_name", "email", "title"]
Although the class is small, quite a lot happens behind it.
Using fields = “__all__”
Instead of manually listing every field that should be available for filtering, Horilla allows you to use:
fields = “__all__”
This makes all model fields available by default.
You can then remove fields that don’t make sense to expose using exclude.
For example:
exclude = [“additional_info”, “is_convert”, “message_id”]
This is useful for internal fields, JSON data, technical identifiers, or other fields that users shouldn’t need in the filter panel.
Defining search fields
The search_fields option controls the fields used by the general search box:
search_fields = [“first_name”, “email”, “title”]
So a search for a person’s name, email address, or lead title can be handled by the same filterset.
Horilla also has support for name-based searching. If both first_name and last_name are included in the search fields, a query such as:
Jane Doe
can be handled as a multi-part name search rather than being treated only as one exact string.
Adding a custom filter
Now let’s say you’ve created a new model called Opportunity and want to give it the same filtering functionality.
Start by creating a filters.py file in your app.
from horilla.contrib.generics.filters import HorillaFilterSet
from .models import Opportunity
class OpportunityFilter(HorillaFilterSet):
class Meta:
model = Opportunity
fields = "__all__"
exclude = ["additional_info"]
search_fields = ["name", "description"]
That’s the basic filterset.
You don’t need to manually create filters for every field. HorillaFilterSet takes care of generating and processing them according to their field types.
Adding ownership-based filtering
Sometimes filtering isn’t just about what the user searches for. You may also want users to see only the records they are allowed to access.
That’s where mixins become useful.
For example, the Lead filter uses:
class LeadFilter(OwnerFiltersetMixin, HorillaFilterSet):
The important part is that OwnerFiltersetMixin comes before HorillaFilterSet.
This allows the ownership-related behavior to be combined with the standard filtering functionality.
You can use the same approach with other Horilla filter mixins when your model needs additional filtering behavior.
Connecting the filter to your view
Creating the filter class isn’t enough. Your list view needs to know which filterset it should use.
For example:
class OpportunityListView(…):
filterset_class = OpportunityFilter
Once the filterset is connected to the view, the generic Horilla list-view system can use it to build the filtering experience.
You don’t need to create separate logic for every operator or search condition. The shared filter implementation handles that work.
Don’t forget to exclude internal fields
One important detail is the exclude option.
Suppose you have an internal field such as:
message_id
and you don’t want users to filter against it.
Add it to:
exclude = [“message_id”]
Horilla’s filtering logic also checks the top-level field name when handling ORM lookups. This prevents an excluded field from being exposed indirectly through a lookup such as:
related_model__message_id
This makes exclude more than just a UI convenience — it also acts as an important guardrail for fields that shouldn’t be exposed through the filtering system.
What you get automatically
Once your filterset is registered, you get much more than a simple database filter.
The shared HorillaFilterSet provides:
- Field-aware operators such as contains, equals, between, and is empty
- Multiple filter rows that can be combined using AND or OR
- Built-in search using the fields defined in search_fields
- Boolean value handling, converting values such as “true” and “false” into proper boolean values
- Relative date filters such as Today, Yesterday, This Week, and This Month
- Consistent filtering behavior across different Horilla modules
The important part is that you don’t have to reimplement these features for every model.
A simple checklist
When adding filtering to a new Horilla CRM model, the process is usually:
- Create or update the app’s filters.py.
- Import HorillaFilterSet.
- Create a filter class for your model.
- Set model in the Meta class.
- Use fields = “__all__” when most fields should be filterable.
- Add internal or unsuitable fields to exclude.
- Define the fields used by the general search using search_fields.
- Add mixins such as OwnerFiltersetMixin when required.
- Set filterset_class on the corresponding list view.
Filtering is one of those CRM features that looks simple from the user’s perspective but can become repetitive and difficult to maintain when every model implements it differently.
Horilla CRM solves this by putting the common filtering behavior into HorillaFilterSet. Instead of writing separate filtering logic for Leads, Accounts, Contacts, Opportunities, and every future model, you define the model, choose the fields to expose, specify the search fields, and let the shared filter system handle the rest.
This approach keeps the code smaller, keeps the UI consistent, and makes it much easier to add filtering to new modules. In most cases, registering a filter for a new model really can be a matter of a few lines of configuration rather than another custom filtering implementation.