Reporting Engine Architecture
Developer guide for the Enezaframework reporting engine. Part of the Add Report series.
Table of Contents
- Overview
- The Four Layers
- Layer Responsibilities
- End-to-End Request Flow
- Where Each Piece Lives
- Related Guides
Overview
The reporting engine separates what a report is from how its data is produced and
how it is rendered. You declare a report by subclassing one unified *ReportView base
(columns, grouping, filters, view, and rendering); the base resolves the report type,
builds the filter form from the type's parameter_spec, instantiates the matching
generator, and renders the unified template.
This separation is what lets one view class serve every report type and keeps each report definition small.
The Four Layers
flowchart TD
TYPES["Report Types<br/>report_types.py"]
PARAMS["Parameters<br/>parameters.py"]
GENS["Generators<br/>generators/*.py"]
VIEW["Unified View<br/>report_base.py · *ReportView"]
TYPES -->|"declares WHAT"| PARAMS
PARAMS -->|"declares filter inputs"| GENS
GENS -->|"produces data"| VIEW
VIEW -->|"HTTP request to template"| TYPES| Layer | File | Nature | Responsibility |
|---|---|---|---|
| Report Types | enezareporting/report_types.py |
Declarative | Define what a report is: model, columns, grouping, pivot field, time pattern, hierarchy. |
| Parameters | enezareporting/parameters.py |
Declarative | Define the filter inputs: date ranges, FK filters, document pickers, choices, saved queries. |
| Generators | enezareporting/generators/*.py |
Imperative | Fetch, aggregate, and format data into a {data, columns, metadata} response. |
| View | enezareporting/report_base.py (*ReportView) |
Imperative | Single-parent base that wires HTTP request → form → generator → template. |
Layer Responsibilities
Report types declare intent
A unified report base declares the data source and the shape of the report. You inherit exactly one class; the internal type metadata, view dispatch, designer rendering, and totals hooks are already included:
from enezareporting.report_base import TabularReportView
class SalesByItem(TabularReportView):
report_model = SalesLine
date_field = "date_placed"
group_by = "item_code"
columns = ["item_description", "__total__"]
The base does no querying itself. It carries attributes (report_model, columns,
group_by, parameter_spec, and so on) that the generator and view read.
Parameters declare the filter form
Each report type has a parameter_spec — a ParameterSet of parameter instances. The
view converts it into a Django form at request time. See
Report Parameters.
Generators produce data
Each report type maps to a generator class. The generator receives the report configuration plus cleaned filter values and returns a structured response. See Report Generators.
The unified base orchestrates
Each *ReportView base resolves the report type from the class itself, builds the form from
parameter_spec, chooses the generator, and injects type-specific context. See
Report Views.
End-to-End Request Flow
sequenceDiagram
actor U as User
participant V as Unified View
participant P as parameter_spec Form
participant G as Generator
participant T as report.html
U->>V: GET /sales-report/
V->>V: resolve report_type from MRO
V->>P: build form from parameter_spec
P-->>U: render filter form
U->>V: submit parameters (GET/POST or modal)
V->>P: validate cleaned_data
V->>G: instantiate with report config + filters
G->>G: fetch_data()
G->>G: transform()
G->>G: get_columns()
G-->>V: {data, columns, metadata, chart_settings}
V->>T: render with report data
T-->>U: report pageThe steps the view performs for every report:
- Resolve the report type from the class MRO (or from
report_type_class). - Build the parameter form from the type's
parameter_spec. - Validate the submitted parameters and extract
Qfilters. - Instantiate the matching generator with the report configuration and filters.
- Call the generator to produce
{data, columns, metadata, chart_settings}. - Render through the unified
eneza_reporting/report.htmltemplate.
ReportEngineViewremains available for advanced and legacy use. It resolves an explicitreport_type_classfirst; otherwise the first class in the MRO that defines aparameter_specis used.
Where Each Piece Lives
enezareporting/
├── report_base.py # Unified single-parent *ReportView bases
├── report_types.py # ReportType hierarchy + HierarchyLevel (metadata)
├── views.py # ReportEngineView, legacy views, BaseDocumentReport
├── generators/
│ ├── tabular.py # TabularReportGenerator, Chart, ListViewReportGenerator
│ ├── crosstab.py # CrosstabReportGenerator
│ ├── time_series.py # TimeSeriesReportGenerator
│ ├── document.py # DocumentReportGenerator
│ └── drilldown.py # DrillDownReportGenerator
├── fields.py # ComputationField + registered field names
├── registry.py # field_registry, report_type_registry
├── eneza_reporting_settings.py # ENEZA_REPORTING_SETTINGS merge
└── templates/eneza_reporting/ # base.html, report.html, partials