diff --git a/Pipfile b/Pipfile index b7e6136903..6beddc1590 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,6 @@ django = "==4.2.15" django-environ = "==0.11.2" django-cors-headers = "==4.3.1" django-filter = "==24.2" -django-hardcopy = "==0.1.4" django-maintenance-mode = "==0.21.1" django-model-utils = "==4.5.1" django-multiselectfield = "==0.1.12" diff --git a/Pipfile.lock b/Pipfile.lock index 51069ba3bd..6cfb4f13d5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "17b769e6a740f13f82e44bce08575af72844ffab86d295d27ba7fca2e06f6a72" + "sha256": "0f478643ec74971469522c09c3e635a7ca9346c16a687ae7c14c7f638335cb1e" }, "pipfile-spec": 6, "requires": { @@ -509,14 +509,6 @@ "markers": "python_version >= '3.8'", "version": "==24.2" }, - "django-hardcopy": { - "hashes": [ - "sha256:1ceda5fb262c9787fbd2554b4b92db9c8feebdd6da4ef6c1746e1aa8c8e55377", - "sha256:8bf962245d95918bd5c4b4e1b809a3d029b19d4f331e1c7c5f65768fb8366ed8" - ], - "index": "pypi", - "version": "==0.1.4" - }, "django-maintenance-mode": { "hashes": [ "sha256:b79afddb671c59972ae542e4fafbc99117d2d37991843eaaa837e328eed12b1b", diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index 95aa138331..b166971dc9 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -1,7 +1,11 @@ +import tempfile + from django.db import transaction from django.db.models import Prefetch from django.db.models.query_utils import Q -from django.shortcuts import get_object_or_404, render +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.utils import timezone from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema from dry_rest_permissions.generics import DRYPermissions @@ -296,7 +300,18 @@ def dev_preview_discharge_summary(request, consultation_id): if not consultation: raise NotFound({"detail": "Consultation not found"}) data = discharge_summary.get_discharge_summary_data(consultation) - return render(request, "reports/patient_discharge_summary_pdf.html", data) + data["date"] = timezone.now() + + with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file: + discharge_summary.generate_discharge_summary_pdf(data, tmp_file) + + with open(tmp_file.name, "rb") as pdf_file: + pdf_content = pdf_file.read() + + response = HttpResponse(pdf_content, content_type="application/pdf") + response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"' + + return response class PatientConsentViewSet( diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index b3d42bef01..8a5aeb6e4c 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -1,4 +1,5 @@ import enum +from datetime import date from dateutil.relativedelta import relativedelta from django.contrib.postgres.aggregates import ArrayAgg @@ -6,6 +7,7 @@ from django.db import models from django.db.models import Case, F, Func, JSONField, Value, When from django.db.models.functions import Coalesce, Now +from django.template.defaultfilters import pluralize from django.utils import timezone from django.utils.translation import gettext_lazy as _ from simple_history.models import HistoricalRecords @@ -481,10 +483,29 @@ def save(self, *args, **kwargs) -> None: self._alias_recovery_to_recovered() super().save(*args, **kwargs) - def get_age(self) -> int: - start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date() + def get_age(self) -> str: + start = self.date_of_birth or date(self.year_of_birth, 1, 1) end = (self.death_datetime or timezone.now()).date() - return relativedelta(end, start).years + + delta = relativedelta(end, start) + + if delta.years > 0: + year_str = f"{delta.years} year{pluralize(delta.years)}" + return f"{year_str}" + + elif delta.months > 0: + month_str = f"{delta.months} month{pluralize(delta.months)}" + day_str = ( + f" {delta.days} day{pluralize(delta.days)}" if delta.days > 0 else "" + ) + return f"{month_str}{day_str}" + + elif delta.days > 0: + day_str = f"{delta.days} day{pluralize(delta.days)}" + return day_str + + else: + return "0 days" def annotate_diagnosis_ids(*args, **kwargs): return ArrayAgg( diff --git a/care/facility/templatetags/data_formatting_tags.py b/care/facility/templatetags/data_formatting_tags.py new file mode 100644 index 0000000000..18cc43c545 --- /dev/null +++ b/care/facility/templatetags/data_formatting_tags.py @@ -0,0 +1,35 @@ +from django import template + +register = template.Library() + + +@register.filter(name="format_empty_data") +def format_empty_data(data): + if data is None or data == "" or data == 0.0 or data == []: + return "N/A" + + return data + + +@register.filter(name="format_to_sentence_case") +def format_to_sentence_case(data): + if data is None: + return + + def convert_to_sentence_case(s): + if s == "ICU": + return "ICU" + s = s.lower() + s = s.replace("_", " ") + return s.capitalize() + + if isinstance(data, str): + items = data.split(", ") + converted_items = [convert_to_sentence_case(item) for item in items] + return ", ".join(converted_items) + + elif isinstance(data, (list, tuple)): + converted_items = [convert_to_sentence_case(item) for item in data] + return ", ".join(converted_items) + + return data diff --git a/care/facility/templatetags/prescription_tags.py b/care/facility/templatetags/prescription_tags.py new file mode 100644 index 0000000000..2f1b2ecee8 --- /dev/null +++ b/care/facility/templatetags/prescription_tags.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + + +@register.filter(name="format_prescription") +def format_prescription(prescription): + if prescription.dosage_type == "TITRATED": + return f"{prescription.medicine_name}, titration from {prescription.base_dosage} to {prescription.target_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." + if prescription.dosage_type == "PRN": + return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}" + else: + return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." diff --git a/care/facility/tests/sample_reports/sample1.png b/care/facility/tests/sample_reports/sample1.png new file mode 100644 index 0000000000..7cb6ce14b9 Binary files /dev/null and b/care/facility/tests/sample_reports/sample1.png differ diff --git a/care/facility/tests/sample_reports/sample2.png b/care/facility/tests/sample_reports/sample2.png new file mode 100644 index 0000000000..e46ee40001 Binary files /dev/null and b/care/facility/tests/sample_reports/sample2.png differ diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py new file mode 100644 index 0000000000..6754c0c43a --- /dev/null +++ b/care/facility/tests/test_pdf_generation.py @@ -0,0 +1,230 @@ +import os +import subprocess +import tempfile +from datetime import date +from pathlib import Path + +from django.conf import settings +from django.template.loader import render_to_string +from django.test import TestCase +from PIL import Image +from rest_framework.test import APIClient + +from care.facility.models import ( + ConditionVerificationStatus, + ICD11Diagnosis, + PrescriptionDosageType, + PrescriptionType, +) +from care.facility.utils.reports import discharge_summary +from care.facility.utils.reports.discharge_summary import compile_typ +from care.utils.tests.test_utils import TestUtils + + +def compare_pngs(png_path1, png_path2): + with Image.open(png_path1) as img1, Image.open(png_path2) as img2: + if img1.mode != img2.mode: + return False + + if img1.size != img2.size: + return False + + img1_data = list(img1.getdata()) + img2_data = list(img2.getdata()) + + if img1_data == img2_data: + return True + else: + return False + + +def test_compile_typ(data): + sample_file_path = os.path.join( + os.getcwd(), "care", "facility", "tests", "sample_reports", "sample{n}.png" + ) + test_output_file_path = os.path.join( + os.getcwd(), "care", "facility", "tests", "sample_reports", "test_output{n}.png" + ) + try: + logo_path = ( + Path(settings.BASE_DIR) + / "staticfiles" + / "images" + / "logos" + / "black-logo.svg" + ) + data["logo_path"] = str(logo_path) + content = render_to_string( + "reports/patient_discharge_summary_pdf_template.typ", context=data + ) + subprocess.run( + ["typst", "compile", "-", test_output_file_path, "--format", "png"], + input=content.encode("utf-8"), + capture_output=True, + check=True, + cwd="/", + ) + + number_of_pngs_generated = 2 + # To be updated only if the number of sample png increase in future + + for i in range(1, number_of_pngs_generated + 1): + current_sample_file_path = sample_file_path + current_sample_file_path = str(current_sample_file_path).replace( + "{n}", str(i) + ) + + current_test_output_file_path = test_output_file_path + current_test_output_file_path = str(current_test_output_file_path).replace( + "{n}", str(i) + ) + + if not compare_pngs( + Path(current_sample_file_path), Path(current_test_output_file_path) + ): + return False + return True + except Exception: + return False + finally: + count = 1 + while True: + current_test_output_file_path = test_output_file_path + current_test_output_file_path = current_test_output_file_path.replace( + "{n}", str(count) + ) + if Path(current_test_output_file_path).exists(): + os.remove(Path(current_test_output_file_path)) + else: + break + count += 1 + + +class TestTypstInstallation(TestCase): + def test_typst_installed(self): + try: + subprocess.run(["typst", "--version"], check=True) + typst_installed = True + except subprocess.CalledProcessError: + typst_installed = False + + self.assertTrue(typst_installed, "Typst is not installed or not accessible") + + +class TestGenerateDischargeSummaryPDF(TestCase, TestUtils): + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state(name="sample_state") + cls.district = cls.create_district(cls.state, name="sample_district") + cls.local_body = cls.create_local_body(cls.district, name="sample_local_body") + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body, name="_Sample_Facility" + ) + cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility) + cls.treating_physician = cls.create_user( + "test Doctor", + cls.district, + home_facility=cls.facility, + first_name="Doctor", + last_name="Tester", + user_type=15, + ) + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.consultation = cls.create_consultation( + cls.patient, + cls.facility, + patient_no="123456", + doctor=cls.treating_physician, + height=178, + weight=80, + suggestion="A", + ) + cls.create_patient_sample(cls.patient, cls.consultation, cls.facility, cls.user) + cls.create_policy(patient=cls.patient, user=cls.user) + cls.create_encounter_symptom(cls.consultation, cls.user) + cls.patient_investigation_group = cls.create_patient_investigation_group() + cls.patient_investigation = cls.create_patient_investigation( + cls.patient_investigation_group + ) + cls.patient_investigation_session = cls.create_patient_investigation_session( + cls.user + ) + cls.create_investigation_value( + cls.patient_investigation, + cls.consultation, + cls.patient_investigation_session, + cls.patient_investigation_group, + ) + cls.create_disease(cls.patient) + cls.create_prescription(cls.consultation, cls.user) + cls.create_prescription( + cls.consultation, cls.user, dosage_type=PrescriptionDosageType.TITRATED + ) + cls.create_prescription( + cls.consultation, cls.user, dosage_type=PrescriptionDosageType.PRN + ) + cls.create_prescription( + cls.consultation, cls.user, prescription_type=PrescriptionType.DISCHARGE + ) + cls.create_prescription( + cls.consultation, + cls.user, + prescription_type=PrescriptionType.DISCHARGE, + dosage_type=PrescriptionDosageType.TITRATED, + ) + cls.create_prescription( + cls.consultation, + cls.user, + prescription_type=PrescriptionType.DISCHARGE, + dosage_type=PrescriptionDosageType.PRN, + ) + cls.create_consultation_diagnosis( + cls.consultation, + ICD11Diagnosis.objects.filter( + label="SG31 Conception vessel pattern (TM1)" + ).first(), + verification_status=ConditionVerificationStatus.CONFIRMED, + ) + cls.create_consultation_diagnosis( + cls.consultation, + ICD11Diagnosis.objects.filter( + label="SG2B Liver meridian pattern (TM1)" + ).first(), + verification_status=ConditionVerificationStatus.DIFFERENTIAL, + ) + cls.create_consultation_diagnosis( + cls.consultation, + ICD11Diagnosis.objects.filter( + label="SG29 Triple energizer meridian pattern (TM1)" + ).first(), + verification_status=ConditionVerificationStatus.PROVISIONAL, + ) + cls.create_consultation_diagnosis( + cls.consultation, + ICD11Diagnosis.objects.filter( + label="SG60 Early yang stage pattern (TM1)" + ).first(), + verification_status=ConditionVerificationStatus.UNCONFIRMED, + ) + + def setUp(self) -> None: + self.client = APIClient() + + def test_pdf_generation_success(self): + test_data = {"consultation": self.consultation} + + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as file: + compile_typ(file.name, test_data) + + self.assertTrue(os.path.exists(file.name)) + self.assertGreater(os.path.getsize(file.name), 0) + + def test_pdf_generation(self): + data = discharge_summary.get_discharge_summary_data(self.consultation) + data["date"] = date(2020, 1, 1) + + # This sorting is test's specific and done in order to keep the values in order + self.assertTrue(test_compile_typ(data)) diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index acec25ea20..b9fb47d077 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -1,18 +1,20 @@ import logging +import subprocess import tempfile from collections.abc import Iterable +from pathlib import Path from uuid import uuid4 from django.conf import settings from django.core.cache import cache from django.core.mail import EmailMessage -from django.db.models import Q +from django.db.models import Case, IntegerField, Q, Value, When from django.template.loader import render_to_string from django.utils import timezone -from hardcopy import bytestring_to_pdf from care.facility.models import ( - DailyRound, + BedType, + ConsultationBed, Disease, EncounterSymptom, InvestigationValue, @@ -28,7 +30,7 @@ ACTIVE_CONDITION_VERIFICATION_STATUSES, ConditionVerificationStatus, ) -from care.facility.static_data.icd11 import get_icd11_diagnoses_objects_by_ids +from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id from care.hcx.models.policy import Policy logger = logging.getLogger(__name__) @@ -65,13 +67,19 @@ def get_diagnoses_data(consultation: PatientConsultation): ) ) - # retrieve diagnosis objects from in-memory table - diagnoses = get_icd11_diagnoses_objects_by_ids([entry[0] for entry in entries]) - + # retrieve diagnosis objects + diagnoses = [] + for entry in entries: + diagnose = get_icd11_diagnosis_object_by_id(entry[0]) + if diagnose: + diagnoses.append(diagnose) principal, unconfirmed, provisional, differential, confirmed = [], [], [], [], [] for diagnosis, record in zip(diagnoses, entries): _, verification_status, is_principal = record + + diagnosis.verification_status = verification_status + if is_principal: principal.append(diagnosis) if verification_status == ConditionVerificationStatus.UNCONFIRMED: @@ -92,40 +100,61 @@ def get_diagnoses_data(consultation: PatientConsultation): } +def format_duration(duration): + if not duration: + return "" + + days = duration.days + if days > 0: + return f"{days} days" + hours, remainder = divmod(duration.seconds, 3600) + minutes, _ = divmod(remainder, 60) + return f"{hours:02}:{minutes:02}" + + def get_discharge_summary_data(consultation: PatientConsultation): logger.info(f"fetching discharge summary data for {consultation.external_id}") samples = PatientSample.objects.filter( patient=consultation.patient, consultation=consultation ) hcx = Policy.objects.filter(patient=consultation.patient) - daily_rounds = DailyRound.objects.filter(consultation=consultation) - symptoms = EncounterSymptom.objects.filter(consultation=consultation).exclude( - clinical_impression_status=ClinicalImpressionStatus.ENTERED_IN_ERROR - ) + symptoms = EncounterSymptom.objects.filter( + consultation=consultation, onset_date__lt=consultation.encounter_date + ).exclude(clinical_impression_status=ClinicalImpressionStatus.ENTERED_IN_ERROR) diagnoses = get_diagnoses_data(consultation) investigations = InvestigationValue.objects.filter( Q(consultation=consultation.id) & (Q(value__isnull=False) | Q(notes__isnull=False)) ) medical_history = Disease.objects.filter(patient=consultation.patient) - prescriptions = Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.REGULAR.value, - ).exclude(dosage_type=PrescriptionDosageType.PRN.value) - prn_prescriptions = Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.REGULAR.value, - dosage_type=PrescriptionDosageType.PRN.value, + prescriptions = ( + Prescription.objects.filter( + consultation=consultation, prescription_type=PrescriptionType.REGULAR.value + ) + .annotate( + order_priority=Case( + When(dosage_type=PrescriptionDosageType.PRN.value, then=Value(2)), + When(dosage_type=PrescriptionDosageType.TITRATED.value, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ) + .order_by("order_priority", "id") ) - discharge_prescriptions = Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.DISCHARGE.value, - ).exclude(dosage_type=PrescriptionDosageType.PRN.value) - - discharge_prn_prescriptions = Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.DISCHARGE.value, - dosage_type=PrescriptionDosageType.PRN.value, + discharge_prescriptions = ( + Prescription.objects.filter( + consultation=consultation, + prescription_type=PrescriptionType.DISCHARGE.value, + ) + .annotate( + order_priority=Case( + When(dosage_type=PrescriptionDosageType.PRN.value, then=Value(2)), + When(dosage_type=PrescriptionDosageType.TITRATED.value, then=Value(1)), + default=Value(0), + output_field=IntegerField(), + ) + ) + .order_by("order_priority", "id") ) files = FileUpload.objects.filter( associating_id=consultation.id, @@ -133,47 +162,91 @@ def get_discharge_summary_data(consultation: PatientConsultation): upload_completed=True, is_archived=False, ) + admitted_to = set() + if ConsultationBed.objects.filter(consultation=consultation).exists(): + for bed in ConsultationBed.objects.filter(consultation=consultation).order_by( + "-created_date" + ): + admitted_to.add(BedType(bed.bed.bed_type).name) + admitted_to = list(admitted_to) + if not admitted_to: + admitted_to = None + + admission_duration = ( + format_duration(consultation.discharge_date - consultation.encounter_date) + if consultation.discharge_date + else None + ) return { "patient": consultation.patient, "samples": samples, "hcx": hcx, "symptoms": symptoms, - "principal_diagnoses": diagnoses["principal"], - "unconfirmed_diagnoses": diagnoses["unconfirmed"], - "provisional_diagnoses": diagnoses["provisional"], - "differential_diagnoses": diagnoses["differential"], - "confirmed_diagnoses": diagnoses["confirmed"], + "admitted_to": admitted_to, + "admission_duration": admission_duration, + "diagnoses": diagnoses["confirmed"] + + diagnoses["provisional"] + + diagnoses["unconfirmed"] + + diagnoses["differential"], + "primary_diagnoses": diagnoses["principal"], "consultation": consultation, "prescriptions": prescriptions, - "prn_prescriptions": prn_prescriptions, "discharge_prescriptions": discharge_prescriptions, - "discharge_prn_prescriptions": discharge_prn_prescriptions, - "dailyrounds": daily_rounds, "medical_history": medical_history, "investigations": investigations, "files": files, } -def generate_discharge_summary_pdf(data, file): - logger.info( - f"Generating Discharge Summary html for {data['consultation'].external_id}" - ) - html_string = render_to_string("reports/patient_discharge_summary_pdf.html", data) +def compile_typ(output_file, data): + try: + logo_path = ( + Path(settings.BASE_DIR) + / "staticfiles" + / "images" + / "logos" + / "black-logo.svg" + ) + + data["logo_path"] = str(logo_path) + + content = render_to_string( + "reports/patient_discharge_summary_pdf_template.typ", context=data + ) + + subprocess.run( + [ + "typst", + "compile", + "-", + str(output_file), + ], + input=content.encode("utf-8"), + capture_output=True, + check=True, + cwd="/", + ) + logging.info( + f"Successfully Compiled Summary pdf for {data['consultation'].external_id}" + ) + return True + + except subprocess.CalledProcessError as e: + logging.error( + f"Error compiling summary pdf for {data['consultation'].external_id}: {e.stderr.decode('utf-8')}" + ) + return False + + +def generate_discharge_summary_pdf(data, file): logger.info( f"Generating Discharge Summary pdf for {data['consultation'].external_id}" ) - bytestring_to_pdf( - html_string.encode(), - file, - **{ - "no-margins": None, - "disable-gpu": None, - "disable-dev-shm-usage": False, - "window-size": "2480,3508", - }, + compile_typ(output_file=file.name, data=data) + logger.info( + f"Successfully Generated Discharge Summary pdf for {data['consultation'].external_id}" ) diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html deleted file mode 100644 index 2a2125c788..0000000000 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ /dev/null @@ -1,1065 +0,0 @@ -{% load filters static %} - - - - - - - - - - - - -
-
-
- {{patient.facility.name}} -
-
-
-

- Patient Discharge Summary -

-

- Created on {{date}} -

-
- -
-
-
-

- Full name: {{patient.name}} -

-

- Gender: {{patient.get_gender_display}} -

-

- Age: {{patient.get_age}} -

- {% if patient.date_of_birth %} -

- Date of Birth: {{patient.date_of_birth}} -

- {% else %} -

- Year of Birth: {{patient.year_of_birth}} -

- {% endif %} -

- Blood Group: {{patient.blood_group}} -

-

- Phone Number: {{patient.phone_number}} -

-

- Address: {{patient.address}} -

-

- Ration Card Category: {{patient.get_ration_card_category_display}} -

-
-
- -
-

- Admission Details -

-
-
-
-

- Route to Facility: - {{consultation.get_route_to_facility_display|field_name_to_label}} -

-

- Decision after consultation: - {{consultation.get_suggestion_display|field_name_to_label}} -

- {% if consultation.icu_admission_date %} -

- ICU Admission Date & Time: - {{consultation.icu_admission_date}} -

- {% endif %} - {% if consultation.suggestion == 'A' %} -

- Date of addmission: - {{consultation.encounter_date}} -

- {% elif consultation.suggestion == 'R' %} -

- Referred to: - {{consultation.referred_to.name}} -

- {% elif consultation.suggestion == 'DD' %} -

- Cause of death: - {{consultation.discharge_notes}} -

-

- Date and time of death: - {{consultation.death_datetime}} -

-

- Death Confirmed by: - {{consultation.death_confirmed_by}} -

- {% endif %} -
-

- - {% if consultation.suggestion == 'A' %} - IP No: - {% else %} - OP No: - {% endif %} - {{consultation.patient_no}} -

-

- Weight: - {{consultation.weight}} kg -

-

- Height: - {{consultation.height}} cm -

-
-
- - {% if hcx %} -

- Health insurance details: -

- -
- - - - - - - - - - - {% for policy in hcx %} - - - - - - - {% endfor %} - -
- Insurer Name - - Issuer ID - - Member ID - - Policy ID -
- {{policy.insurer_name}} - - {{policy.insurer_id}} - - {{policy.subscriber_id}} - - {{policy.policy_id}} -
-
- {% endif %} - - {% if symptoms %} -

- Symptoms: -

-
- - - - - - - - - - {% for symptom in symptoms %} - - - - - - {% endfor %} - -
- Name - - Onset Date - - Cure Date -
- {% if symptom.symptom == 9 %} - {{symptom.other_symptom}} - {% else %} - {{symptom.get_symptom_display}} - {% endif %} - - {{symptom.onset_date.date}} - - {{symptom.cure_date.date}} -
-
- {% endif %} - - {% if principal_diagnosis %} -

- Principal Diagnosis (as per ICD-11 recommended by WHO): -

-
- - - - - - - - - {% for disease in principal_diagnosis %} - - - - - {% endfor %} - -
- ID - - Name -
- {{disease.id}} - - {{disease.label}} -
-
- {% endif %} - - {% if unconfirmed_diagnoses %} -

- Unconfirmed Diagnoses (as per ICD-11 recommended by WHO): -

-
- - - - - - - - - {% for disease in unconfirmed_diagnoses %} - - - - - {% endfor %} - -
- ID - - Name -
- {{disease.id}} - - {{disease.label}} -
-
- {% endif %} - - {% if provisional_diagnoses %} -

- Provisional Diagnosis (as per ICD-11 recommended by WHO): -

-
- - - - - - - - - {% for disease in provisional_diagnoses %} - - - - - {% endfor %} - -
- ID - - Name -
- {{disease.id}} - - {{disease.label}} -
-
- {% endif %} - - {% if differential_diagnoses %} -

- Differential Diagnoses (as per ICD-11 recommended by WHO): -

-
- - - - - - - - - {% for disease in differential_diagnoses %} - - - - - {% endfor %} - -
- ID - - Name -
- {{disease.id}} - - {{disease.label}} -
-
- {% endif %} - - {% if confirmed_diagnoses %} -

- Confirmed Diagnoses (as per ICD-11 recommended by WHO): -

-
- - - - - - - - - {% for disease in confirmed_diagnoses %} - - - - - {% endfor %} - -
- ID - - Name -
- {{disease.id}} - - {{disease.label}} -
-
- {% endif %} - - - {% if medical_history %} -

- Medication History: -

-
- - - - - - - - - {% for disease in medical_history %} - - - - - {% endfor %} - -
- Comorbidity - - Details -
- {{disease.get_disease_display}} - - {{disease.details}} -
-
- {% endif %} - -
-

- Health status at admission: -

-

- Present health condition: - {{patient.present_health}} -

-

- Ongoing Medication: - {{patient.ongoing_medication}} -

- {% if consultation.route_to_facility %} -

- History of present illness: - {{consultation.history_of_present_illness}} -

- {% endif %} -

- Examination details and Clinical conditions: - {{consultation.examination_details}} -

-

- Allergies: - {{patient.allergies}} -

-
-
- - {% if consultation.suggestion != 'DD' %} -
-

- Treatment Summary -

-
-
- {% if patient.disease_status == 2 %} -
-

- COVID Disease Status: - Positive -

- {% if patient.date_of_result %} -

- Test Result Date: - {{patient.date_of_result.date}} -

- {% endif %} -

- Vaccinated against COVID: - {% if patient.is_vaccinated %} - Yes - {% else %} - No - {% endif %} -

-
- {% endif %} - - {% if prescriptions %} -

- Prescription Medication: -

-
- - - - - - - - - - - - - {% for prescription in prescriptions %} - - - - - - - - - {% endfor %} - -
- Medicine - - Dosage - - Route - - Frequency - - Days - - Notes -
- {{ prescription.medicine_name }} - - {{ prescription.base_dosage }} - {% if prescription.dosage_type == 'TITRATED' %} - to {{ prescription.target_dosage }} -

Instruction on titration: {{ prescription.instruction_on_titration }}

- {% endif %} -
- {{ prescription.route }} - - {{ prescription.frequency }} - - {{ prescription.days }} - - {{ prescription.notes }} -
-
- {% endif %} - - {% if prn_prescriptions %} -

- PRN Prescription: -

-
- - - - - - - - - - - - - {% for prescription in prn_prescriptions %} - - - - - - - - - {% endfor %} - -
- Medicine - - Dosage - - Max Dosage - - Min Time btwn. 2 doses - - Route - - Indicator -
- {{ prescription.medicine_name }} - - {{ prescription.base_dosage }} - - {{ prescription.max_dosage }} - - {{ prescription.min_hours_between_doses }} Hrs. - - {{ prescription.route }} - - {{ prescription.indicator }} -
-
- {% endif %} - - {% if consultation.investigation %} -

- Investigations Suggestions: -

-
- - - - - - - - - - {% for investigation in consultation.investigation %} - - - - - - {% endfor %} - -
- Type - - Time - - Notes -
- {{investigation.type|join:", "}} - - {% if investigation.repetitive %} - every {{investigation.frequency}} - {% else %} - {{investigation.time|parse_datetime}} - {% endif %} - - {{investigation.notes}} -
-
- {% endif %} - - {% if consultation.procedure %} -

- Procedures: -

-
- - - - - - - - - - {% for procedure in consultation.procedure %} - - - - - - {% endfor %} - -
- Procedure - - Time - - Notes -
- {{procedure.procedure}} - - {% if procedure.repetitive %} - every {{procedure.frequency}} - {% else %} - {{procedure.time|parse_datetime}} - {% endif %} - - {{procedure.notes}} -
-
- {% endif %} - - {% if consultation.treatment_plan %} -

- Prescribed medication: -

-

- {{ consultation.treatment_plan }} -

- {% endif %} - - {% if consultation.consultation_notes %} -

- General Instructions (Advice): -

-

- {{consultation.consultation_notes}} -

- {% endif %} - - {% if consultation.special_instruction %} -

- Special Instructions: -

-

- {{consultation.special_instruction}} -

- {% endif %} - - - {% if samples %} -

- Lab Reports: -

-
- - - - - - - - - - - {% for sample in samples %} - - - - - - - {% endfor %} - -
- Requested on - - Sample type - - Label - - Result -
- {{sample.created_date}} - - {{sample.get_sample_type_display}} - - {{sample.icmr_label}} - - {{sample.get_result_display}} -
-
- {% endif %} - - {% if dailyrounds %} -

- Daily Rounds: -

- {% for daily_round in dailyrounds %} -
-
-

- {{daily_round.created_date}} -

-
-
-
-
- Category -
-
- {{daily_round.get_patient_category_display}} -
-
-
-
-
- Physical Examination Details -
-
- {{daily_round.physical_examination_info}} -
-
-
-
- Other Details -
-
- {{daily_round.other_details}} -
-
-
-
-
- {% endfor %} - {% endif %} - - {% if investigations %} -
-

- Investigation History -

-
-
- - - - - - - - - - - - {% for investigation in investigations %} - - - - - - - - {% endfor %} - -
- Group - - Name - - Result - - Range - - Date -
- {{investigation.investigation.groups.first}} - - {{investigation.investigation.name}} - - {% if investigation.value%} - {{investigation.value}} - {% else %} - {{investigation.notes}} - {% endif %} - - {% if investigation.investigation.min_value and investigation.investigation.max_value%} - {{investigation.investigation.min_value}} - {{investigation.investigation.max_value}} - {% else %} - - - {% endif %} - {% if investigation.investigation.unit %} - {{investigation.investigation.unit}} - {% endif %} - - {{investigation.created_date}} -
-
- {% endif %} -
- {% endif %} - -
-

- Discharge Summary -

-
-
-
-

- Discharge Date: {{consultation.discharge_date}} -

-

- Discharge Reason: {{consultation.get_discharge_reason_display}} -

-
- {% if consultation.new_discharge_reason == 1 %} - {% if discharge_prescriptions %} -

- Discharge Prescription Medication: -

-
- - - - - - - - - - - - - {% for prescription in discharge_prescriptions %} - - - - - - - - - {% endfor %} - -
- Medicine - - Dosage - - Route - - Frequency - - Days - - Notes -
- {{ prescription.medicine_name }} - - {{ prescription.base_dosage }} - - {{ prescription.route }} - - {{ prescription.frequency }} - - {{ prescription.days }} - - {{ prescription.notes }} -
-
- {% endif %} - - {% if discharge_prn_prescriptions %} -

- Discharge PRN Prescription: -

-
- - - - - - - - - - - - - {% for prescription in discharge_prn_prescriptions %} - - - - - - - - - {% endfor %} - -
- Medicine - - Dosage - - Max Dosage - - Min Time btwn. 2 doses - - Route - - Indicator -
- {{ prescription.medicine_name }} - - {{ prescription.base_dosage }} - - {{ prescription.max_dosage }} - - {{ prescription.min_hours_between_doses }} Hrs. - - {{ prescription.route }} - - {{ prescription.indicator }} -
-
- {% endif %} - {% elif consultation.new_discharge_reason == 2 %} - {% elif consultation.new_discharge_reason == 3 %} - {% elif consultation.new_discharge_reason == 4 %} - {% endif %} - -

- Discharge Notes: {{consultation.discharge_notes}} -

-
- - {% if files %} -
-

- Annexes -

-
-
-

- Uploaded Files: -

-
- - - - - - - - - {% for file in files %} - - - - - {% endfor %} - -
- Uploaded at - - Name -
- {{file.modified_date}} - - {{file.name}} -
-
-
- {% endif %} - -
-
- Treating Physician -
-
- {% if consultation.treating_physician %} - {{ consultation.treating_physician.first_name }} {{ consultation.treating_physician.last_name }} - {% else %} - - - {% endif %} -
-
- - - diff --git a/care/templates/reports/patient_discharge_summary_pdf_template.typ b/care/templates/reports/patient_discharge_summary_pdf_template.typ new file mode 100644 index 0000000000..b24c8402e4 --- /dev/null +++ b/care/templates/reports/patient_discharge_summary_pdf_template.typ @@ -0,0 +1,419 @@ +{% load static %} +{% load filters static %} +{% load prescription_tags %} +{% load data_formatting_tags %} + +#set page("a4",margin: 40pt) +#set text(font: "DejaVu Sans",size: 10pt,hyphenate: true) +#let mygray = luma(100) + +#let frame(stroke) = (x, y) => ( + left: if x > 0 { 0pt } else { stroke }, + right: stroke, + top: if y < 2 { stroke } else { 0pt }, + bottom: stroke, +) + +#set table( + fill: (_, y) => if calc.odd(y) { rgb("EAF2F5") }, + stroke: frame(rgb("21222C")), +) + +#let facility_name="{{patient.facility.name}}" + +#align(center, text(24pt,weight: "bold")[= #facility_name]) + +#line(length: 100%, stroke: mygray) + +#grid( + columns: (auto, 1fr), + row-gutter: 1em, + align: (left, right), + {% if consultation.suggestion == "A" %} + text(size: 15pt)[= Patient Discharge Summary], + {% else %} + text(size: 15pt)[= Patient Summary], + {% endif %} + grid.cell(align: right, rowspan: 2)[#scale(x:90%, y:90%, reflow: true)[#image("{{ logo_path }}")]], + [#text(fill: mygray, weight: 500)[*Created on {{date}}*]] +) + +#line(length: 100%, stroke: mygray) + +#show grid.cell.where(x: 0): set text(fill: mygray,weight: "bold") +#show grid.cell.where(x: 2): set text(fill: mygray,weight: "bold") + +#grid( + columns: (1fr, 1fr, 1fr, 1fr), + row-gutter: 1.5em, + [Full name:], "{{patient.name}}", + [Gender:], "{{patient.get_gender_display }}", + [Age:], "{{patient.get_age }}", + [Blood Group:], "{{patient.blood_group }}", + [Phone Number:], "{{patient.phone_number }}", + [Ration Card Category:], "{{patient.get_ration_card_category_display|format_empty_data }}", + [Address:], grid.cell(colspan: 3, "{{patient.address }}"), +) + +#line(length: 100%, stroke: mygray) + +{% if consultation.suggestion == "A" %} +#align(left, text(18pt)[== Admission Details]) +{% else %} +#align(left, text(18pt)[== Patient Details]) +{% endif %} +#text("") +#grid( + columns: (1.1fr, 2fr), + row-gutter: 1.2em, + align: (left), + [Route to Facility:], "{{ consultation.get_route_to_facility_display | field_name_to_label }}", + {% if consultation.suggestion == "A" %} + [Admitted To:], "{{ admitted_to|format_to_sentence_case|format_empty_data }}", + [Duration of Admission:], "{{admission_duration|format_empty_data}}", + [Date of admission:], "{{ consultation.encounter_date }}", + [IP No:], "{{ consultation.patient_no }}", + [Weight:], + {% if consultation.weight == 0.0 %} + "N/A" + {% else %} + "{{ consultation.weight }} kg" + {% endif %}, + [Height:], + {% if consultation.height == 0.0 %} + "N/A" + {% else %} + "{{ consultation.height }} cm" + {% endif %}, + [Diagnosis at admission:],[#stack( + dir: ttb, + spacing: 10pt, + {% for diagnose in diagnoses %} + "{{ diagnose.label }} ({{diagnose.verification_status }})", + {% endfor %} + )], + [Reason for admission:],[#stack( + dir: ttb, + spacing: 10pt, + {% if primary_diagnoses %} + {% for diagnose in primary_diagnoses %} + "{{ diagnose.label }}", + {% endfor %} + {% else %} + "N/A" + {% endif %} + )], + [Symptoms at admission], [#stack( + dir: ttb, + spacing: 10pt, + {% if symptoms %} + {% for symptom in symptoms %} + {% if symptom.symptom == 9 %} + "{{ symptom.other_symptom }}", + {% else %} + "{{ symptom.get_symptom_display }}", + {% endif %} + {% endfor %} + {% else %} + "Asymptomatic" + {% endif %} + )], + {% else %} + [OP No:], "{{ consultation.patient_no }}", + [Weight:], + {% if consultation.weight == 0.0 %} + "N/A" + {% else %} + "{{ consultation.weight }} kg" + {% endif %}, + [Height:], + {% if consultation.height == 0.0 %} + "N/A" + {% else %} + "{{ consultation.height }} cm" + {% endif %}, + [Diagnosis:],[#stack( + dir: ttb, + spacing: 10pt, + {% for diagnose in diagnoses %} + "{{ diagnose.label }} ({{diagnose.verification_status }})", + {% endfor %} + )], + [Principal Diagnosis:],[#stack( + dir: ttb, + spacing: 10pt, + {% if primary_diagnoses %} + {% for diagnose in primary_diagnoses %} + "{{ diagnose.label }}", + {% endfor %} + {% else %} + "N/A" + {% endif %} + )], + [Symptoms], [#stack( + dir: ttb, + spacing: 10pt, + {% if symptoms %} + {% for symptom in symptoms %} + {% if symptom.symptom == 9 %} + "{{ symptom.other_symptom }}", + {% else %} + "{{ symptom.get_symptom_display }}", + {% endif %} + {% endfor %} + {% else %} + "Asymptomatic" + {% endif %} + )], + {% endif %} + [Reported Allergies:], "{{ patient.allergies |format_empty_data }}", +) + +#text("\n") + +#align(center, [#line(length: 40%, stroke: mygray)]) + +{% if medical_history.0.get_disease_display != "NO" %} + +#align(left, text(14pt,weight: "bold",)[=== Medication History:]) + +#table( + columns: (1.5fr, 3.5fr), + inset: 10pt, + align: horizon, + table.header( + [*COMORBIDITY*], [*DETAILS*], + ), + {% for disease in medical_history %} + "{{disease.get_disease_display }}", "{{disease.details|format_empty_data }}", + {% endfor %} +) +#align(center, [#line(length: 40%, stroke: mygray,)]) +{% endif %} +{% if consultation.suggestion != 'DD' %} + {% if patient.disease_status == 2 or prescriptions or consultation.investigation or consultation.procedure or investigations or samples %} + #align(left, text(18pt,)[== Treatment Summary]) + #text("") + {% endif %} + + {% if patient.disease_status == 2 %} + #grid( + columns: (1fr, 1fr), + gutter: 1.4em, + align: (left), + [COVID Disease Status:], [Positive], + {% if patient.date_of_result %} + [Test Result Date:], "{{ patient.date_of_result.date }}", + {% endif %} + [Vaccinated against COVID:], [ + {% if patient.is_vaccinated %} + Yes + {% else %} + No + {% endif %} + ], + ) + {% endif %} + + {% if prescriptions %} + #align(left, text(14pt,weight: "bold",)[=== Medication Administered:]) + #table( + columns: (1fr,), + inset: 10pt, + align: horizon, + stroke: 1pt, + table.header( + align(center, text([*MEDICATION DETAILS*])) + ), + {% for prescription in prescriptions %} + [#grid( + columns: (0.5fr, 9.5fr), + row-gutter: 1.2em, + align: (left), + "{{ forloop.counter }}", + "{{ prescription|format_prescription }}", + )], + {% endfor %} + ) + + #align(center, [#line(length: 40%, stroke: mygray,)]) + {% endif %} + + {% if consultation.investigation %} + #align(left, text(14pt,weight: "bold",)[=== Investigations Conducted:]) + + #table( + columns: (1.5fr, 1fr, 1.5fr), + inset: 10pt, + align: horizon, + table.header( + [*TYPE*], [*TIME*], [*NOTES*] + ), + {% for investigation in consultation.investigation %} + "{{ investigation.type|join:", " }}", + "{% if investigation.repetitive %}every {{ investigation.frequency }}{% else %}{{ investigation.time|date:"DATETIME_FORMAT" }}{% endif %}", + "{{ investigation.notes |format_empty_data }}", + {% endfor %} + ) + + #align(center, [#line(length: 40%, stroke: mygray,)]) + {% endif %} + + {% if consultation.procedure %} + #align(left, text(14pt,weight: "bold",)[=== Procedures Conducted:]) + + #table( + columns: (1fr, 1fr, 2fr), + inset: 10pt, + align: horizon, + table.header( + [*PROCEDURE*], [*TIME*], [*NOTES*] + ), + {% for procedure in consultation.procedure %} + "{{ procedure.procedure }}", + "{% if procedure.repetitive %} every {{procedure.frequency }} {% else %} {{procedure.time|parse_datetime }} {% endif %}", + "{{ procedure.notes |format_empty_data }}", + {% endfor %} + ) + + #align(center, [#line(length: 40%, stroke: mygray,)]) + {% endif %} + + {% if samples %} + #align(left, text(14pt,weight: "bold",)[=== Lab Reports:]) + + #table( + columns: (1fr, 1fr, 1fr,1fr), + inset: 10pt, + align: horizon, + table.header( + [*REQUESTED ON*], [*SAMPLE TYPE*], [*LABEL*],[*RESULT*], + ), + {% for sample in samples %} + "{{ sample.created_date }}", "{{ sample.get_sample_type_display }}", "{{ sample.icmr_label }}","{{ sample.get_result_display }}", + {% endfor %} + ) + + #align(center, [#line(length: 40%, stroke: mygray,)]) + {% endif %} + + {% if investigations %} + #align(left, text(14pt,weight: "bold")[=== Investigation History:]) + #table( + columns: (1fr,), + inset: 10pt, + align: horizon, + stroke: 1pt, + table.header( + align(center, text([*INVESTIGATION DETAILS*])) + ), + {% for investigation in investigations %} + [#grid( + columns: (1fr, 3fr), + row-gutter: 1.2em, + align: (left), + [Group:], "{{ investigation.investigation.groups.first }}", + [Name:], "{{ investigation.investigation.name }}", + [Result:], [{% if investigation.value %}{{ investigation.value }}{% else %}{{ investigation.notes }}{% endif %}], + [Range:], [{% if investigation.investigation.min_value and investigation.investigation.max_value %} + {{ investigation.investigation.min_value }} - {{ investigation.investigation.max_value }} + {% else %} + - + {% endif %} + {% if investigation.investigation.unit %} + {{ investigation.investigation.unit }} + {% endif %} + ], + [Date:], "{{ investigation.created_date }}", + )], + {% endfor %} + ) + + #align(center, [#line(length: 40%, stroke: mygray)]) + {% endif %} + +{% endif %} + +#align(left, text(18pt,)[== Discharge Summary]) +#grid( + columns: (1fr,3fr), + row-gutter: 1.2em, + align: (left), + [Discharge Date:], "{{consultation.discharge_date|format_empty_data }}", + [Discharge Reason:], "{{consultation.get_new_discharge_reason_display|format_to_sentence_case|format_empty_data }}", + [Discharge Advice:], "{{consultation.discharge_notes|format_empty_data }}", +) + +{% if consultation.new_discharge_reason == 1 %} + {% if discharge_prescriptions %} + #align(left, text(14pt,weight: "bold",)[=== Discharge Prescription :]) + #table( + columns: (1fr,), + inset: 10pt, + align: horizon, + stroke: 1pt, + table.header( + align(center, text([*MEDICATION DETAILS*])) + ), + {% for prescription in discharge_prescriptions %} + [#grid( + columns: (0.5fr, 9.5fr), + row-gutter: 1.2em, + align: (left), + "{{ forloop.counter }}", + "{{ prescription|format_prescription }}", + )], + {% endfor %} + ) + {% endif %} + +{% elif consultation.new_discharge_reason == 2 %} +{% elif consultation.new_discharge_reason == 3 %} +{% elif consultation.new_discharge_reason == 4 %} +{% endif %} + +#text("") + +#align(right)[#text(12pt,fill: mygray)[*Treating Physician* :] #text(10pt,weight: "bold")[{% if consultation.treating_physician %} + {{ consultation.treating_physician.first_name }} {{ consultation.treating_physician.last_name }} +{% else %} + - +{% endif %}]] + +{% if hcx %} + #align(center, [#line(length: 40%, stroke: mygray,)]) + + #align(left, text(14pt,weight: "bold")[=== Health Insurance Details]) + + #table( + columns: (1fr, 1fr, 1fr, 1fr), + inset: 10pt, + align: horizon, + table.header( + [*INSURER NAME*], [*ISSUER ID*], [*MEMBER ID*], [*POLICY ID*], + ), + {% for policy in hcx %} + "{{policy.insurer_name|format_empty_data }}", "{{policy.insurer_id|format_empty_data }}", "{{policy.subscriber_id }}", "{{policy.policy_id }}", + {% endfor %} + ) +{% endif %} + +{% if files %} + #align(center, [#line(length: 40%, stroke: mygray,)]) + + #align(left, text(18pt,)[== Annexes]) + #align(left, text(14pt,weight: "bold",)[=== Uploaded Files:]) + + #table( + columns: (1fr, 1fr,), + inset: 10pt, + align: horizon, + table.header( + [*UPLOADED AT*], [*NAME*], + ), + {% for file in files %} + "{{file.modified_date }}", "{{file.name }}", + {% endfor %} + ) +{% endif %} +#line(length: 100%, stroke: mygray) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index dd5bd07477..3b340ff5b0 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -20,11 +20,18 @@ Ambulance, Disease, DiseaseStatusEnum, + EncounterSymptom, Facility, + InvestigationSession, + InvestigationValue, LocalBody, PatientConsultation, PatientExternalTest, + PatientInvestigation, + PatientInvestigationGroup, PatientRegistration, + PatientSample, + Prescription, ShiftingRequest, ShiftingRequestComment, User, @@ -44,6 +51,7 @@ PatientCodeStatusType, PatientConsent, ) +from care.hcx.models.policy import Policy from care.users.models import District, State fake = Faker() @@ -174,6 +182,8 @@ def create_user( username = f"user{now().timestamp()}" data = { + "first_name": "Foo", + "last_name": "Bar", "email": f"{username}@somedomain.com", "phone_number": "5554446667", "date_of_birth": date(1992, 4, 1), @@ -294,7 +304,9 @@ def get_patient_data(cls, district, state) -> dict: } @classmethod - def create_patient(cls, district: District, facility: Facility, **kwargs): + def create_patient( + cls, district: District, facility: Facility, **kwargs + ) -> PatientRegistration: patient_data = cls.get_patient_data(district, district.state).copy() medical_history = patient_data.pop("medical_history", []) @@ -513,6 +525,227 @@ def create_ambulance(cls, district: District, user: User, **kwargs) -> Ambulance data.update(**kwargs) return Ambulance.objects.create(**data) + @classmethod + def get_patient_sample_data(cls, patient, consultation, facility, user) -> dict: + return { + "patient": patient, + "consultation": consultation, + "sample_type": 1, + "sample_type_other": "sample sample type other field", + "has_sari": False, + "has_ari": False, + "doctor_name": "Sample Doctor", + "diagnosis": "Sample diagnosis", + "diff_diagnosis": "Sample different diagnosis", + "etiology_identified": "Sample etiology identified", + "is_atypical_presentation": False, + "atypical_presentation": "Sample atypical presentation", + "is_unusual_course": False, + "icmr_category": 10, + "icmr_label": "Sample ICMR", + "date_of_sample": make_aware(datetime(2020, 4, 1, 15, 30, 00)), + "date_of_result": make_aware(datetime(2020, 4, 5, 15, 30, 00)), + "testing_facility": facility, + "created_by": user, + "last_edited_by": user, + } + + @classmethod + def create_patient_sample( + cls, + patient: PatientRegistration, + consultation: PatientConsultation, + facility: Facility, + user: User, + **kwargs, + ) -> PatientSample: + data = cls.get_patient_sample_data(patient, consultation, facility, user) + data.update(**kwargs) + sample = PatientSample.objects.create(**data) + + # To make date static updating the object here for pdf testing + sample.created_date = make_aware(datetime(2020, 4, 1, 15, 30, 00)) + sample.save() + return sample + + @classmethod + def get_policy_data(cls, patient, user) -> dict: + return { + "patient": patient, + "subscriber_id": "sample_subscriber_id", + "policy_id": "sample_policy_id", + "insurer_id": "sample_insurer_id", + "insurer_name": "Sample Insurer", + "status": "active", + "priority": "normal", + "purpose": "discovery", + "outcome": "complete", + "error_text": "No errors", + "created_by": user, + "last_modified_by": user, + } + + @classmethod + def create_policy( + cls, patient: PatientRegistration, user: User, **kwargs + ) -> Policy: + data = cls.get_policy_data(patient, user) + data.update(**kwargs) + return Policy.objects.create(**data) + + @classmethod + def get_encounter_symptom_data(cls, consultation, user) -> dict: + return { + "symptom": 2, + "onset_date": make_aware(datetime(2020, 4, 1, 15, 30, 00)), + "cure_date": make_aware(datetime(2020, 5, 1, 15, 30, 00)), + "clinical_impression_status": 3, + "consultation": consultation, + "created_by": user, + "updated_by": user, + "is_migrated": False, + } + + @classmethod + def create_encounter_symptom( + cls, consultation: PatientConsultation, user: User, **kwargs + ) -> EncounterSymptom: + data = cls.get_encounter_symptom_data(consultation, user) + data.update(**kwargs) + return EncounterSymptom.objects.create(**data) + + @classmethod + def get_patient_investigation_data(cls) -> dict: + return { + "name": "Sample Investigation", + "unit": "mg/dL", + "ideal_value": "50-100", + "min_value": 50.0, + "max_value": 100.0, + "investigation_type": "Choice", + "choices": "Option1,Option2,Option3", + } + + @classmethod + def create_patient_investigation( + cls, patient_investigation_group: PatientInvestigationGroup, **kwargs + ) -> PatientInvestigation: + data = cls.get_patient_investigation_data() + data.update(**kwargs) + investigation = PatientInvestigation.objects.create(**data) + if "groups" in kwargs: + investigation.groups.set(kwargs["groups"]) + else: + investigation.groups.set([patient_investigation_group]) + return investigation + + @classmethod + def get_patient_investigation_group_data(cls) -> dict: + return { + "name": "Sample Investigation group", + } + + @classmethod + def create_patient_investigation_group(cls, **kwargs) -> PatientInvestigationGroup: + data = cls.get_patient_investigation_group_data() + data.update(**kwargs) + investigation_group = PatientInvestigationGroup.objects.create(**data) + return investigation_group + + @classmethod + def get_patient_investigation_session_data(cls, user) -> dict: + return { + "created_by": user, + } + + @classmethod + def create_patient_investigation_session( + cls, user: User, **kwargs + ) -> InvestigationSession: + data = cls.get_patient_investigation_session_data(user) + data.update(**kwargs) + investigation_session = InvestigationSession.objects.create(**data) + return investigation_session + + @classmethod + def get_investigation_value_data( + cls, investigation, consultation, session, group + ) -> dict: + return { + "investigation": investigation, + "group": group, + "value": 5.0, + "notes": "Sample notes", + "consultation": consultation, + "session": session, + } + + @classmethod + def create_investigation_value( + cls, + investigation: PatientInvestigation, + consultation: PatientConsultation, + session: InvestigationSession, + group: PatientInvestigationGroup, + **kwargs, + ) -> InvestigationValue: + data = cls.get_investigation_value_data( + investigation, consultation, session, group + ) + data.update(**kwargs) + investigation_value = InvestigationValue.objects.create(**data) + # To make created date static updating the object here for pdf testing + investigation_value.created_date = make_aware(datetime(2020, 4, 1, 15, 30, 00)) + investigation_value.save() + return investigation_value + + @classmethod + def get_disease_data(cls, patient) -> dict: + return { + "patient": patient, + "disease": 4, + "details": "Sample disease details", + } + + @classmethod + def create_disease(cls, patient: PatientRegistration, **kwargs) -> Disease: + data = cls.get_disease_data(patient) + data.update(**kwargs) + return Disease.objects.create(**data) + + @classmethod + def get_prescription_data(cls, consultation, user) -> dict: + return { + "consultation": consultation, + "prescription_type": "REGULAR", + "medicine": None, # TODO : Create medibase medicine + "medicine_old": "Sample old Medicine", + "route": "Oral", + "base_dosage": "500mg", + "dosage_type": "REGULAR", + "target_dosage": "1000mg", + "instruction_on_titration": "Sample Instruction for titration", + "frequency": "8th hourly", + "days": 7, + # prn fields + "indicator": "Sample indicator", + "max_dosage": "2000mg", + "min_hours_between_doses": 6, + "notes": "Take with food", + "prescribed_by": user, + "discontinued": False, + "discontinued_reason": "sample discontinued reason", + "discontinued_date": date(2020, 4, 1), + } + + @classmethod + def create_prescription( + cls, consultation: PatientConsultation, user: User, **kwargs + ) -> Prescription: + data = cls.get_prescription_data(consultation, user) + data.update(**kwargs) + return Prescription.objects.create(**data) + def get_list_representation(self, obj) -> dict: """ Returns the dict representation of the obj in list API diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 6cc595fa0a..13f6da537c 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -5,13 +5,30 @@ ENV PYTHONDONTWRITEBYTECODE 1 ENV PATH /venv/bin:$PATH +ARG TYPST_VERSION=0.11.0 RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev \ - libpq-dev gettext wget curl gnupg chromium git \ + libpq-dev gettext wget curl gnupg git \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* +# Download and install Typst for the correct architecture +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "amd64" ]; then \ + TYPST_ARCH="x86_64-unknown-linux-musl"; \ + elif [ "$ARCH" = "arm64" ]; then \ + TYPST_ARCH="aarch64-unknown-linux-musl"; \ + else \ + echo "Unsupported architecture: $ARCH"; \ + exit 1; \ + fi && \ + wget -O typst.tar.xz https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-${TYPST_ARCH}.tar.xz && \ + tar -xf typst.tar.xz && \ + mv typst-${TYPST_ARCH}/typst /usr/local/bin/typst && \ + chmod +x /usr/local/bin/typst && \ + rm -rf typst.tar.xz typst-${TYPST_ARCH} + # use pipenv to manage virtualenv RUN python -m venv /venv RUN pip install pipenv @@ -19,6 +36,7 @@ RUN pip install pipenv COPY Pipfile Pipfile.lock ./ RUN pipenv install --system --categories "packages dev-packages" + COPY . /app RUN python3 /app/install_plugins.py diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 0e89d39c17..f044d75c8e 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -10,16 +10,34 @@ ENV PYTHONDONTWRITEBYTECODE 1 FROM base as builder ARG BUILD_ENVIRONMENT=production +ARG TYPST_VERSION=0.11.0 ENV PATH /venv/bin:$PATH RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev libpq-dev git +# Download and install Typst for the correct architecture +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "amd64" ]; then \ + TYPST_ARCH="x86_64-unknown-linux-musl"; \ + elif [ "$ARCH" = "arm64" ]; then \ + TYPST_ARCH="aarch64-unknown-linux-musl"; \ + else \ + echo "Unsupported architecture: $ARCH"; \ + exit 1; \ + fi && \ + wget -O typst.tar.xz https://github.com/typst/typst/releases/download/v${TYPST_VERSION}/typst-${TYPST_ARCH}.tar.xz && \ + tar -xf typst.tar.xz && \ + mv typst-${TYPST_ARCH}/typst /usr/local/bin/typst && \ + chmod +x /usr/local/bin/typst && \ + rm -rf typst.tar.xz typst-${TYPST_ARCH} \ + # use pipenv to manage virtualenv RUN python -m venv /venv RUN pip install pipenv + COPY Pipfile Pipfile.lock ./ RUN pipenv sync --system --categories "packages" @@ -27,6 +45,7 @@ COPY . /app RUN python3 /app/install_plugins.py + # --- FROM base as runtime @@ -44,13 +63,17 @@ ENV PATH /venv/bin:$PATH WORKDIR ${APP_HOME} RUN apt-get update && apt-get install --no-install-recommends -y \ - libpq-dev gettext wget curl gnupg chromium \ + libpq-dev gettext wget curl gnupg \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* +# copy typst binary from builder stage +COPY --from=builder /usr/local/bin/typst /usr/local/bin/typst + # copy in Python environment COPY --from=builder /venv /venv + COPY --chmod=0755 ./scripts/*.sh ./ HEALTHCHECK \