Reporting Engine Architecture

Developer guide for the Enezaframework reporting engine. Part of the Add Report series.

Table of Contents

  1. Overview
  2. The Four Layers
  3. Layer Responsibilities
  4. End-to-End Request Flow
  5. Where Each Piece Lives
  6. 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:

python
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 page

The steps the view performs for every report:

  1. Resolve the report type from the class MRO (or from report_type_class).
  2. Build the parameter form from the type's parameter_spec.
  3. Validate the submitted parameters and extract Q filters.
  4. Instantiate the matching generator with the report configuration and filters.
  5. Call the generator to produce {data, columns, metadata, chart_settings}.
  6. Render through the unified eneza_reporting/report.html template.

ReportEngineView remains available for advanced and legacy use. It resolves an explicit report_type_class first; otherwise the first class in the MRO that defines a parameter_spec is used.


Where Each Piece Lives

text
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