From 45549e8180276b04862b33b96aaddacad421f0c5 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Thu, 26 Feb 2026 19:36:28 +0100 Subject: [PATCH 1/4] O3-5452: Printing of encounters based on O3-forms --- api/pom.xml | 14 + .../PatientDocumentsActivator.java | 12 + .../patientdocuments/common/Helper.java | 23 +- .../common/PatientDocumentsConstants.java | 12 + .../PatientDocumentsPrivilegeConstants.java | 4 +- .../renderer/EncounterPdfReportRenderer.java | 122 ++++ .../renderer/EncounterPrintingContext.java | 43 ++ .../renderer/EncounterXmlBuilder.java | 521 ++++++++++++++++++ .../reports/EncounterPdfReport.java | 81 +++ .../reports/EncounterPdfReportManager.java | 89 +++ .../defaultEncounterFormFopStylesheet.xsl | 202 +++++++ .../EncounterDataPdfExportController.java | 134 +++++ omod/src/main/resources/config.xml | 8 + pom.xml | 1 + readme/EncounterPrinting.md | 144 +++++ 15 files changed, 1392 insertions(+), 18 deletions(-) create mode 100644 api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPdfReportRenderer.java create mode 100644 api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPrintingContext.java create mode 100644 api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterXmlBuilder.java create mode 100644 api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReport.java create mode 100644 api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManager.java create mode 100644 api/src/main/resources/defaultEncounterFormFopStylesheet.xsl create mode 100644 omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/EncounterDataPdfExportController.java create mode 100644 readme/EncounterPrinting.md diff --git a/api/pom.xml b/api/pom.xml index 14a78f7..f1249fd 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -33,6 +33,20 @@ provided + + org.openmrs.module + o3forms-omod + ${o3FormsVersion} + provided + + + + org.openmrs.module + webservices.rest-omod-common + ${openmrsWebRestVersion} + provided + + org.apache.xmlgraphics fop diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/PatientDocumentsActivator.java b/api/src/main/java/org/openmrs/module/patientdocuments/PatientDocumentsActivator.java index 5dda567..4d270c7 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/PatientDocumentsActivator.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/PatientDocumentsActivator.java @@ -12,6 +12,7 @@ import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_NAME; import org.openmrs.module.BaseModuleActivator; +import org.openmrs.module.patientdocuments.reports.EncounterPdfReportManager; import org.openmrs.module.patientdocuments.reports.PatientIdStickerReportManager; import org.openmrs.module.reporting.report.manager.ReportManagerUtil; import org.slf4j.Logger; @@ -42,6 +43,17 @@ public void started() { catch (Exception e) { log.error("Failed to set up report '{}'", patientIdStickerReportName, e); } + + EncounterPdfReportManager encounterReportManager = new EncounterPdfReportManager(); + String encounterPdfReportName = encounterReportManager.getName(); + + log.info("Setting up report: {} ...", encounterPdfReportName); + try { + ReportManagerUtil.setupReport(encounterReportManager); + log.info("Successfully set up report: {}", encounterPdfReportName); + } catch (Exception e) { + log.error("Failed to set up report '{}'", encounterPdfReportName, e); + } } /** diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/common/Helper.java b/api/src/main/java/org/openmrs/module/patientdocuments/common/Helper.java index df2b099..bfda735 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/common/Helper.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/common/Helper.java @@ -9,28 +9,17 @@ */ package org.openmrs.module.patientdocuments.common; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.io.IOUtils; import org.openmrs.util.OpenmrsClassLoader; +import java.io.InputStream; + public class Helper { - - /** - * Given a location on the classpath, return the contents of this resource as a String - */ - public static String getStringFromResource(String resourceName) { - InputStream is = null; + + public static InputStream getInputStreamByResource(String resourceName) { try { - is = OpenmrsClassLoader.getInstance().getResourceAsStream(resourceName); - return IOUtils.toString(is, StandardCharsets.UTF_8.name()); - } - catch (Exception e) { + return OpenmrsClassLoader.getInstance().getResourceAsStream(resourceName); + } catch (Exception e) { throw new IllegalArgumentException("Unable to load resource: " + resourceName, e); } - finally { - IOUtils.closeQuietly(is); - } } } diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsConstants.java b/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsConstants.java index 2ad9878..664127e 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsConstants.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsConstants.java @@ -31,4 +31,16 @@ public class PatientDocumentsConstants { * The path to the style sheet for Patient History reports. */ public static final String PATIENT_ID_STICKER_XSL_PATH = "patientIdStickerFopStylesheet.xsl"; + + public static final String DEFAULT_ENCOUNTER_FORM_XSL_PATH = "defaultEncounterFormFopStylesheet.xsl"; + + public static final String ENCOUNTER_PRINTING_HEADER_PREFIX = "report.encounterPrinting.header."; + + public static final String ENCOUNTER_PRINTING_FOOTER_PREFIX = "report.encounterPrinting.footer."; + + public static final String ENCOUNTER_STYLESHEET_KEY = "report.encounterPrinting.stylesheet"; + + public static final String NO_DATA_RECORDED_PLACEHOLDER = "No data recorded"; + + public static final String MISSING_VALUE_PLACEHOLDER = "-"; } diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsPrivilegeConstants.java b/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsPrivilegeConstants.java index 46ff4f0..51244c3 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsPrivilegeConstants.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/common/PatientDocumentsPrivilegeConstants.java @@ -15,5 +15,7 @@ public class PatientDocumentsPrivilegeConstants { public static final String VIEW_PATIENT_ID_STICKER = "App: Can generate a Patient Identity Sticker"; - + + public static final String PRINT_ENCOUNTER_FORMS_PRIVILEGE = "App: Print encounter forms"; + } diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPdfReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPdfReportRenderer.java new file mode 100644 index 0000000..4385559 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPdfReportRenderer.java @@ -0,0 +1,122 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.renderer; + +import org.apache.commons.lang3.StringUtils; +import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.apps.Fop; +import org.apache.fop.apps.FopFactory; +import org.apache.fop.apps.MimeConstants; +import org.openmrs.Encounter; +import org.openmrs.annotation.Handler; +import org.openmrs.api.EncounterService; +import org.openmrs.api.context.Context; +import org.openmrs.module.initializer.api.InitializerService; +import org.openmrs.module.patientdocuments.common.Helper; +import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants; +import org.openmrs.module.patientdocuments.reports.EncounterPdfReportManager; +import org.openmrs.module.reporting.report.ReportData; +import org.openmrs.module.reporting.report.ReportRequest; +import org.openmrs.module.reporting.report.renderer.RenderingException; +import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; +import org.springframework.stereotype.Component; + +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@Component +@Handler +public class EncounterPdfReportRenderer extends ReportDesignRenderer { + + @Override + public String getFilename(ReportRequest request) { + return "EncountersReport.pdf"; + } + + @Override + public String getRenderedContentType(ReportRequest request) { + return "application/pdf"; + } + + @Override + public void render(ReportData results, String argument, OutputStream out) throws RenderingException { + try { + String encounterUuidsParam = (String) getReportParam(results, EncounterPdfReportManager.ENCOUNTER_UUIDS_PARAM); + if (StringUtils.isBlank(encounterUuidsParam)) { + throw new RenderingException("No encounter UUIDs provided"); + } + + List encounters = collectEncounters(encounterUuidsParam); + Locale reportLocale = (Locale) getReportParam(results, EncounterPdfReportManager.ENCOUNTER_LOCALE_PARAM); + EncounterPrintingContext printingContext = new EncounterPrintingContext(encounters, reportLocale); + String encountersXml = new EncounterXmlBuilder().build(printingContext); + transformXmlToPdf(encountersXml, out); + } catch (Exception e) { + throw new RenderingException("Error generating PDF: " + e.getMessage(), e); + } + } + + private Object getReportParam(ReportData data, String paramName) { + return data.getContext().getParameterValue(paramName); + } + + private List collectEncounters(String encounterUuids) { + EncounterService encounterService = Context.getEncounterService(); + String[] uuids = encounterUuids.split(","); + List encounters = new ArrayList<>(); + for (String uuid : uuids) { + Encounter encounter = encounterService.getEncounterByUuid(uuid.trim()); + if (encounter != null) { + encounters.add(encounter); + } + } + + return encounters; + } + + private void transformXmlToPdf(String xmlData, OutputStream outStream) throws Exception { + FopFactory fopFactory = FopFactory.newInstance(new URI(".")); + FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); + Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, outStream); + + String stylesheetName = getStylesheetName(); + try (InputStream xslStream = Helper.getInputStreamByResource(stylesheetName)) { + if (xslStream == null) { + throw new FileNotFoundException("Stylesheet not found at " + stylesheetName); + } + + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(new StreamSource(xslStream)); + Source src = new StreamSource(new StringReader(xmlData)); + Result res = new SAXResult(fop.getDefaultHandler()); + transformer.transform(src, res); + } + } + + private String getStylesheetName() { + String stylesheetName = Context.getService(InitializerService.class).getValueFromKey(PatientDocumentsConstants.ENCOUNTER_STYLESHEET_KEY); + if (StringUtils.isBlank(stylesheetName)) { + stylesheetName = PatientDocumentsConstants.DEFAULT_ENCOUNTER_FORM_XSL_PATH; + } + return stylesheetName; + } +} diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPrintingContext.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPrintingContext.java new file mode 100644 index 0000000..9adb31f --- /dev/null +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterPrintingContext.java @@ -0,0 +1,43 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.renderer; + +import org.openmrs.Encounter; + +import java.util.List; +import java.util.Locale; + +public class EncounterPrintingContext { + private List encounters; + + private Locale locale; + + public EncounterPrintingContext(List encounters, Locale locale) { + this.encounters = encounters; + this.locale = locale; + } + + public List getEncounters() { + return encounters; + } + + public void setEncounters(List encounters) { + this.encounters = encounters; + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } +} + diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterXmlBuilder.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterXmlBuilder.java new file mode 100644 index 0000000..16c289a --- /dev/null +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/EncounterXmlBuilder.java @@ -0,0 +1,521 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.renderer; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang.StringUtils; +import org.openmrs.Concept; +import org.openmrs.ConceptName; +import org.openmrs.Encounter; +import org.openmrs.Obs; +import org.openmrs.Patient; +import org.openmrs.PatientIdentifier; +import org.openmrs.PersonAttribute; +import org.openmrs.User; +import org.openmrs.Visit; +import org.openmrs.VisitAttribute; +import org.openmrs.api.ConceptService; +import org.openmrs.api.context.Context; +import org.openmrs.messagesource.MessageSourceService; +import org.openmrs.module.initializer.api.InitializerService; +import org.openmrs.module.o3forms.api.O3FormsService; +import org.openmrs.module.patientdocuments.common.PatientDocumentsConstants; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.util.OpenmrsUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + + +public class EncounterXmlBuilder { + + private static final String QUESTION_OPTIONS_SECTION = "questionOptions"; + + private static final String QUESTIONS_SECTION = "questions"; + + private static final String LABEL_FIELD = "label"; + + private InitializerService initializerService; + + private InitializerService getInitializerService() { + if (initializerService == null) { + initializerService = Context.getService(InitializerService.class); + } + return initializerService; + } + + public String build(EncounterPrintingContext printingContext) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + xml.append(""); + + O3FormsService o3FormsService = Context.getService(O3FormsService.class); + User user = Context.getAuthenticatedUser(); + for (Encounter encounter : printingContext.getEncounters()) { + if (encounter.getForm() != null) { + xml.append(renderSingleEncounter(encounter, printingContext.getLocale(), o3FormsService, user)); + } + } + + xml.append(""); + + return xml.toString(); + } + + private String renderSingleEncounter(Encounter encounter, Locale locale, O3FormsService o3FormsService, User user) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + + xml.append(buildHeaderContent(encounter, locale)); + xml.append(buildMainContent(encounter, locale, o3FormsService)); + xml.append(buildFooterContent(user, locale)); + + xml.append(""); + return xml.toString(); + } + + private String buildHeaderContent(Encounter encounter, Locale locale) { + StringBuilder xml = new StringBuilder(); + Visit visit = encounter.getVisit(); + Patient patient = encounter.getPatient(); + + if (isHeaderFieldEnabled("patientName")) { + xml.append("") + .append(escape(patient.getPersonName().getFullName())) + .append(""); + } + + if (isHeaderFieldEnabled("formName")) { + xml.append("true"); + } + + if (isHeaderFieldEnabled("location")) { + String locationName = encounter.getLocation() != null ? encounter.getLocation().getName() + : PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER; + xml.append("") + .append(escape(locationName)) + .append(""); + } + + if (isHeaderFieldEnabled("encounterDate")) { + xml.append("") + .append(escape(OpenmrsUtil.getDateFormat(locale).format(encounter.getEncounterDatetime()))) + .append(""); + } + + if (isHeaderFieldEnabled("visitStartDate")) { + String visitStartDate = (visit != null && visit.getStartDatetime() != null) + ? OpenmrsUtil.getDateFormat(locale).format(visit.getStartDatetime()) + : PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER; + xml.append("") + .append(escape(visitStartDate)) + .append(""); + } + + if (isHeaderFieldEnabled("visitEndDate")) { + String visitEndDate = (visit != null && visit.getStopDatetime() != null) + ? OpenmrsUtil.getDateFormat(locale).format(visit.getStopDatetime()) + : PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER; + xml.append("") + .append(escape(visitEndDate)) + .append(""); + } + + if (isHeaderFieldEnabled("visitType")) { + String visitTypeName = (visit != null && visit.getVisitType() != null) + ? visit.getVisitType().getName() + : PatientDocumentsConstants.MISSING_VALUE_PLACEHOLDER; + xml.append("") + .append(escape(visitTypeName)) + .append(""); + } + + if (isHeaderFieldEnabled("patientIdentifiers")) { + xml.append(renderPatientIdentifiers(patient)); + } + + if (isHeaderFieldEnabled("personAttributes")) { + xml.append(renderPersonAttributes(patient)); + } + + if (isHeaderFieldEnabled("visitAttributes")) { + xml.append(renderVisitAttributes(visit)); + } + + return xml.toString(); + } + + private String buildFooterContent(User user, Locale locale) { + StringBuilder xml = new StringBuilder(); + + String userName = (user != null && user.getPersonName() != null) ? user.getPersonName().getFullName() : "System"; + String systemId = (user != null && user.getSystemId() != null) ? user.getSystemId() : "Unknown"; + String printTimestamp = OpenmrsUtil.getDateTimeFormat(locale).format(new Date()); + + String printedBy = String.format("Printed by %s (%s) at %s", userName, systemId, printTimestamp); + xml.append("").append(escape(printedBy)).append(""); + + String customFooterText = getInitializerService() + .getValueFromKey(PatientDocumentsConstants.ENCOUNTER_PRINTING_FOOTER_PREFIX + "customText"); + if (StringUtils.isNotBlank(customFooterText)) { + xml.append("").append(escape(customFooterText)).append(""); + } + + return xml.toString(); + } + + private String buildMainContent(Encounter encounter, Locale locale, O3FormsService o3FormsService) { + StringBuilder xml = new StringBuilder(); + + xml.append("") + .append(escape(encounter.getForm().getName())) + .append(""); + + String formUuid = encounter.getForm().getUuid(); + try { + SimpleObject schema = o3FormsService.compileFormSchema(formUuid); + + Map> obsMap = buildObsMap(encounter); + + xml.append(""); + List> pages = schema.get("pages"); + if (pages != null) { + for (Map page : pages) { + xml.append(renderPage(page, obsMap, locale)); + } + } + xml.append(""); + } catch (Exception e) { + xml.append("Could not render form: ") + .append(escape(e.getMessage())) + .append(""); + } + + return xml.toString(); + } + + private Map> buildObsMap(Encounter encounter) { + Map> obsMap = new HashMap<>(); + for (Obs obs : encounter.getAllObs()) { + String conceptUuid = obs.getConcept().getUuid(); + obsMap.computeIfAbsent(conceptUuid, k -> new ArrayList<>()).add(obs); + } + return obsMap; + } + + private String renderPage(Map page, Map> obsMap, Locale locale) { + StringBuilder xml = new StringBuilder(); + String originalLabel = (String) page.getOrDefault(LABEL_FIELD, "Page"); + String localizedLabel = getLocalizedLabel(originalLabel, null, locale); + + xml.append(""); + + List> sections = (List>) page.get("sections"); + if (sections != null) { + for (Map section : sections) { + xml.append(renderSection(section, obsMap, locale)); + } + } + + xml.append(""); + return xml.toString(); + } + + private String renderSection(Map section, Map> obsMap, Locale locale) { + StringBuilder xml = new StringBuilder(); + String originalLabel = (String) section.getOrDefault(LABEL_FIELD, ""); + String localizedLabel = getLocalizedLabel(originalLabel, null, locale); + + xml.append("
"); + + List> questions = (List>) section.get(QUESTIONS_SECTION); + if (questions != null) { + for (Map question : questions) { + xml.append(renderQuestion(question, obsMap, locale)); + } + } + + xml.append("
"); + return xml.toString(); + } + + private String renderQuestion(Map question, Map> obsMap, Locale locale) { + String type = (String) question.get("type"); + + if ("markdown".equals(type) || "label".equals(type)) { + return renderMarkdownQuestion(question, locale); + } + + if ("obsGroup".equals(type)) { + return renderObsGroupQuestion(question, obsMap, locale); + } + + if ("obs".equals(type)) { + return renderObsQuestion(question, obsMap, locale); + } + + return renderSubQuestions(question, obsMap, locale); + } + + private String renderMarkdownQuestion(Map question, Locale locale) { + StringBuilder sb = new StringBuilder(); + String text = formatValueAsText(question.get("value")); + sb.append("").append(escape(text)).append(""); + return sb.toString(); + } + + private String renderObsGroupQuestion(Map question, Map> obsMap, Locale locale) { + StringBuilder sb = new StringBuilder(); + String label = getQuestionLabel(question, locale); + + if (StringUtils.isNotBlank(label)) { + sb.append("").append(escape(label)).append(""); + } + + sb.append(renderSubQuestions(question, obsMap, locale)); + return sb.toString(); + } + + private String renderObsQuestion(Map question, Map> obsMap, Locale locale) { + StringBuilder sb = new StringBuilder(); + String label = getQuestionLabel(question, locale); + + sb.append(""); + String obsValue = findObsValue(question, obsMap, locale); + sb.append(escape(obsValue)); + sb.append(""); + + sb.append(renderSubQuestions(question, obsMap, locale)); + return sb.toString(); + } + + private String renderSubQuestions(Map question, Map> obsMap, Locale locale) { + StringBuilder sb = new StringBuilder(); + List> subQuestions = (List>) question.get(QUESTIONS_SECTION); + if (subQuestions != null) { + for (Map subQuestion : subQuestions) { + sb.append(renderQuestion(subQuestion, obsMap, locale)); + } + } + return sb.toString(); + } + + private String getQuestionLabel(Map question, Locale locale) { + String originalLabel = (String) question.getOrDefault(LABEL_FIELD, ""); + String conceptRef = extractConceptRef(question); + return getLocalizedLabel(originalLabel, conceptRef, locale); + } + + private String extractConceptRef(Map question) { + if (!question.containsKey(QUESTION_OPTIONS_SECTION)) { + return null; + } + Map options = (Map) question.get(QUESTION_OPTIONS_SECTION); + return (String) options.get("concept"); + } + + private String formatValueAsText(Object value) { + if (value instanceof List) { + return String.join("\n", (List) value); + } + return value != null ? value.toString() : ""; + } + + private String findObsValue(Map question, Map> obsMap, Locale locale) { + String conceptRef = extractConceptRef(question); + if (conceptRef == null) { + return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER; + } + + Concept concept = findConceptByRef(conceptRef); + if (concept == null || !obsMap.containsKey(concept.getUuid())) { + return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER; + } + + List observations = obsMap.get(concept.getUuid()); + if (observations.isEmpty()) { + return PatientDocumentsConstants.NO_DATA_RECORDED_PLACEHOLDER; + } + + return observations.stream() + .map(obs -> getLocalizedObsValue(obs, locale)) + .collect(Collectors.joining(", ")); + } + + private String escape(String input) { + return StringEscapeUtils.escapeXml(StringUtils.defaultString(input)); + } + + private Concept findConceptByRef(String conceptRef) { + Concept concept = getConceptService().getConceptByReference(conceptRef); + if (concept == null) { + concept = getConceptService().getConceptByUuid(conceptRef); // fallback for UUIDs that are non-UUID format + } + + return concept; + } + + private String getLocalizedObsValue(Obs obs, Locale locale) { + if (obs.getValueCoded() != null) { + ConceptName localizedName = obs.getValueCoded().getName(locale); + if (localizedName != null) { + return localizedName.getName(); + } else { + return obs.getValueCoded().getDisplayString(); + } + } + return obs.getValueAsString(locale); + } + + private String getLocalizedLabel(String defaultLabel, String conceptRef, Locale locale) { + if (StringUtils.isNotBlank(conceptRef)) { + Concept concept = getConceptService().getConceptByReference(conceptRef); + if (concept != null) { + ConceptName localizedName = concept.getName(locale); + if (localizedName != null) { + return localizedName.getName(); + } + } + } + + if (StringUtils.isNotBlank(defaultLabel)) { + try { + String translated = getMessageSourceService().getMessage(defaultLabel, null, locale); + if (StringUtils.isNotBlank(translated) && !translated.equals(defaultLabel)) { + return translated; + } + } catch (Exception ignored) { + } + } + + return defaultLabel; + } + + private MessageSourceService getMessageSourceService() { + return Context.getMessageSourceService(); + } + + private ConceptService getConceptService() { + return Context.getConceptService(); + } + + private boolean isHeaderFieldEnabled(String fieldName) { + String configKey = PatientDocumentsConstants.ENCOUNTER_PRINTING_HEADER_PREFIX + fieldName; + Boolean enabled = getInitializerService().getBooleanFromKey(configKey); + return Boolean.TRUE.equals(enabled); + } + + private String renderPatientIdentifiers(Patient patient) { + StringBuilder xml = new StringBuilder(); + String configuredPatientIdentifierTypes = getInitializerService() + .getValueFromKey(PatientDocumentsConstants.ENCOUNTER_PRINTING_HEADER_PREFIX + "patientIdentifierTypes"); + List identifierTypes = parseCommaSeparatedList(configuredPatientIdentifierTypes); + + List identifiers = new ArrayList<>(patient.getIdentifiers()); + boolean hasIdentifiers = false; + + for (PatientIdentifier identifier : identifiers) { + String identifierTypeName = identifier.getIdentifierType().getName(); + if (identifierTypes.isEmpty() || identifierTypes.contains(identifierTypeName)) { + if (!hasIdentifiers) { + xml.append(""); + hasIdentifiers = true; + } + xml.append("") + .append(escape(identifier.getIdentifier())) + .append(""); + } + } + + if (hasIdentifiers) { + xml.append(""); + } + + return xml.toString(); + } + + private String renderPersonAttributes(Patient patient) { + StringBuilder xml = new StringBuilder(); + String configuredPersonAttributeTypes = getInitializerService() + .getValueFromKey(PatientDocumentsConstants.ENCOUNTER_PRINTING_HEADER_PREFIX + "personAttributeTypes"); + List attributeTypes = parseCommaSeparatedList(configuredPersonAttributeTypes); + + List attributes = new ArrayList<>(patient.getAttributes()); + boolean hasAttributes = false; + + for (PersonAttribute attribute : attributes) { + String attributeTypeName = attribute.getAttributeType().getName(); + if (attributeTypes.isEmpty() || attributeTypes.contains(attributeTypeName)) { + if (!hasAttributes) { + xml.append(""); + hasAttributes = true; + } + xml.append("") + .append(escape(attribute.getValue() != null ? attribute.getValue() : "")) + .append(""); + } + } + + if (hasAttributes) { + xml.append(""); + } + + return xml.toString(); + } + + private String renderVisitAttributes(Visit visit) { + StringBuilder xml = new StringBuilder(); + if (visit == null) { + return xml.toString(); + } + + String configuredVisitAttributeTypes = getInitializerService() + .getValueFromKey(PatientDocumentsConstants.ENCOUNTER_PRINTING_HEADER_PREFIX + "visitAttributeTypes"); + List attributeTypes = parseCommaSeparatedList(configuredVisitAttributeTypes); + + List attributes = new ArrayList<>(visit.getActiveAttributes()); + boolean hasAttributes = false; + + for (VisitAttribute attribute : attributes) { + String attributeTypeName = attribute.getAttributeType().getName(); + if (attributeTypes.isEmpty() || attributeTypes.contains(attributeTypeName)) { + if (!hasAttributes) { + xml.append(""); + hasAttributes = true; + } + xml.append("") + .append(escape(attribute.getValue() != null ? attribute.getValue().toString() : "")) + .append(""); + } + } + + if (hasAttributes) { + xml.append(""); + } + + return xml.toString(); + } + + private List parseCommaSeparatedList(String value) { + if (StringUtils.isBlank(value)) { + return new ArrayList<>(); + } + return Arrays.stream(value.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } +} diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReport.java new file mode 100644 index 0000000..cb74fa5 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReport.java @@ -0,0 +1,81 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.reports; + +import org.openmrs.api.context.Context; +import org.openmrs.module.patientdocuments.renderer.EncounterPdfReportRenderer; +import org.openmrs.module.reporting.evaluation.parameter.Mapped; +import org.openmrs.module.reporting.report.ReportRequest; +import org.openmrs.module.reporting.report.definition.ReportDefinition; +import org.openmrs.module.reporting.report.definition.service.ReportDefinitionService; +import org.openmrs.module.reporting.report.renderer.RenderingMode; +import org.openmrs.module.reporting.report.service.ReportService; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +public class EncounterPdfReport { + + public SimpleObject triggerEncountersPrinting(List encounterUuids) { + ReportService reportService = Context.getService(ReportService.class); + ReportRequest reportRequest = buildReportRequest(encounterUuids, reportService); + reportRequest = reportService.queueReport(reportRequest); + reportService.processNextQueuedReports(); + + SimpleObject response = new SimpleObject(); + response.put("uuid", reportRequest.getUuid()); + response.put("status", reportRequest.getStatus().name()); + + return response; + } + + private ReportRequest buildReportRequest(List encounterUuids, ReportService reportService) { + ReportDefinitionService reportDefinitionService = Context.getService(ReportDefinitionService.class); + ReportDefinition reportDef = + reportDefinitionService.getDefinitionByUuid(EncounterPdfReportManager.REPORT_DEFINITION_UUID); + if (reportDef == null) { + throw new RuntimeException("Report definition not found"); + } + + Map parameterMappings = new HashMap<>(); + String joinedUuids = String.join(",", encounterUuids); + parameterMappings.put(EncounterPdfReportManager.ENCOUNTER_UUIDS_PARAM, joinedUuids); + parameterMappings.put(EncounterPdfReportManager.ENCOUNTER_LOCALE_PARAM, Context.getLocale()); + + Mapped mappedReportDef = new Mapped<>(); + mappedReportDef.setParameterizable(reportDef); + mappedReportDef.setParameterMappings(parameterMappings); + + ReportRequest reportRequest = new ReportRequest(); + reportRequest.setReportDefinition(mappedReportDef); + reportRequest.setPriority(ReportRequest.Priority.NORMAL); + + RenderingMode renderingMode = null; + for (RenderingMode mode : reportService.getRenderingModes(reportDef)) { + if (mode.getRenderer() instanceof EncounterPdfReportRenderer) { + renderingMode = mode; + break; + } + } + + if (renderingMode == null) { + throw new IllegalStateException( + "No rendering mode configured for " + EncounterPdfReportRenderer.class.getName()); + } + + reportRequest.setRenderingMode(renderingMode); + + return reportRequest; + } +} diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManager.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManager.java new file mode 100644 index 0000000..2b63601 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManager.java @@ -0,0 +1,89 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.reports; + +import org.openmrs.module.patientdocuments.renderer.EncounterPdfReportRenderer; +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.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +@Component +public class EncounterPdfReportManager extends BaseReportManager { + + public static final String REPORT_DESIGN_UUID = "1ce73eb4-10bb-11f1-a6da-0242ac1e0002"; + + public static final String REPORT_DESIGN_NAME = "Encounter PDF Design"; + + public static final String REPORT_DEFINITION_UUID = "0e89ec4c-10bb-11f1-a6da-0242ac1e0002"; + + public static final String REPORT_DEFINITION_NAME = "Generic Encounter PDF Report"; + + public static final String REPORT_DESCRIPTION = "Dynamically generates PDF printouts for one or more clinical encounters based on O3 forms."; + + public static final String ENCOUNTER_UUIDS_PARAM = "encounterUuids"; + + public static final String ENCOUNTER_LOCALE_PARAM = "locale"; + + @Override + public String getUuid() { + return REPORT_DEFINITION_UUID; + } + + @Override + public String getName() { + return REPORT_DEFINITION_NAME; + } + + @Override + public String getDescription() { + return REPORT_DESCRIPTION; + } + + @Override + public String getVersion() { + return "2.0"; + } + + @Override + public List getParameters() { + List params = new ArrayList<>(); + params.add(new Parameter(ENCOUNTER_UUIDS_PARAM, "Encounter UUIDs (comma separated)", String.class)); + params.add(new Parameter(ENCOUNTER_LOCALE_PARAM, "Locale", Locale.class)); + return params; + } + + @Override + public ReportDefinition constructReportDefinition() { + ReportDefinition rd = new ReportDefinition(); + rd.setUuid(getUuid()); + rd.setName(getName()); + rd.setDescription(getDescription()); + rd.setParameters(getParameters()); + return rd; + } + + @Override + public List constructReportDesigns(ReportDefinition reportDefinition) { + ReportDesign reportDesign = new ReportDesign(); + reportDesign.setName(REPORT_DESIGN_NAME); + reportDesign.setUuid(REPORT_DESIGN_UUID); + reportDesign.setReportDefinition(reportDefinition); + reportDesign.setRendererType(EncounterPdfReportRenderer.class); + + return Collections.singletonList(reportDesign); + } +} diff --git a/api/src/main/resources/defaultEncounterFormFopStylesheet.xsl b/api/src/main/resources/defaultEncounterFormFopStylesheet.xsl new file mode 100644 index 0000000..a1365f0 --- /dev/null +++ b/api/src/main/resources/defaultEncounterFormFopStylesheet.xsl @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + : + + + : + + + + + + + + + + + + + + + + + + + + + + Patient Name: + + + Location: + + + Visit Start: + + + + : + + + + Form: + + + + + + + + + + + + Visit Date: + + + Encounter Date: + + + Visit End: + + + Visit Type: + + + + : + + + + + : + + + + + + + + + + + + + + + + + + + + + + + + + + + Page + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No data recorded for this section. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/EncounterDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/EncounterDataPdfExportController.java new file mode 100644 index 0000000..95be053 --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/EncounterDataPdfExportController.java @@ -0,0 +1,134 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.web.rest.controller; + +import org.openmrs.api.context.Context; +import org.openmrs.api.context.ContextAuthenticationException; +import org.openmrs.module.patientdocuments.common.PatientDocumentsPrivilegeConstants; +import org.openmrs.module.patientdocuments.reports.EncounterPdfReport; +import org.openmrs.module.reporting.report.ReportRequest; +import org.openmrs.module.reporting.report.service.ReportService; +import org.openmrs.module.webservices.rest.SimpleObject; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.openmrs.module.webservices.rest.web.v1_0.controller.BaseRestController; +import org.openmrs.util.OpenmrsUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.io.File; +import java.nio.file.Files; +import java.util.Date; +import java.util.List; + +import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; + +@Controller +@RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/" + MODULE_ARTIFACT_ID + "/encounters") +public class EncounterDataPdfExportController extends BaseRestController { + + @Autowired + private EncounterPdfReport encounterPdfReport; + + @RequestMapping(method = RequestMethod.POST) + @ResponseBody + public ResponseEntity triggerEncounterPrinting(@RequestBody List encounterUuids) { + if (encounterUuids == null || encounterUuids.isEmpty()) { + return ResponseEntity.badRequest().body(createError("No encounter UUIDs provided")); + } + + try { + validatePrivileges(); + SimpleObject response = encounterPdfReport.triggerEncountersPrinting(encounterUuids); + return ResponseEntity.ok(response); + } catch (ContextAuthenticationException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(createError(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(createError("Failed to queue print job: " + e.getMessage())); + } + } + + @RequestMapping(value = "/status/{requestUuid}", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity getReportStatus(@PathVariable("requestUuid") String requestUuid) { + try { + validatePrivileges(); + ReportService reportService = Context.getService(ReportService.class); + ReportRequest request = reportService.getReportRequestByUuid(requestUuid); + + if (request == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body( + createError("Report request with id: " + requestUuid + " not found")); + } + + SimpleObject response = new SimpleObject(); + response.put("uuid", request.getUuid()); + response.put("status", request.getStatus().name()); + + return ResponseEntity.ok(response); + } catch (ContextAuthenticationException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(createError(e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(createError(e.getMessage())); + } + } + + @RequestMapping(value = "/download/{requestUuid}", method = RequestMethod.GET) + public ResponseEntity downloadPdf(@PathVariable("requestUuid") String requestUuid) { + try { + validatePrivileges(); + ReportService reportService = Context.getService(ReportService.class); + ReportRequest request = reportService.getReportRequestByUuid(requestUuid); + + if (request == null || request.getStatus() != ReportRequest.Status.COMPLETED) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); + } + + File file = reportService.getReportOutputFile(request); + if (file == null || !file.exists()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); + } + + byte[] pdfBytes = Files.readAllBytes(file.toPath()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + + String dateStr = OpenmrsUtil.getDateFormat(Context.getLocale()).format(new Date()); + String filename = dateStr + "_PatientReport.pdf"; + headers.add("Content-Disposition", "attachment; filename=\"" + filename + "\""); + + return new ResponseEntity<>(pdfBytes, headers, HttpStatus.OK); + } catch (ContextAuthenticationException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + } + + private void validatePrivileges() { + Context.requirePrivilege(PatientDocumentsPrivilegeConstants.PRINT_ENCOUNTER_FORMS_PRIVILEGE); + } + + private SimpleObject createError(String message) { + SimpleObject error = new SimpleObject(); + error.put("error", message); + return error; + } +} diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index d5b5c31..e62d552 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -21,6 +21,9 @@ org.openmrs.module.initializer + + org.openmrs.module.o3forms + @@ -38,6 +41,11 @@ App: Can generate a Patient Identity Sticker Allows user to access the patient identifier sticker report. + + + App: Print encounter forms + Allows user to print encounter forms + diff --git a/pom.xml b/pom.xml index 848eed7..48a6b70 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 2.9.0 1.3.11 2.50.0 + 2.3.0 diff --git a/readme/EncounterPrinting.md b/readme/EncounterPrinting.md new file mode 100644 index 0000000..7b41bac --- /dev/null +++ b/readme/EncounterPrinting.md @@ -0,0 +1,144 @@ +# Encounter Printing Configuration + +This configuration defines the settings for generating printable encounter forms (PDF reports). Each configuration property controls which header/footer fields are displayed on the report. + +## Configuration Fields + +### Header Fields +These flags control which patient and visit information is displayed in the header of each page. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.header.patientName` | Show patient name in header | +| `report.encounterPrinting.header.location` | Show location in header | +| `report.encounterPrinting.header.formName` | Show form name in header | +| `report.encounterPrinting.header.encounterDate` | Show encounter date in header | +| `report.encounterPrinting.header.visitStartDate` | Show visit start datetime in header | +| `report.encounterPrinting.header.visitEndDate` | Show visit stop datetime in header | +| `report.encounterPrinting.header.visitType` | Show visit type in header | + +**Note:** The form name (`formName`) is always displayed in the body of the report as the title, regardless of this configuration. Set `formName` to `true` to also display it in the header. + +**Note:** For fields that depend on Visit (visitStartDate, visitEndDate, visitType), if no visit exists or the value is not set, a "-" placeholder will be displayed. + +### Patient Identifiers +Configuration for displaying patient identifiers in the header. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.header.patientIdentifiers` | Enable patient identifiers display | +| `report.encounterPrinting.header.patientIdentifierTypes` | Comma-separated identifier type **names** to display | + +### Person Attributes +Configuration for displaying patient person attributes in the header. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.header.personAttributes` | Enable person attributes display | +| `report.encounterPrinting.header.personAttributeTypes` | Comma-separated person attribute type **names** to display | + +### Visit Attributes +Configuration for displaying visit attributes in the header. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.header.visitAttributes` | Enable visit attributes display | +| `report.encounterPrinting.header.visitAttributeTypes` | Comma-separated visit attribute type **names** to display | + +### Footer +Configuration for the footer section. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.footer.customText` | Custom text to display below the "Printed by" statement | + +### Stylesheet +Configuration for the XSL stylesheet used to render the PDF. + +| Key | Description | +|-----|-------------| +| `report.encounterPrinting.stylesheet` | XSL stylesheet filename to use for rendering (e.g., `defaultEncounterFormFopStylesheet.xsl`) | + +## Example Configuration + +```json +{ + "report.encounterPrinting.header.patientName": "true", + "report.encounterPrinting.header.location": "true", + "report.encounterPrinting.header.formName": "true", + "report.encounterPrinting.header.encounterDate": "true", + "report.encounterPrinting.header.visitStartDate": "true", + "report.encounterPrinting.header.visitEndDate": "true", + "report.encounterPrinting.header.visitType": "true", + "report.encounterPrinting.header.patientIdentifiers": "true", + "report.encounterPrinting.header.patientIdentifierTypes": "OpenMRS ID", + "report.encounterPrinting.header.personAttributes": "true", + "report.encounterPrinting.header.personAttributeTypes": "Telephone Number,Health District", + "report.encounterPrinting.header.visitAttributes": "true", + "report.encounterPrinting.header.visitAttributeTypes": "Payment Method", + "report.encounterPrinting.footer.customText": "Organization Name", + "report.encounterPrinting.stylesheet": "defaultEncounterFormFopStylesheet.xsl" +} +``` + +## REST API + +### Endpoints + +The module exposes REST endpoints for printing encounter forms: + +#### 1. Trigger Print Job +``` +POST /openmrs/ws/rest/v1/patientdocuments/encounters +``` + +**Request Body:** Array of encounter UUIDs +```json +["encounter-uuid-1", "encounter-uuid-2"] +``` + +**Response:** +```json +{ + "requestUuid": "report-request-uuid", + "status": "QUEUED" +} +``` + +#### 2. Check Print Job Status +``` +GET /openmrs/ws/rest/v1/patientdocuments/encounters/status/{requestUuid} +``` + +**Response:** +```json +{ + "uuid": "report-request-uuid", + "status": "COMPLETED" +} +``` + +#### 3. Download PDF +``` +GET /openmrs/ws/rest/v1/patientdocuments/encounters/download/{requestUuid} +``` + +**Response:** PDF file download + +### Required Privilege + +To use the encounter printing API, users must have the following privilege: + +| Privilege | Description | +|-----------|-------------| +| `App: Print encounter forms` | Required to trigger print jobs and download PDFs | + +This privilege should be assigned to users or roles that need to print encounter forms. + +## Notes + +- All boolean values should be strings `"true"` or `"false"` to maintain compatibility with properties-based parsers. +- For patient identifiers, person attributes, and visit attributes, use the **type names** (e.g., "Patient ID", "Telephone Number") not UUIDs. +- The customFooterText appears below the "Printed by..." line on every page. +- Header fields are displayed in a two-column table layout. +- When no configuration is provided, no header fields will be displayed. From 71488b613f8aa28128b78ef6e86d42fcfb2776b9 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Wed, 11 Mar 2026 17:00:23 +0100 Subject: [PATCH 2/4] O3-5452: Added tests and fixed build process --- api/pom.xml | 37 +++++++ .../EncounterPdfReportManagerTest.java | 97 +++++++++++++++++++ .../testconfig/WebServicesRestTestConfig.java | 22 +++++ .../resources/TestingApplicationContext.xml | 8 ++ .../EncounterDataPdfExportControllerTest.java | 71 ++++++++++++++ 5 files changed, 235 insertions(+) create mode 100644 api/src/test/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManagerTest.java create mode 100644 api/src/test/java/org/openmrs/module/patientdocuments/testconfig/WebServicesRestTestConfig.java create mode 100644 omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/EncounterDataPdfExportControllerTest.java diff --git a/api/pom.xml b/api/pom.xml index f1249fd..4379285 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -129,6 +129,31 @@
+ + + org.openmrs.module + webservices.rest-omod-common + ${openmrsWebRestVersion} + provided + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + @@ -168,5 +193,17 @@ false + + + + org.apache.maven.plugins + maven-surefire-plugin + + + com.fasterxml.jackson.datatype:jackson-datatype-jsr310 + + + + diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManagerTest.java b/api/src/test/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManagerTest.java new file mode 100644 index 0000000..e4c1ed5 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/patientdocuments/reports/EncounterPdfReportManagerTest.java @@ -0,0 +1,97 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.patientdocuments.reports; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.List; +import java.util.Locale; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openmrs.module.patientdocuments.renderer.EncounterPdfReportRenderer; +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.ReportManagerUtil; +import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; + +public class EncounterPdfReportManagerTest extends BaseModuleContextSensitiveTest { + + @BeforeEach + public void setUp() throws Exception { + ReportManagerUtil.setupReport(new EncounterPdfReportManager()); + } + + private ReportDesign setupAndReturnReportDesign() { + EncounterPdfReportManager manager = new EncounterPdfReportManager(); + ReportDefinition reportDefinition = manager.constructReportDefinition(); + assertNotNull(reportDefinition); + assertEquals(EncounterPdfReportManager.REPORT_DEFINITION_NAME, reportDefinition.getName()); + assertEquals(EncounterPdfReportManager.REPORT_DEFINITION_UUID, reportDefinition.getUuid()); + assertNotNull(reportDefinition.getParameters()); + assertThat(reportDefinition.getParameters().size(), Matchers.is(2)); + + List reportDesigns = manager.constructReportDesigns(reportDefinition); + assertNotNull(reportDesigns); + assertThat(reportDesigns.size(), org.hamcrest.Matchers.is(1)); + + return reportDesigns.get(0); + } + + @Test + public void setupReport_shouldSetupEncounterPdfReport() throws Exception { + ReportDesign reportDesign = setupAndReturnReportDesign(); + assertEquals(EncounterPdfReportManager.REPORT_DESIGN_UUID, reportDesign.getUuid()); + assertEquals(EncounterPdfReportManager.REPORT_DESIGN_NAME, reportDesign.getName()); + } + + @Test + public void constructReportDefinition_shouldHaveCorrectParameters() throws Exception { + EncounterPdfReportManager manager = new EncounterPdfReportManager(); + List parameters = manager.getParameters(); + + assertNotNull(parameters); + assertEquals(2, parameters.size()); + + boolean hasEncounterUuidsParam = false; + boolean hasLocaleParam = false; + + for (Parameter param : parameters) { + if (EncounterPdfReportManager.ENCOUNTER_UUIDS_PARAM.equals(param.getName())) { + hasEncounterUuidsParam = true; + assertEquals(String.class, param.getType()); + } + if (EncounterPdfReportManager.ENCOUNTER_LOCALE_PARAM.equals(param.getName())) { + hasLocaleParam = true; + assertEquals(Locale.class, param.getType()); + } + } + + assertThat("Should have encounterUuids parameter", hasEncounterUuidsParam, Matchers.is(true)); + assertThat("Should have locale parameter", hasLocaleParam, Matchers.is(true)); + } + + @Test + public void constructReportDesigns_shouldUseEncounterPdfReportRenderer() throws Exception { + EncounterPdfReportManager manager = new EncounterPdfReportManager(); + ReportDefinition reportDefinition = manager.constructReportDefinition(); + List reportDesigns = manager.constructReportDesigns(reportDefinition); + + assertNotNull(reportDesigns); + assertEquals(1, reportDesigns.size()); + + ReportDesign reportDesign = reportDesigns.get(0); + assertEquals(EncounterPdfReportRenderer.class, reportDesign.getRendererType()); + } +} diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/testconfig/WebServicesRestTestConfig.java b/api/src/test/java/org/openmrs/module/patientdocuments/testconfig/WebServicesRestTestConfig.java new file mode 100644 index 0000000..dce01c0 --- /dev/null +++ b/api/src/test/java/org/openmrs/module/patientdocuments/testconfig/WebServicesRestTestConfig.java @@ -0,0 +1,22 @@ +package org.openmrs.module.patientdocuments.testconfig; + +import org.openmrs.module.webservices.rest.web.api.RestService; +import org.openmrs.module.webservices.rest.web.api.RestHelperService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.Mockito.mock; + +@Configuration +public class WebServicesRestTestConfig { + + @Bean + public RestService restService() { + return mock(RestService.class); + } + + @Bean(name = "restHelperService") + public RestHelperService restHelperService() { + return mock(RestHelperService.class); + } +} \ No newline at end of file diff --git a/api/src/test/resources/TestingApplicationContext.xml b/api/src/test/resources/TestingApplicationContext.xml index fdcb39a..4ec4f81 100644 --- a/api/src/test/resources/TestingApplicationContext.xml +++ b/api/src/test/resources/TestingApplicationContext.xml @@ -10,6 +10,14 @@ + + + + +