horilla.contrib.reports builds pivot reports with folders, sharing, charts, and export. Only models in report_models appear in the module picker.

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

What is the reports builder?

The reports builder enables you to:

  • Drag-and-drop pivot configuration
  • Report folders and permissions
  • AppLauncher report_files registration
  • Export to CSV/Excel

Feature registration

The reports feature is registered as report_models. This feature boundary determines which application models are eligible for reporting and connects them to the platform’s registration/permission machinery. A new CRM model does not become reportable merely because it has a database table. Register it through the approved feature surface, define permitted fields and relations, and verify that tenant filters remain applied to every generated queryset.

That restriction is a security requirement as well as a usability feature. Free-form model lookup would let an otherwise ordinary report builder discover internal models or traverse relationships across company boundaries. Prefer the existing report model registry to accepting arbitrary Python import paths or model labels from an HTTP request.

Tabular aggregation with pandas

The report implementation uses pandas pivot operations for grouped and cross-tabular output. Pivoting occurs after the application obtains permitted, filtered data; pandas is a shaping tool, not an authorization layer. Convert only the selected report rows and columns into a DataFrame, then pivot using the saved configuration.

import pandas as pd


frame = pd.DataFrame(rows)


pivot = pd.pivot_table(


    frame,


    index=["owner"],


    columns=["stage"],


    values="amount",


    aggfunc="sum",


    fill_value=0,


)

Production report code must validate that owner, stage, and amount are allowed selected fields rather than passing user-supplied strings directly to pandas. Normalize date, money, and null values before aggregation, and bound the size of source querysets. A large unrestricted DataFrame can consume far more memory than an equivalent database aggregate.

Detail rendering and chart configuration

ReportDetailView resolves a saved report and renders its output. It is the correct place to apply report-level permissions, parse its stored configuration, run the report service, and select the appropriate partial for table or chart output. Do not make templates construct ORM queries from report JSON; template logic is not a safe or testable query compiler.

Report.chart_type selects how a chartable result is presented. The chart type is presentation metadata, not a replacement for grouping and aggregation rules. For example, a bar chart still needs a labeled dimension and numeric measure from the validated result. If a report configuration cannot produce a meaningful series for its chart type, reject or gracefully fall back rather than emitting malformed client data.

class SalesReportDetail(ReportDetailView):


    # The base view resolves configuration and builds the report result.


    pass

Use the installed view’s documented hooks for custom columns or charts; the example intentionally does not replace its permission and query behavior.

Export contract

ReportExportView produces CSV and Excel exports. Exports must use the same resolved report service, filters, and field permissions as ReportDetailView. Creating a separate export queryset is a common data-leak bug: it may omit a UI-only filter or include a field hidden on the detail page.

response = ReportExportView.as_view()(request, pk=report.pk)

Generate CSV/Excel from the already authorized report result, set an appropriate attachment filename, and avoid interpreting cell values as formulas. Where spreadsheet formula injection is relevant, neutralize untrusted strings beginning with formula-significant characters before writing the export. For large reports, apply the application’s pagination, row-limit, or asynchronous export policy instead of loading an unbounded result into memory.

Saved filters and sharing

A report definition is only useful if another authorized user can open it and see the same logic. Prefer storing filter criteria, column order, and chart options on the Report record rather than in browser local storage. When sharing reports across roles, re-check field permissions for the viewer: a manager may see owner and amount columns that a restricted role must not export. Folder hierarchy helps organize definitions, but access control still belongs on the report and underlying model, not only on the folder name.

Document for administrators which models are registered under report_models and which fields are intentionally excluded (secrets, internal IDs, or cross-company relations). That documentation reduces support tickets when a user expects a custom field to appear in the builder, and it does not.

Benefits of Reports in Horilla CRM

  • Self-service analytics for business users
  • Module apps ship starter reports
  • Consistent with company filtering

Ship report_files from your AppLauncher app, and users can extend analytics without waiting on custom SQL.

Continue the series

Previous: Part 21 — Everything You Need to Know About Sales Forecasting in Horilla CRM

Next: Part 23 — How to Implement Dynamic Brand Colors in Horilla CRM with Tailwind CSS

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

Share this article