Skip to main content

Report Generation

Reports are generated on-demand via GET /api/user/report. All generation is entirely in-memory — no files are written to disk or stored in blob storage at any point. The response is a binary file stream (PDF, XLSX) or an HTML string returned directly with the appropriate Content-Type header.

The pipeline has three stages: request parsingdata retrieval (via ReportService) → formatting and rendering (via ReportFormatter). Every report type and format passes through this same pipeline; only the report class, the Jinja2 template, and the final converter differ.


GET /api/user/report

GET /api/user/report?reportType=daily&reportFormat=pdf&service=water&startDate=09/11/2024
Authorization: Bearer <access_token>
tip

Browser-direct download — JWT may be passed as a query parameter instead of a header for direct browser download links:

GET https://prod-aquagen.azurewebsites.net/api/user/report?jwt=<access_token>&reportType=granular&reportFormat=xlsx&service=water&startDate=09/11/2024

Request Parameters

ParameterRequiredDefaultDescription
startDateNocurrent_user['nowUTCWithShiftDateTime'] formatted as DD/MM/YYYYStart of the report window in DD/MM/YYYY format. Defaults to the industry's current date (shift-adjusted) if not provided.
endDateNo (required for some types)Auto-calculated per reportType — see table belowEnd of the report window in DD/MM/YYYY format. The exact behaviour depends on reportType.
unitIdNoFilter the report to a single unit. Typically used for hourly and granular reports where per-unit breakdown is the primary view.
unitIdsNoComma-separated list of unit IDs to include. Split into a list on the server. If the list resolves to ['null'], it is converted to None (all units).
useShiftNotrueWhen true, aligns the date range to the industry's configured shift boundaries rather than calendar midnight.
reportTypeNodailyThe period/aggregation type for the report. Accepts values from the ReportType enum.
reportFormatNohtmlThe output format. Accepts values from the ReportFormat enum.
serviceNowaterSelects which report class runs. Accepts values from the ServiceType enum.
subCategoriesNo'' (empty, split into list)Comma-separated subcategory IDs. Only used by smart_city — the server filters current_user['categories'][sourceCategory]['subCategories'] down to only those present in this list before any processing begins.
divisionFactorNo1000Integer divisor applied to raw device readings before display. Default of 1000 converts litre readings to kilolitres.
formatNov1Internal version flag for the report template. Passed through to ReportRequestData and used by some report classes to select between legacy and updated template layouts.

POST /api/user/report

Accepts the same parameters as GET but additionally takes a JSON request body. The body is passed as additional_data to the report class — used primarily for custom report configurations that cannot be expressed as simple query parameters.

POST https://prod-aquagen.azurewebsites.net/api/user/report
Authorization: Bearer <access_token>
Content-Type: application/json

{
"reportType": "custom",
"reportFormat": "xlsx",
"service": "custom_water",
"startDate": "01/11/2024",
"endDate": "30/11/2024"
}
warning

A schema validation failure on the request body returns error 1430.


startDate and endDate Behaviour

startDate defaults to the industry's current shift-adjusted datetime (current_user['nowUTCWithShiftDateTime'].strftime('%d/%m/%Y')) if not provided by the caller.

endDate is not simply passed through — the route recalculates it based on reportType before building ReportRequestData. Two internal date values are computed:

  • end_date — the exclusive upper bound used in database queries (e.g. the day after the last data day)
  • end_date_actual — the display end date shown in the report header (the last data day itself)
reportTypeendDate requiredend_date (query bound)end_date_actual (display)
dailyNostartDate + 1 daystartDate + 1 day
monthlyNo (auto-calculated); yes if service is smart_city or consolidated and caller provides itAuto: startDate + monthrange(year, month)[1] days (last day of month). Caller-provided: taken as-is.Auto: last day of the start month. Caller-provided: same as end_date.
hourlyYes — not auto-calculatedendDate paramendDate param
granularYesendDate + timedelta(days=1)endDate param
weeklyNostartDate + 1 daystartDate + 1 day
daily_rangeYesendDate paramendDate param
monthly_rangeYesendDate paramendDate param
customNo — optionalIf provided: endDate param. If not: startDate + 1 day.Same as end_date.
summaryNostartDate + 1 daystartDate + 1 day
aqua_gptNostartDate + 1 daystartDate + 1 day
note

Note on granular: The caller always passes the last data day as endDate. The server internally adds one day so that the database query captures the full final day of data. The displayed end date in the report remains the caller-provided value.

Note on monthly for smart_city/consolidated: When the caller explicitly provides endDate for these services, the server uses it verbatim. Otherwise, the server computes the number of days in the start month via calendar.monthrange(year, month)[1] and sets end_date to startDate + that many days.


unitIds Handling

unitIds is accepted as a comma-separated string and split into a list on the server. If the resolved list equals ['null'] (i.e. the caller passed unitIds=null), it is normalised to None, which causes all units to be included in the report. This is the standard way to pass "no filter" explicitly.

tip

Passing unitIds=null explicitly is the correct way to signal "include all units" when you need to override a previously scoped request.


Service Types

Each service value maps to a report class that extends GenericReport. The class is responsible for all data retrieval and business logic for that domain.

serviceReport classDescription
waterFlowReportWater flow and consumption reporting
energyEnergyReportEnergy consumption reporting
levelLevelReportWater level and tank fill readings
qualityQualityReportWater quality parameters (pH, TDS, turbidity, etc.)
borewellBorewellReportGroundwater borewell extraction data
rain_waterRainWaterReportRainfall collection and volumes
summarySummaryReportCross-category aggregated summary
smart_citySmartCityReportSmart city multi-subcategory monitoring. Overrides get_report() directly rather than dispatching through individual type methods. subCategories parameter filters active subcategories before any processing.
water_balanceWaterBalanceReportInflow vs outflow analysis with loss calculation
alertAlertReportAlert and breach event history
consolidatedConsolidatedReportMonthly consolidated report across multiple categories
custom_waterCustomFlowReportCustom water flow configuration. XLSX uses HTML-to-XLSX pipeline.
custom_energyCustomEnergyReportCustom energy configuration
custom_levelCustomLevelReportCustom level configuration. XLSX uses HTML-to-XLSX pipeline.
custom_qualityCustomQualityReportCustom quality configuration
custom_withdrawalCustomWithdrawalReportCustom withdrawal report. XLSX uses HTML-to-XLSX pipeline.
ground_water_withdrawalGroundWaterWithdrawalReportGroundwater extraction and withdrawal data
executiveExecutiveReportExecutive summary with complex chart layouts. PDF uses Playwright instead of xhtml2pdf.
aqua_gptAquaGPTReportAI-generated natural-language report summary

Report Types

reportTypeDescriptionSupported service values
dailySingle-day aggregated totals for all unitswater, energy, level, quality, borewell, rain_water, summary, custom_water, custom_energy, custom_level, custom_quality, custom_withdrawal, ground_water_withdrawal, aqua_gpt
monthlyMonth-level aggregation. Covers one calendar month from startDate.water, energy, level, quality, borewell, rain_water, water_balance, consolidated, custom_water, custom_withdrawal, ground_water_withdrawal, executive
hourlyBreakdown by hour within the day.water, energy, level, quality, borewell, custom_water, custom_energy, ground_water_withdrawal
granularRaw or fine-grained readings within the specified date rangewater, energy, level, quality, borewell, rain_water, custom_water, ground_water_withdrawal, alert
weeklyAggregated weekly viewrain_water
daily_rangeDaily totals across a multi-day range (startDate to endDate)water, energy, level, quality, borewell
monthly_rangeMonthly totals across a multi-month rangewater, energy
customFlexible range — respects caller-provided endDate or defaults to startDate + 1 daywater_balance, executive
summaryAggregated summary viewrain_water
aqua_gptProduces an AI-generated narrative summary of the data windowaqua_gpt

Report Formats

reportFormatBackendOutput Content-Type
pdfxhtml2pdf for all services except executive. Playwright headless Chromium for executive.application/pdf
xlsxxlsxwriter standard pipeline. Exception: custom_water, custom_level, custom_withdrawal use the HTML-to-XLSX pipeline (see below).application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
htmlJinja2 template rendered and returned as a string. No conversion step.text/html
note

All formats are generated entirely in memory (BytesIO objects) — nothing is written to disk at any point. The binary stream is returned directly as the HTTP response body.

XLSX Pipeline — Custom Services

When reportFormat=xlsx and service is one of custom_water, custom_level, or custom_withdrawal, the standard xlsxwriter path is bypassed. Instead:

  1. ReportFormatter(report_request, report_data).get_formatted_html() renders the Jinja2 template to an HTML string
  2. get_xlsx_file_for_custom_monthly(html_data, report_data['fileName']) converts that HTML to an XLSX workbook
note

This HTML-to-XLSX path exists because these custom report templates have complex cell-spanning layouts that are defined in HTML and cannot be reproduced easily with the xlsxwriter programmatic API.

PDF Backends

xhtml2pdf — used for all services except executive. Converts inline-styled HTML+CSS to PDF. External stylesheets and complex layout models (multi-column flexbox, CSS Grid) are not supported. All templates targeting xhtml2pdf must have fully inline styles.

Playwright — used exclusively for the executive service. Launches a headless Chromium browser instance, loads the rendered HTML, and exports to PDF. This path supports the full modern CSS and JavaScript feature set, which the executive report requires for its embedded chart components.


Execution Flow

info

1. Request parsing — ReportGetHandler.get()

The route handler uses requestParser to extract and type-coerce all parameters:

  • startDate, endDate — strings in DD/MM/YYYY
  • unitId — single string
  • unitIds — string, split on comma into list
  • useShift — boolean, default True
  • reportType — validated against ReportType enum, default daily
  • reportFormat — validated against ReportFormat enum, default html
  • service — validated against ServiceType enum, default water
  • subCategories — string, split into list, default ''
  • divisionFactor — integer, default 1000
  • format — string, default v1

2. smart_city subcategory pre-filter

If service == SMART_CITY, the handler immediately narrows the user context before any further processing:

current_user['categories'][sourceCategory]['subCategories'] = [
sc for sc in current_user['categories'][sourceCategory]['subCategories']
if sc in args['subCategories']
]

This filter happens at the route level, before ReportRequestData is constructed, so the report class and all downstream logic only ever see the filtered subcategory list.

3. startDate defaulting

If startDate was not provided by the caller:

start_date = current_user['nowUTCWithShiftDateTime'].strftime('%d/%m/%Y')

This uses the industry's shift-adjusted current datetime, not plain UTC, ensuring the default date matches the operator's logical "today."

4. endDate computation

end_date and end_date_actual are computed per the table in the startDate and endDate Behaviour section above.

5. unitIds normalisation

if unit_ids == ['null']:
unit_ids = None

6. ReportRequestData construction

All resolved parameters are assembled into a ReportRequestData dataclass:

ReportRequestData(
report_type=report_type,
report_format=report_format,
industry_id=industry_id,
use_shift=use_shift,
timezone=industry_timezone,
start_date=start_date,
unit_id=unit_id,
unit_ids=unit_ids,
end_date=end_date,
end_date_actual=end_date_actual,
service_type=service_type,
start_time=start_time,
end_time=end_time,
division_factor=division_factor,
format=format,
additional_data=additional_data, # POST body, if any
)

7. ReportService(request).get_report()

ReportService uses the service_type field of ReportRequestData to look up the correct report class in its dispatcher dict and instantiate it. It then calls get_report() on the instance, which dispatches to the method matching report_type:

report_typeMethod called
dailyget_daily_report()
monthlyget_monthly_report()
hourlyget_hourly_report()
granularget_granular_report()
weeklyget_weekly_report()
daily_rangeget_daily_range_report()
monthly_rangeget_monthly_range_report()
customget_custom_report()
summaryget_summary_report()
aqua_gptget_aqua_gpt_report()

Exception: SmartCityReport overrides get_report() directly and does not use the per-type method dispatch.

Inside each report class method:

  • Raw device data is fetched via DatabaseSupporter
  • For multi-unit or multi-date queries, fetches are parallelised with ThreadPoolExecutor
  • Business logic runs: aggregations, hourly breakdowns, threshold comparisons, timezone conversions
  • A structured data dict is returned with all keys the Jinja2 template expects

8. report_generation_time injection

After get_report() returns, the route adds the current local time to the data dict:

report_data['report_generation_time'] = DateTimeUtil().convert_date_from_utc_to_date(
utc_now, ..., t_zone=industry_timezone
).strftime("%I:%M %p")

This is the timestamp displayed in report footers as the generation time in the industry's local timezone.

9. Format routing

XLSX + custom service path:

if report_format == XLSX and service in (CUSTOM_WATER, CUSTOM_LEVEL, CUSTOM_WITHDRAWAL):
html_data = ReportFormatter(report_request, report_data).get_formatted_html()
return get_xlsx_file_for_custom_monthly(html_data, report_data['fileName'])

Standard path:

return ReportFormatter(report_request, report_data).get_file()

ReportFormatter.get_file():

  1. Selects the Jinja2 template path based on service_type and report_type
  2. Instantiates the formatter class for the service (e.g. FlowReportFormatter) to transform the raw data dict into a template-ready structure (number formatting, colour thresholds, date formatting, unit labels)
  3. Renders the Jinja2 template with the formatted context
  4. Passes the rendered HTML to the appropriate converter:
    • html: returns the string directly
    • pdf: passes to xhtml2pdf (or Playwright for executive)
    • xlsx: passes to xlsxwriter
  5. Returns the binary stream

10. HTTP response

The file stream is returned with the appropriate Content-Type header and Content-Disposition: attachment; filename="{report_data['fileName']}".


Example Requests

Daily water consumption report (PDF)

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=daily&reportFormat=pdf&service=water&startDate=09/11/2024
Authorization: Bearer <access_token>

Granular water report for a single unit (XLSX)

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=granular&reportFormat=xlsx&service=water&startDate=09/11/2024&endDate=09/11/2024&unitId=U001
Authorization: Bearer <access_token>

Monthly water report (auto end date)

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=monthly&reportFormat=pdf&service=water&startDate=01/11/2024
Authorization: Bearer <access_token>

endDate is not required — the server calculates it as the last day of November 2024.

Daily range report across multiple days

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=daily_range&reportFormat=xlsx&service=water&startDate=01/11/2024&endDate=30/11/2024
Authorization: Bearer <access_token>

Smart city report with subcategory filter

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=daily&reportFormat=pdf&service=smart_city&startDate=09/11/2024&subCategories=SC001,SC002
Authorization: Bearer <access_token>

Executive report (Playwright PDF)

GET https://prod-aquagen.azurewebsites.net/api/user/report?reportType=custom&reportFormat=pdf&service=executive&startDate=01/11/2024&endDate=30/11/2024
Authorization: Bearer <access_token>

Browser-direct download (JWT in query param)

GET https://prod-aquagen.azurewebsites.net/api/user/report?jwt=<access_token>&reportType=granular&reportFormat=xlsx&service=water&startDate=09/11/2024&endDate=09/11/2024

No Authorization header needed — used when constructing download URLs for the browser to navigate to directly.

Custom report via POST with additional config

POST https://prod-aquagen.azurewebsites.net/api/user/report
Authorization: Bearer <access_token>
Content-Type: application/json

{
"reportType": "custom",
"reportFormat": "xlsx",
"service": "custom_water",
"startDate": "01/11/2024",
"endDate": "30/11/2024"
}

The full JSON body is passed as additional_data to the report class.


Edge Cases

warning
ScenarioBehaviour
startDate not providedDefaults to current_user['nowUTCWithShiftDateTime'].strftime('%d/%m/%Y') — the industry's shift-adjusted current date
unitIds=null passedServer converts ['null'] list to None, treating it as "all units"
reportType=granular with endDateServer internally adds +1 day to endDate for the DB query; end_date_actual remains the original caller value
reportType=monthly, auto end datecalendar.monthrange(year, month)[1] determines the number of days; end_date = startDate + that count; end_date_actual = last day of month
reportType=monthly with service=smart_city or consolidated and endDate providedServer uses the caller's endDate verbatim instead of auto-calculating
reportFormat=xlsx with service=custom_water/custom_level/custom_withdrawalHTML-to-XLSX pipeline via get_formatted_html() + get_xlsx_file_for_custom_monthly(). Standard xlsxwriter is not used.
service=executive with reportFormat=pdfPlaywright headless Chromium renders the HTML; xhtml2pdf is not used
service=smart_cityget_report() is overridden directly; per-type method dispatch does not apply
No data found for the given parametersReturns error 1401
endDate required but not provided (e.g. reportType=hourly)The query runs with end_date=None. Callers must provide endDate for hourly, granular, daily_range, and monthly_range.
Schema validation failure on POST bodyReturns error 1430
JWT missing or expiredReturns error 401
subCategories filter provided with non-smart_city serviceParameter is parsed and stored in ReportRequestData but the pre-filter logic only runs for SMART_CITY — other services ignore it

Error Reference

CodeMeaning
401Missing or expired JWT
1401No data found for the given parameters
1403Missing required field (service, reportType, reportFormat, or startDate)
1430Schema validation failure on POST request body

For adding a new report service to the codebase, see Adding a Report Service.