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 selectionLocation.class- Location pickerString.class- Text inputInteger.class- Number inputCohort.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 recordsperson: Demographic data (shared by patients, users, etc.)person_name: Patient namespatient_identifier: Patient IDs/MRNsencounter: Clinical encounters/visitsobs: Observations (vitals, lab results, diagnoses, etc.)visit: Visit recordslocation: Health facilitiesconcept: 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:
Report Manager Pattern - Deep dive into BaseReportManager
SQL-Based Reports - Tutorial for SQL reports
Cohort-Based Reports - Tutorial for aggregation reports