Programmatic Reporting in OpenMRS

Programmatic Reporting in OpenMRS

The OpenMRS Reporting Module provides a powerful framework for creating custom reports programmatically. This guide introduces the core concepts and architecture for building reports in Java code.

What is Programmatic Reporting?

Programmatic reporting means creating report definitions in Java code rather than through the OpenMRS user interface. Reports are defined as Java classes that specify:

  • What data to extract or calculate

  • What parameters the user can provide

  • How to format the output

When to Use Programmatic Reports

Use Programmatic Reports When:

Complex Logic Required: Calculations, aggregations, or data transformations too complex for UI configuration

Version Control Needed: Reports must be tracked in source control alongside code

Reproducible Across Installations: Same report definition needed on multiple servers

Integration with Module Logic: Report uses custom services or data from your module

Reusable Components: Building libraries of data definitions for multiple reports

Automated Testing: Need to write unit tests for report logic

Use UI-Defined Reports When:

❌ Simple data queries that change frequently

❌ One-off reports for specific analysis

❌ Non-technical users need to create reports

❌ Rapid prototyping and iteration

Core Concepts

1. ReportDefinition

The ReportDefinition is the top-level object that defines a complete report. It contains:

  • Metadata: Name, description, UUID

  • Parameters: User inputs (dates, locations, etc.)

  • Datasets: One or more datasets that make up the report

ReportDefinition reportDefinition = new ReportDefinition(); reportDefinition.setUuid("abc-123"); reportDefinition.setName("Patient Registration Report"); reportDefinition.setDescription("Lists all patients registered in a date range");

2. DataSetDefinition

A DataSetDefinition defines what data the report will contain. There are several types:

SqlDataSetDefinition

Executes SQL queries against the OpenMRS database.

Use for: Direct data extraction, simple queries

SqlDataSetDefinition sqlDsd = new SqlDataSetDefinition(); sqlDsd.setSqlQuery("SELECT patient_id, identifier FROM patient_identifier");

CohortCrossTabDataSetDefinition

Creates cross-tabulated data with cohorts in rows and disaggregations in columns.

Use for: Aggregation reports, indicator calculations, age/gender disaggregation

CohortCrossTabDataSetDefinition dsd = new CohortCrossTabDataSetDefinition(); // Define rows (cohorts) and columns (disaggregations)

PatientDataSetDefinition

Creates patient-level data with one row per patient.

Use for: Patient lists, data exports, line-level reports

PatientDataSetDefinition dsd = new PatientDataSetDefinition(); // Define columns using PatientDataDefinitions

3. Parameters

Parameters allow users to provide inputs when running the report:

Parameter startDate = new Parameter("startDate", "Start Date", Date.class); Parameter endDate = new Parameter("endDate", "End Date", Date.class); Parameter location = new Parameter("location", "Location", Location.class); reportDefinition.addParameter(startDate); reportDefinition.addParameter(endDate); reportDefinition.addParameter(location);

Common parameter types:

  • Date.class - Date selection

  • Location.class - Location picker

  • String.class - Text input

  • Integer.class - Number input

  • Cohort.class - Patient cohort

4. ReportDesign

A ReportDesign defines how the report output is formatted:

// CSV format ReportDesign csvDesign = ReportManagerUtil.createCsvReportDesign( "design-uuid", reportDefinition ); // Excel format ReportDesign excelDesign = ReportManagerUtil.createExcelReportDesign( "design-uuid", reportDefinition );

5. Data Converters

Data Converters transform raw database values into formatted output:

// Convert M/F to Male/Female new GenderConverter() // Convert 1/0 to Yes/No new ObsBooleanConverter() // Format dates new DateConverter("yyyy-MM-dd")

Architecture Overview

Report Creation Flow

1. Developer creates ReportManager class 2. ReportManager.constructReportDefinition() builds the report 3. ReportManager.constructReportDesigns() defines output formats 4. Module Activator registers the report with OpenMRS 5. Report appears in UI for users to run

Report Execution Flow

1. User selects report from UI 2. User provides parameter values (dates, locations, etc.) 3. OpenMRS evaluates the ReportDefinition 4. DataSetDefinitions execute (SQL queries, cohort calculations, etc.) 5. Data is formatted according to ReportDesign 6. User downloads CSV/Excel/PDF file

Report Manager Pattern

The BaseReportManager pattern is the recommended approach for creating programmatic reports:

@Component public class PatientListReportManager extends BaseReportManager { @Override public String getUuid() { return "unique-uuid-here"; } @Override public String getName() { return "Patient List Report"; } @Override public String getDescription() { return "Lists all active patients"; } @Override public List<Parameter> getParameters() { // Define parameters } @Override public ReportDefinition constructReportDefinition() { // Build the report } @Override public List<ReportDesign> constructReportDesigns(ReportDefinition rd) { // Define output formats } }

Why Use Report Managers?

  • Automatic Registration: Spring automatically finds and registers report managers

  • Lifecycle Management: OpenMRS handles setup and teardown

  • Standardized Pattern: Consistent structure across all reports

  • Easy Testing: Clear methods to test independently

OpenMRS Data Model Basics

Understanding the OpenMRS data model is essential for creating reports:

Core Tables

  • patient: Patient records

  • person: Demographic data (shared by patients, users, etc.)

  • person_name: Patient names

  • patient_identifier: Patient IDs/MRNs

  • encounter: Clinical encounters/visits

  • obs: Observations (vitals, lab results, diagnoses, etc.)

  • visit: Visit records

  • location: Health facilities

  • concept: Medical concepts (questions, answers, tests, etc.)

Key Relationships

patient → person (1:1) patient → patient_identifier (1:many) person → person_name (1:many) patient → encounter (1:many) encounter → obs (1:many) obs → concept (many:1)

Module Setup

Maven Dependency

Add the Reporting Module dependency to your module's pom.xml:

<dependency> <groupId>org.openmrs.module</groupId> <artifactId>reporting-api</artifactId> <version>1.21.0</version> <scope>provided</scope> </dependency>

Module Activation

The Reporting Module must be started before your module. Add to config.xml:

<require_modules> <require_module version="1.21.0"> org.openmrs.module.reporting </require_module> </require_modules>

Spring Component Scanning

Enable component scanning to auto-discover report managers:

<!-- In moduleApplicationContext.xml --> <context:component-scan base-package="org.openmrs.module.yourmodule.reports" />

Simple Example

Here's a complete minimal example:

package org.openmrs.module.yourmodule.reports; import org.openmrs.module.reporting.dataset.definition.SqlDataSetDefinition; import org.openmrs.module.reporting.evaluation.parameter.Parameter; import org.openmrs.module.reporting.report.ReportDesign; import org.openmrs.module.reporting.report.definition.ReportDefinition; import org.openmrs.module.reporting.report.manager.BaseReportManager; import org.openmrs.module.reporting.report.manager.ReportManagerUtil; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Component public class SimplePatientListReportManager extends BaseReportManager { @Override public String getUuid() { return "12345678-1234-1234-1234-123456789abc"; } @Override public String getName() { return "Simple Patient List"; } @Override public String getDescription() { return "Lists patients with basic demographics"; } @Override public List<Parameter> getParameters() { List<Parameter> params = new ArrayList<>(); params.add(new Parameter("startDate", "Start Date", Date.class)); params.add(new Parameter("endDate", "End Date", Date.class)); return params; } @Override public ReportDefinition constructReportDefinition() { ReportDefinition rd = new ReportDefinition(); rd.setUuid(getUuid()); rd.setName(getName()); rd.setDescription(getDescription()); rd.setParameters(getParameters()); // Create SQL dataset SqlDataSetDefinition sqlDsd = new SqlDataSetDefinition(); sqlDsd.setName("Patients"); sqlDsd.addParameters(getParameters()); String sql = "SELECT " + " p.patient_id, " + " pi.identifier, " + " pn.given_name, " + " pn.family_name, " + " per.gender, " + " per.birthdate " + "FROM patient p " + "INNER JOIN person per ON p.patient_id = per.person_id " + "INNER JOIN patient_identifier pi ON p.patient_id = pi.patient_id AND pi.preferred = 1 " + "INNER JOIN person_name pn ON p.patient_id = pn.person_id AND pn.preferred = 1 " + "WHERE DATE(p.date_created) BETWEEN :startDate AND :endDate"; sqlDsd.setSqlQuery(sql); // Map parameters Map<String, Object> parameterMappings = new HashMap<>(); parameterMappings.put("startDate", "${startDate}"); parameterMappings.put("endDate", "${endDate}"); // Add dataset to report rd.addDataSetDefinition("patients", sqlDsd, parameterMappings); return rd; } @Override public List<ReportDesign> constructReportDesigns(ReportDefinition reportDefinition) { List<ReportDesign> designs = new ArrayList<>(); designs.add(ReportManagerUtil.createCsvReportDesign( "abcdef01-2345-6789-abcd-ef0123456789", reportDefinition )); return designs; } }

Next Steps

Now that you understand the core concepts, proceed to:

  1. Report Manager Pattern - Deep dive into BaseReportManager

  2. SQL-Based Reports - Tutorial for SQL reports

  3. Cohort-Based Reports - Tutorial for aggregation reports

Additional Resources