diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py
index 214257bc02..52e9c5fea5 100644
--- a/care/abdm/utils/fhir.py
+++ b/care/abdm/utils/fhir.py
@@ -32,8 +32,9 @@
from fhir.resources.reference import Reference
from care.facility.models.file_upload import FileUpload
+from care.facility.models.icd11_diagnosis import REVERSE_CONDITION_VERIFICATION_STATUSES
from care.facility.models.patient_investigation import InvestigationValue
-from care.facility.static_data.icd11 import ICDDiseases
+from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id
class Fhir:
@@ -136,8 +137,8 @@ def _organization(self):
return self._organization_profile
- def _condition(self, diagnosis_id, provisional=False):
- diagnosis = ICDDiseases.by.id[diagnosis_id]
+ def _condition(self, diagnosis_id, verification_status):
+ diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id)
[code, label] = diagnosis.label.split(" ", 1)
condition_profile = Condition(
id=diagnosis_id,
@@ -158,8 +159,10 @@ def _condition(self, diagnosis_id, provisional=False):
coding=[
Coding(
system="http://terminology.hl7.org/CodeSystem/condition-ver-status",
- code="provisional" if provisional else "confirmed",
- display="Provisional" if provisional else "Confirmed",
+ code=verification_status,
+ display=REVERSE_CONDITION_VERIFICATION_STATUSES[
+ verification_status
+ ],
)
]
),
@@ -368,20 +371,15 @@ def _encounter(self, include_diagnosis=False):
"period": Period(start=period_start, end=period_end),
"diagnosis": list(
map(
- lambda diagnosis: EncounterDiagnosis(
+ lambda consultation_diagnosis: EncounterDiagnosis(
condition=self._reference(
- self._condition(diagnosis),
+ self._condition(
+ consultation_diagnosis.diagnosis_id,
+ consultation_diagnosis.verification_status,
+ ),
)
),
- self.consultation.icd11_diagnoses,
- )
- )
- + list(
- map(
- lambda diagnosis: EncounterDiagnosis(
- condition=self._reference(self._condition(diagnosis))
- ),
- self.consultation.icd11_provisional_diagnoses,
+ self.consultation.diagnoses.all(),
)
)
if include_diagnosis
diff --git a/care/facility/api/serializers/consultation_diagnosis.py b/care/facility/api/serializers/consultation_diagnosis.py
new file mode 100644
index 0000000000..62f5d80a6a
--- /dev/null
+++ b/care/facility/api/serializers/consultation_diagnosis.py
@@ -0,0 +1,127 @@
+from typing import Any
+
+from rest_framework import serializers
+
+from care.facility.models import (
+ INACTIVE_CONDITION_VERIFICATION_STATUSES,
+ ConsultationDiagnosis,
+)
+from care.facility.models.icd11_diagnosis import ICD11Diagnosis
+from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id
+from care.users.api.serializers.user import UserBaseMinimumSerializer
+
+
+class ConsultationCreateDiagnosisSerializer(serializers.ModelSerializer):
+ def validate_verification_status(self, value):
+ if value in INACTIVE_CONDITION_VERIFICATION_STATUSES:
+ raise serializers.ValidationError("Verification status not allowed")
+ return value
+
+ class Meta:
+ model = ConsultationDiagnosis
+ fields = ("diagnosis", "verification_status", "is_principal")
+
+
+class ConsultationDiagnosisSerializer(serializers.ModelSerializer):
+ id = serializers.UUIDField(source="external_id", read_only=True)
+ diagnosis = serializers.PrimaryKeyRelatedField(
+ queryset=ICD11Diagnosis.objects.all(), required=True, allow_null=False
+ )
+ diagnosis_object = serializers.SerializerMethodField()
+ created_by = UserBaseMinimumSerializer(read_only=True)
+
+ def get_diagnosis_object(self, obj):
+ return get_icd11_diagnosis_object_by_id(obj.diagnosis_id, as_dict=True)
+
+ class Meta:
+ model = ConsultationDiagnosis
+ exclude = (
+ "consultation",
+ "external_id",
+ "deleted",
+ )
+ read_only_fields = (
+ "created_by",
+ "created_date",
+ "modified_date",
+ "is_migrated",
+ )
+
+ def get_consultation_external_id(self):
+ return self.context["request"].parser_context["kwargs"][
+ "consultation_external_id"
+ ]
+
+ def validate_diagnosis(self, value):
+ if self.instance and value != self.instance.diagnosis:
+ raise serializers.ValidationError("Diagnosis cannot be changed")
+
+ if (
+ not self.instance
+ and ConsultationDiagnosis.objects.filter(
+ consultation__external_id=self.get_consultation_external_id(),
+ diagnosis=value,
+ ).exists()
+ ):
+ raise serializers.ValidationError(
+ "Diagnosis already exists for consultation"
+ )
+
+ return value
+
+ def validate_verification_status(self, value):
+ if not self.instance and value in INACTIVE_CONDITION_VERIFICATION_STATUSES:
+ raise serializers.ValidationError("Verification status not allowed")
+ return value
+
+ def validate_is_principal(self, value):
+ if not value:
+ return value
+
+ qs = ConsultationDiagnosis.objects.filter(
+ consultation__external_id=self.get_consultation_external_id(),
+ is_principal=True,
+ )
+
+ if self.instance:
+ qs = qs.exclude(id=self.instance.id)
+
+ if qs.exists():
+ raise serializers.ValidationError(
+ "Consultation already has a principal diagnosis. Unset the existing principal diagnosis first."
+ )
+
+ return value
+
+ def update(self, instance, validated_data):
+ if (
+ "verification_status" in validated_data
+ and validated_data["verification_status"]
+ in INACTIVE_CONDITION_VERIFICATION_STATUSES
+ ):
+ instance.is_principal = False
+ return super().update(instance, validated_data)
+
+ def validate(self, attrs: Any) -> Any:
+ validated = super().validate(attrs)
+
+ if (
+ "verification_status" in validated
+ and validated["verification_status"]
+ in INACTIVE_CONDITION_VERIFICATION_STATUSES
+ ):
+ validated["is_principal"] = False
+
+ if "is_principal" in validated and validated["is_principal"]:
+ verification_status = validated.get(
+ "verification_status",
+ self.instance.verification_status if self.instance else None,
+ )
+ if verification_status in INACTIVE_CONDITION_VERIFICATION_STATUSES:
+ raise serializers.ValidationError(
+ {
+ "is_principal": "Refuted/Entered in error diagnoses cannot be marked as Principal Diagnosis"
+ }
+ )
+
+ return validated
diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py
index 726facf027..7c58788329 100644
--- a/care/facility/api/serializers/patient_consultation.py
+++ b/care/facility/api/serializers/patient_consultation.py
@@ -8,6 +8,10 @@
from care.abdm.utils.api_call import AbdmGateway
from care.facility.api.serializers import TIMESTAMP_FIELDS
from care.facility.api.serializers.bed import ConsultationBedSerializer
+from care.facility.api.serializers.consultation_diagnosis import (
+ ConsultationCreateDiagnosisSerializer,
+ ConsultationDiagnosisSerializer,
+)
from care.facility.api.serializers.daily_round import DailyRoundSerializer
from care.facility.api.serializers.facility import FacilityBasicInfoSerializer
from care.facility.models import (
@@ -19,6 +23,10 @@
PrescriptionType,
)
from care.facility.models.bed import Bed, ConsultationBed
+from care.facility.models.icd11_diagnosis import (
+ ConditionVerificationStatus,
+ ConsultationDiagnosis,
+)
from care.facility.models.notification import Notification
from care.facility.models.patient_base import (
DISCHARGE_REASON_CHOICES,
@@ -26,7 +34,6 @@
SuggestionChoices,
)
from care.facility.models.patient_consultation import PatientConsultation
-from care.facility.static_data.icd11 import get_icd11_diagnoses_objects_by_ids
from care.users.api.serializers.user import (
UserAssignedSerializer,
UserBaseMinimumSerializer,
@@ -100,11 +107,13 @@ class PatientConsultationSerializer(serializers.ModelSerializer):
bed = ExternalIdSerializerField(queryset=Bed.objects.all(), required=False)
- icd11_diagnoses_object = serializers.SerializerMethodField(read_only=True)
-
- icd11_provisional_diagnoses_object = serializers.SerializerMethodField(
- read_only=True
+ create_diagnoses = ConsultationCreateDiagnosisSerializer(
+ many=True,
+ write_only=True,
+ required=False,
+ help_text="Bulk create diagnoses for the consultation upon creation",
)
+ diagnoses = ConsultationDiagnosisSerializer(many=True, read_only=True)
medico_legal_case = serializers.BooleanField(default=False, required=False)
@@ -122,14 +131,6 @@ def get_discharge_prn_prescription(self, consultation):
is_prn=True,
).values()
- def get_icd11_diagnoses_object(self, consultation):
- return get_icd11_diagnoses_objects_by_ids(consultation.icd11_diagnoses)
-
- def get_icd11_provisional_diagnoses_object(self, consultation):
- return get_icd11_diagnoses_objects_by_ids(
- consultation.icd11_provisional_diagnoses
- )
-
class Meta:
model = PatientConsultation
read_only_fields = TIMESTAMP_FIELDS + (
@@ -139,9 +140,16 @@ class Meta:
"created_by",
"kasp_enabled_date",
"is_readmission",
+ "deprecated_diagnosis",
"deprecated_verified_by",
)
- exclude = ("deleted", "external_id")
+ exclude = (
+ "deleted",
+ "external_id",
+ "deprecated_icd11_provisional_diagnoses",
+ "deprecated_icd11_diagnoses",
+ "deprecated_icd11_principal_diagnosis",
+ )
def validate_bed_number(self, bed_number):
try:
@@ -222,6 +230,7 @@ def update(self, instance, validated_data):
return consultation
def create(self, validated_data):
+ create_diagnosis = validated_data.pop("create_diagnoses")
action = -1
review_interval = -1
if "action" in validated_data:
@@ -283,6 +292,19 @@ def create(self, validated_data):
consultation.is_readmission = True
consultation.save()
+ ConsultationDiagnosis.objects.bulk_create(
+ [
+ ConsultationDiagnosis(
+ consultation=consultation,
+ diagnosis_id=obj["diagnosis"].id,
+ is_principal=obj["is_principal"],
+ verification_status=obj["verification_status"],
+ created_by=self.context["request"].user,
+ )
+ for obj in create_diagnosis
+ ]
+ )
+
if bed and consultation.suggestion == SuggestionChoices.A:
consultation_bed = ConsultationBed(
bed=bed,
@@ -332,6 +354,45 @@ def create(self, validated_data):
return consultation
+ def validate_create_diagnoses(self, value):
+ # Reject if create_diagnoses is present for edits
+ if self.instance and value:
+ raise ValidationError("Bulk create diagnoses is not allowed on update")
+
+ # Reject if no diagnoses are provided
+ if len(value) == 0:
+ raise ValidationError("Atleast one diagnosis is required")
+
+ # Reject if duplicate diagnoses are provided
+ if len(value) != len(set([obj["diagnosis"].id for obj in value])):
+ raise ValidationError("Duplicate diagnoses are not allowed")
+
+ principal_diagnosis, confirmed_diagnoses = None, []
+ for obj in value:
+ if obj["verification_status"] == ConditionVerificationStatus.CONFIRMED:
+ confirmed_diagnoses.append(obj)
+
+ # Reject if there are more than one principal diagnosis
+ if obj["is_principal"]:
+ if principal_diagnosis:
+ raise ValidationError(
+ "Only one diagnosis can be set as principal diagnosis"
+ )
+ principal_diagnosis = obj
+
+ # Reject if principal diagnosis is not one of confirmed diagnosis (if it is present)
+ if (
+ principal_diagnosis
+ and len(confirmed_diagnoses)
+ and principal_diagnosis["verification_status"]
+ != ConditionVerificationStatus.CONFIRMED
+ ):
+ raise ValidationError(
+ "Only confirmed diagnosis can be set as principal diagnosis if it is present"
+ )
+
+ return value
+
def validate(self, attrs):
validated = super().validate(attrs)
# TODO Add Bed Authorisation Validation
@@ -410,72 +471,9 @@ def validate(self, attrs):
]
}
)
- from care.facility.static_data.icd11 import ICDDiseases
- final_diagnosis = []
- provisional_diagnosis = []
-
- if "icd11_diagnoses" in validated:
- for diagnosis in validated["icd11_diagnoses"]:
- try:
- ICDDiseases.by.id[diagnosis]
- final_diagnosis.append(diagnosis)
- except BaseException:
- raise ValidationError(
- {
- "icd11_diagnoses": [
- f"{diagnosis} is not a valid ICD 11 Diagnosis ID"
- ]
- }
- )
-
- if "icd11_provisional_diagnoses" in validated:
- for diagnosis in validated["icd11_provisional_diagnoses"]:
- try:
- ICDDiseases.by.id[diagnosis]
- provisional_diagnosis.append(diagnosis)
- except BaseException:
- raise ValidationError(
- {
- "icd11_provisional_diagnoses": [
- f"{diagnosis} is not a valid ICD 11 Diagnosis ID"
- ]
- }
- )
-
- if (
- "icd11_principal_diagnosis" in validated
- and validated.get("suggestion") != SuggestionChoices.DD
- ):
- if len(final_diagnosis):
- if validated["icd11_principal_diagnosis"] not in final_diagnosis:
- raise ValidationError(
- {
- "icd11_principal_diagnosis": [
- "Principal Diagnosis must be one of the Final Diagnosis"
- ]
- }
- )
- elif len(provisional_diagnosis):
- if validated["icd11_principal_diagnosis"] not in provisional_diagnosis:
- raise ValidationError(
- {
- "icd11_principal_diagnosis": [
- "Principal Diagnosis must be one of the Provisional Diagnosis"
- ]
- }
- )
- else:
- raise ValidationError(
- {
- "icd11_diagnoses": [
- "Atleast one diagnosis is required for final diagnosis"
- ],
- "icd11_provisional_diagnoses": [
- "Atleast one diagnosis is required for provisional diagnosis"
- ],
- }
- )
+ if not self.instance and "create_diagnoses" not in validated:
+ raise ValidationError({"create_diagnoses": ["This field is required."]})
return validated
diff --git a/care/facility/api/viewsets/consultation_diagnosis.py b/care/facility/api/viewsets/consultation_diagnosis.py
new file mode 100644
index 0000000000..73d218bc3e
--- /dev/null
+++ b/care/facility/api/viewsets/consultation_diagnosis.py
@@ -0,0 +1,55 @@
+from django.shortcuts import get_object_or_404
+from django_filters import rest_framework as filters
+from dry_rest_permissions.generics import DRYPermissions
+from rest_framework import mixins
+from rest_framework.permissions import IsAuthenticated
+from rest_framework.viewsets import GenericViewSet
+
+from care.facility.api.serializers.consultation_diagnosis import (
+ ConsultationDiagnosisSerializer,
+)
+from care.facility.models import (
+ ConditionVerificationStatus,
+ ConsultationDiagnosis,
+ generate_choices,
+)
+from care.utils.filters.choicefilter import CareChoiceFilter
+from care.utils.queryset.consultation import get_consultation_queryset
+
+
+class ConsultationDiagnosisFilter(filters.FilterSet):
+ verification_status = CareChoiceFilter(
+ choices=generate_choices(ConditionVerificationStatus)
+ )
+
+
+class ConsultationDiagnosisViewSet(
+ mixins.CreateModelMixin,
+ mixins.ListModelMixin,
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ GenericViewSet,
+):
+ serializer_class = ConsultationDiagnosisSerializer
+ permission_classes = (IsAuthenticated, DRYPermissions)
+ queryset = (
+ ConsultationDiagnosis.objects.all()
+ .select_related("created_by")
+ .order_by("-created_date")
+ )
+ lookup_field = "external_id"
+
+ def get_consultation_obj(self):
+ return get_object_or_404(
+ get_consultation_queryset(self.request.user).filter(
+ external_id=self.kwargs["consultation_external_id"]
+ )
+ )
+
+ def get_queryset(self):
+ consultation = self.get_consultation_obj()
+ return self.queryset.filter(consultation_id=consultation.id)
+
+ def perform_create(self, serializer):
+ consultation = self.get_consultation_obj()
+ serializer.save(consultation=consultation, created_by=self.request.user)
diff --git a/care/facility/api/viewsets/icd.py b/care/facility/api/viewsets/icd.py
index 56561b9b70..d4de44d9f4 100644
--- a/care/facility/api/viewsets/icd.py
+++ b/care/facility/api/viewsets/icd.py
@@ -10,7 +10,9 @@ def serailize_data(icd11_object):
for object in icd11_object:
if type(object) == tuple:
object = object[0]
- result.append({"id": object.id, "label": object.label})
+ result.append(
+ {"id": object.id, "label": object.label, "chapter": object.chapter}
+ )
return result
diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py
index 6caaafaa42..a84979a9f5 100644
--- a/care/facility/api/viewsets/patient.py
+++ b/care/facility/api/viewsets/patient.py
@@ -409,8 +409,10 @@ def list(self, request, *args, **kwargs):
}
)
# End Date Limiting Validation
- queryset = self.filter_queryset(self.get_queryset()).values(
- *PatientRegistration.CSV_MAPPING.keys()
+ queryset = (
+ self.filter_queryset(self.get_queryset())
+ .annotate(**PatientRegistration.CSV_ANNOTATE_FIELDS)
+ .values(*PatientRegistration.CSV_MAPPING.keys())
)
return render_to_csv_response(
queryset,
diff --git a/care/facility/management/commands/load_icd11_diagnoses_data.py b/care/facility/management/commands/load_icd11_diagnoses_data.py
index 4ad383bba7..faa8ba6df9 100644
--- a/care/facility/management/commands/load_icd11_diagnoses_data.py
+++ b/care/facility/management/commands/load_icd11_diagnoses_data.py
@@ -37,7 +37,7 @@ class Command(BaseCommand):
"""
Management command to load ICD11 diagnoses to database. Not for production
use.
- Usage: python manage.py load_icd11_diagnoses
+ Usage: python manage.py load_icd11_diagnoses_data
"""
help = "Loads ICD11 diagnoses data to database"
@@ -143,7 +143,6 @@ def roots(item):
result["meta_hidden"] = mapped is None
return result
- ICD11Diagnosis.objects.all().delete()
ICD11Diagnosis.objects.bulk_create(
[
ICD11Diagnosis(
@@ -159,7 +158,8 @@ def roots(item):
**roots(obj),
)
for obj in self.data
- ]
+ ],
+ ignore_conflicts=True, # Voluntarily set to skip duplicates, so that we can run this command multiple times + existing relations are not affected
)
except Exception as e:
raise CommandError(e)
diff --git a/care/facility/migrations/0001_initial_squashed.py b/care/facility/migrations/0001_initial_squashed.py
index 0917e3e324..7ec400916a 100644
--- a/care/facility/migrations/0001_initial_squashed.py
+++ b/care/facility/migrations/0001_initial_squashed.py
@@ -2664,7 +2664,7 @@ class Migration(migrations.Migration):
],
bases=(
models.Model,
- care.facility.models.mixins.permissions.patient.PatientRelatedPermissionMixin,
+ care.facility.models.mixins.permissions.patient.ConsultationRelatedPermissionMixin,
),
),
migrations.CreateModel(
@@ -4550,7 +4550,7 @@ class Migration(migrations.Migration):
},
bases=(
models.Model,
- care.facility.models.mixins.permissions.patient.PatientRelatedPermissionMixin,
+ care.facility.models.mixins.permissions.patient.ConsultationRelatedPermissionMixin,
),
),
migrations.CreateModel(
diff --git a/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py b/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py
new file mode 100644
index 0000000000..d5f67b6f30
--- /dev/null
+++ b/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py
@@ -0,0 +1,191 @@
+# Generated by Django 4.2.5 on 2023-11-03 07:49
+
+import uuid
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+import care.facility.models.mixins.permissions.patient
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("facility", "0392_alter_dailyround_consciousness_level"),
+ ]
+
+ def populate_consultation_diagnosis(apps, schema_editor):
+ PatientConsultation = apps.get_model("facility", "PatientConsultation")
+ ConsultationDiagnosis = apps.get_model("facility", "ConsultationDiagnosis")
+
+ objects = []
+
+ for consultation in PatientConsultation.objects.all():
+ processed_diagnosis_ids = [] # to skip duplicates if any
+ principal_diagnosis_id = consultation.deprecated_icd11_principal_diagnosis
+
+ # confirmed diagnoses
+ for diagnosis_id in consultation.deprecated_icd11_diagnoses:
+ if diagnosis_id in processed_diagnosis_ids:
+ continue
+ processed_diagnosis_ids.append(diagnosis_id)
+ objects.append(
+ ConsultationDiagnosis(
+ is_migrated=True,
+ consultation=consultation,
+ diagnosis_id=diagnosis_id,
+ verification_status="confirmed",
+ is_principal=diagnosis_id == principal_diagnosis_id,
+ )
+ )
+
+ # provisional diagnoses
+ for diagnosis_id in consultation.deprecated_icd11_provisional_diagnoses:
+ if diagnosis_id in processed_diagnosis_ids:
+ continue
+ processed_diagnosis_ids.append(diagnosis_id)
+ objects.append(
+ ConsultationDiagnosis(
+ is_migrated=True,
+ consultation=consultation,
+ diagnosis_id=diagnosis_id,
+ verification_status="provisional",
+ is_principal=diagnosis_id == principal_diagnosis_id,
+ )
+ )
+
+ ConsultationDiagnosis.objects.bulk_create(objects, batch_size=2000)
+
+ operations = [
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="diagnosis",
+ new_name="deprecated_diagnosis",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="icd11_diagnoses",
+ new_name="deprecated_icd11_diagnoses",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="icd11_principal_diagnosis",
+ new_name="deprecated_icd11_principal_diagnosis",
+ ),
+ migrations.RenameField(
+ model_name="patientconsultation",
+ old_name="icd11_provisional_diagnoses",
+ new_name="deprecated_icd11_provisional_diagnoses",
+ ),
+ migrations.CreateModel(
+ name="ConsultationDiagnosis",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "external_id",
+ models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
+ ),
+ (
+ "created_date",
+ models.DateTimeField(auto_now_add=True, db_index=True, null=True),
+ ),
+ (
+ "modified_date",
+ models.DateTimeField(auto_now=True, db_index=True, null=True),
+ ),
+ ("deleted", models.BooleanField(db_index=True, default=False)),
+ (
+ "verification_status",
+ models.CharField(
+ choices=[
+ ("unconfirmed", "Unconfirmed"),
+ ("provisional", "Provisional"),
+ ("differential", "Differential"),
+ ("confirmed", "Confirmed"),
+ ("refuted", "Refuted"),
+ ("entered-in-error", "Entered in Error"),
+ ],
+ max_length=20,
+ ),
+ ),
+ ("is_principal", models.BooleanField(default=False)),
+ (
+ "is_migrated",
+ models.BooleanField(
+ default=False,
+ help_text="This field is to throw caution to data that was previously ported over",
+ ),
+ ),
+ (
+ "consultation",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="diagnoses",
+ to="facility.patientconsultation",
+ ),
+ ),
+ (
+ "created_by",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "diagnosis",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="facility.icd11diagnosis",
+ ),
+ ),
+ ],
+ bases=(
+ models.Model,
+ care.facility.models.mixins.permissions.patient.ConsultationRelatedPermissionMixin,
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="consultationdiagnosis",
+ constraint=models.UniqueConstraint(
+ fields=("consultation", "diagnosis"),
+ name="unique_diagnosis_per_consultation",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="consultationdiagnosis",
+ constraint=models.UniqueConstraint(
+ condition=models.Q(("is_principal", True)),
+ fields=("consultation", "is_principal"),
+ name="unique_principal_diagnosis",
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="consultationdiagnosis",
+ constraint=models.CheckConstraint(
+ check=models.Q(
+ ("is_principal", False),
+ models.Q(
+ ("verification_status__in", ["refuted", "entered-in-error"]),
+ _negated=True,
+ ),
+ _connector="OR",
+ ),
+ name="refuted_or_entered_in_error_diagnosis_cannot_be_principal",
+ ),
+ ),
+ migrations.RunPython(
+ populate_consultation_diagnosis, reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/care/facility/migrations_old/0353_auto_20230429_2026.py b/care/facility/migrations_old/0353_auto_20230429_2026.py
index 101e9a7d4f..010f93e357 100644
--- a/care/facility/migrations_old/0353_auto_20230429_2026.py
+++ b/care/facility/migrations_old/0353_auto_20230429_2026.py
@@ -127,7 +127,7 @@ class Migration(migrations.Migration):
},
bases=(
models.Model,
- care.facility.models.mixins.permissions.patient.PatientRelatedPermissionMixin,
+ care.facility.models.mixins.permissions.patient.ConsultationRelatedPermissionMixin,
),
),
migrations.CreateModel(
diff --git a/care/facility/models/icd11_diagnosis.py b/care/facility/models/icd11_diagnosis.py
index 959340fe12..35ba3a0a4a 100644
--- a/care/facility/models/icd11_diagnosis.py
+++ b/care/facility/models/icd11_diagnosis.py
@@ -1,4 +1,11 @@
from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from care.facility.models.mixins.permissions.patient import (
+ ConsultationRelatedPermissionMixin,
+)
+from care.facility.models.patient_base import reverse_choices
+from care.utils.models.base import BaseModel
class ICD11ClassKind(models.TextChoices):
@@ -31,3 +38,74 @@ class ICD11Diagnosis(models.Model):
def __str__(self) -> str:
return self.label
+
+
+class ConditionVerificationStatus(models.TextChoices):
+ """
+ See: https://www.hl7.org/fhir/valueset-condition-ver-status.html
+ """
+
+ UNCONFIRMED = "unconfirmed", _("Unconfirmed")
+ PROVISIONAL = "provisional", _("Provisional")
+ DIFFERENTIAL = "differential", _("Differential")
+ CONFIRMED = "confirmed", _("Confirmed")
+ REFUTED = "refuted", _("Refuted")
+ ENTERED_IN_ERROR = "entered-in-error", _("Entered in Error")
+
+
+INACTIVE_CONDITION_VERIFICATION_STATUSES = [
+ ConditionVerificationStatus.REFUTED,
+ ConditionVerificationStatus.ENTERED_IN_ERROR,
+] # Theses statuses are not allowed to be selected during create or can't be a principal diagnosis
+
+ACTIVE_CONDITION_VERIFICATION_STATUSES = [
+ status
+ for status in ConditionVerificationStatus
+ if status not in INACTIVE_CONDITION_VERIFICATION_STATUSES
+] # These statuses are allowed to be selected during create and these diagnosis can only be a principal diagnosis
+
+REVERSE_CONDITION_VERIFICATION_STATUSES = reverse_choices(ConditionVerificationStatus)
+
+
+class ConsultationDiagnosis(BaseModel, ConsultationRelatedPermissionMixin):
+ consultation = models.ForeignKey(
+ "PatientConsultation", on_delete=models.CASCADE, related_name="diagnoses"
+ )
+ diagnosis = models.ForeignKey(ICD11Diagnosis, on_delete=models.PROTECT)
+ verification_status = models.CharField(
+ max_length=20, choices=ConditionVerificationStatus.choices
+ )
+ is_principal = models.BooleanField(default=False)
+
+ created_by = models.ForeignKey(
+ "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+"
+ )
+ is_migrated = models.BooleanField(
+ default=False,
+ help_text="This field is to throw caution to data that was previously ported over",
+ )
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=["consultation", "diagnosis"],
+ name="unique_diagnosis_per_consultation",
+ ),
+ # A consultation can have only one principal diagnosis
+ # in other words: unique together (consultation, is_principal) where is_principal is true
+ models.UniqueConstraint(
+ fields=["consultation", "is_principal"],
+ condition=models.Q(is_principal=True),
+ name="unique_principal_diagnosis",
+ ),
+ # Diagnosis cannot be principal if verification status is one of refuted/entered-in-error.
+ models.CheckConstraint(
+ check=(
+ models.Q(is_principal=False)
+ | ~models.Q(
+ verification_status__in=INACTIVE_CONDITION_VERIFICATION_STATUSES
+ )
+ ),
+ name="refuted_or_entered_in_error_diagnosis_cannot_be_principal",
+ ),
+ ]
diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py
index f014b4f2a4..c81685392b 100644
--- a/care/facility/models/mixins/permissions/patient.py
+++ b/care/facility/models/mixins/permissions/patient.py
@@ -150,7 +150,10 @@ def has_object_transfer_permission(self, request):
)
-class PatientRelatedPermissionMixin(BasePermissionMixin):
+class ConsultationRelatedPermissionMixin(BasePermissionMixin):
+ def get_related_consultation(self):
+ return self.consultation
+
@staticmethod
def has_write_permission(request):
if (
@@ -159,38 +162,11 @@ def has_write_permission(request):
or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"]
):
return False
- return (
- request.user.is_superuser
- or request.user.verified
- and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"]
- )
+ return True
def has_object_read_permission(self, request):
- return (
- request.user.is_superuser
- or (
- self.patient.facility
- and request.user in self.patient.facility.users.all()
- )
- or (
- self.assigned_to == request.user
- or request.user == self.patient.assigned_to
- )
- or (
- request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]
- and (
- self.patient.facility
- and request.user.district == self.patient.facility.district
- )
- )
- or (
- request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]
- and (
- self.patient.facility
- and request.user.state == self.patient.facility.state
- )
- )
- )
+ # This is because, `get_queryset` for related models already filters by consultation.
+ return True
def has_object_update_permission(self, request):
if (
@@ -199,28 +175,4 @@ def has_object_update_permission(self, request):
or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"]
):
return False
- return (
- request.user.is_superuser
- or (
- self.patient.facility
- and self.patient.facility == request.user.home_facility
- )
- or (
- self.assigned_to == request.user
- or request.user == self.patient.assigned_to
- )
- or (
- request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]
- and (
- self.patient.facility
- and request.user.district == self.patient.facility.district
- )
- )
- or (
- request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]
- and (
- self.patient.facility
- and request.user.state == self.patient.facility.state
- )
- )
- )
+ return True
diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py
index cd14e5fcfa..6778153505 100644
--- a/care/facility/models/patient.py
+++ b/care/facility/models/patient.py
@@ -1,6 +1,7 @@
import datetime
import enum
+from django.contrib.postgres.aggregates import ArrayAgg
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import JSONField
@@ -18,12 +19,13 @@
State,
Ward,
)
+from care.facility.models.icd11_diagnosis import ConditionVerificationStatus
from care.facility.models.mixins.permissions.facility import (
FacilityRelatedPermissionMixin,
)
from care.facility.models.mixins.permissions.patient import (
+ ConsultationRelatedPermissionMixin,
PatientPermissionMixin,
- PatientRelatedPermissionMixin,
)
from care.facility.models.patient_base import (
BLOOD_GROUP_CHOICES,
@@ -33,7 +35,7 @@
REVERSE_DISCHARGE_REASON_CHOICES,
)
from care.facility.models.patient_consultation import PatientConsultation
-from care.facility.static_data.icd11 import ICDDiseases
+from care.facility.static_data.icd11 import get_icd11_diagnoses_objects_by_ids
from care.users.models import GENDER_CHOICES, REVERSE_GENDER_CHOICES, User
from care.utils.models.base import BaseManager, BaseModel
from care.utils.models.validators import mobile_or_landline_number_validator
@@ -479,6 +481,31 @@ def save(self, *args, **kwargs) -> None:
self._alias_recovery_to_recovered()
super().save(*args, **kwargs)
+ def annotate_diagnosis_ids(*args, **kwargs):
+ return ArrayAgg(
+ "last_consultation__diagnoses__diagnosis_id",
+ filter=models.Q(*args, **kwargs),
+ )
+
+ CSV_ANNOTATE_FIELDS = {
+ # Principal Diagnoses
+ "principal_diagnoses": annotate_diagnosis_ids(
+ last_consultation__diagnoses__is_principal=True
+ ),
+ "unconfirmed_diagnoses": annotate_diagnosis_ids(
+ last_consultation__diagnoses__verification_status=ConditionVerificationStatus.UNCONFIRMED
+ ),
+ "provisional_diagnoses": annotate_diagnosis_ids(
+ last_consultation__diagnoses__verification_status=ConditionVerificationStatus.PROVISIONAL
+ ),
+ "differential_diagnoses": annotate_diagnosis_ids(
+ last_consultation__diagnoses__verification_status=ConditionVerificationStatus.DIFFERENTIAL
+ ),
+ "confirmed_diagnoses": annotate_diagnosis_ids(
+ last_consultation__diagnoses__verification_status=ConditionVerificationStatus.CONFIRMED
+ ),
+ }
+
CSV_MAPPING = {
# Patient Details
"external_id": "Patient ID",
@@ -491,8 +518,13 @@ def save(self, *args, **kwargs) -> None:
"last_consultation__consultation_status": "Status during consultation",
"last_consultation__created_date": "Date of first consultation",
"last_consultation__created_date__time": "Time of first consultation",
- "last_consultation__icd11_diagnoses": "Diagnoses",
- "last_consultation__icd11_provisional_diagnoses": "Provisional Diagnoses",
+ # Diagnosis Details
+ "principal_diagnoses": "Principal Diagnosis",
+ "unconfirmed_diagnoses": "Unconfirmed Diagnoses",
+ "provisional_diagnoses": "Provisional Diagnoses",
+ "differential_diagnoses": "Differential Diagnoses",
+ "confirmed_diagnoses": "Confirmed Diagnoses",
+ # Last Consultation Details
"last_consultation__suggestion": "Decision after consultation",
"last_consultation__category": "Category",
"last_consultation__discharge_date": "Date of discharge",
@@ -505,6 +537,10 @@ def format_as_date(date):
def format_as_time(time):
return time.strftime("%H:%M")
+ def format_diagnoses(diagnosis_ids):
+ diagnoses = get_icd11_diagnoses_objects_by_ids(diagnosis_ids)
+ return ", ".join([diagnosis["label"] for diagnosis in diagnoses])
+
CSV_MAKE_PRETTY = {
"gender": (lambda x: REVERSE_GENDER_CHOICES[x]),
"created_date": format_as_date,
@@ -514,12 +550,11 @@ def format_as_time(time):
"last_consultation__suggestion": (
lambda x: PatientConsultation.REVERSE_SUGGESTION_CHOICES.get(x, "-")
),
- "last_consultation__icd11_diagnoses": (
- lambda x: ", ".join([ICDDiseases.by.id[id].label.strip() for id in x])
- ),
- "last_consultation__icd11_provisional_diagnoses": (
- lambda x: ", ".join([ICDDiseases.by.id[id].label.strip() for id in x])
- ),
+ "principal_diagnoses": format_diagnoses,
+ "unconfirmed_diagnoses": format_diagnoses,
+ "provisional_diagnoses": format_diagnoses,
+ "differential_diagnoses": format_diagnoses,
+ "confirmed_diagnoses": format_diagnoses,
"last_consultation__consultation_status": (
lambda x: REVERSE_CONSULTATION_STATUS_CHOICES.get(x, "-").replace("_", " ")
),
@@ -675,7 +710,7 @@ class PatientMobileOTP(BaseModel):
otp = models.CharField(max_length=10)
-class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin):
+class PatientNotes(FacilityBaseModel, ConsultationRelatedPermissionMixin):
patient = models.ForeignKey(
PatientRegistration, on_delete=models.PROTECT, null=False, blank=False
)
@@ -689,3 +724,9 @@ class PatientNotes(FacilityBaseModel, PatientRelatedPermissionMixin):
null=True,
)
note = models.TextField(default="", blank=True)
+
+ def get_related_consultation(self):
+ # This is a temporary hack! this model does not have `assigned_to` field
+ # and hence the permission mixin will fail if edit/object_read permissions are checked (although not used as of now)
+ # Remove once patient notes is made consultation specific.
+ return self
diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py
index 6707e967a5..0b920757c2 100644
--- a/care/facility/models/patient_consultation.py
+++ b/care/facility/models/patient_consultation.py
@@ -11,7 +11,7 @@
PatientBaseModel,
)
from care.facility.models.mixins.permissions.patient import (
- PatientRelatedPermissionMixin,
+ ConsultationRelatedPermissionMixin,
)
from care.facility.models.patient_base import (
DISCHARGE_REASON_CHOICES,
@@ -26,7 +26,7 @@
from care.users.models import User
-class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin):
+class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin):
SUGGESTION_CHOICES = [
(SuggestionChoices.HI, "HOME ISOLATION"),
(SuggestionChoices.A, "ADMISSION"),
@@ -57,16 +57,18 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin):
facility = models.ForeignKey(
"Facility", on_delete=models.CASCADE, related_name="consultations"
)
- diagnosis = models.TextField(default="", null=True, blank=True) # Deprecated
- icd11_provisional_diagnoses = ArrayField(
+ deprecated_diagnosis = models.TextField(
+ default="", null=True, blank=True
+ ) # Deprecated
+ deprecated_icd11_provisional_diagnoses = ArrayField(
models.CharField(max_length=100), default=list, blank=True, null=True
- )
- icd11_diagnoses = ArrayField(
+ ) # Deprecated in favour of ConsultationDiagnosis M2M model
+ deprecated_icd11_diagnoses = ArrayField(
models.CharField(max_length=100), default=list, blank=True, null=True
- )
- icd11_principal_diagnosis = models.CharField(
+ ) # Deprecated in favour of ConsultationDiagnosis M2M model
+ deprecated_icd11_principal_diagnosis = models.CharField(
max_length=100, default="", blank=True, null=True
- )
+ ) # Deprecated in favour of ConsultationDiagnosis M2M model
symptoms = MultiSelectField(
choices=SYMPTOM_CHOICES,
default=1,
@@ -205,6 +207,9 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin):
prn_prescription = JSONField(default=dict)
discharge_advice = JSONField(default=dict)
+ def get_related_consultation(self):
+ return self
+
CSV_MAPPING = {
"consultation_created_date": "Date of Consultation",
"admission_date": "Date of Admission",
@@ -263,6 +268,74 @@ class Meta:
),
]
+ @staticmethod
+ def has_write_permission(request):
+ if not ConsultationRelatedPermissionMixin.has_write_permission(request):
+ return False
+ return (
+ request.user.is_superuser
+ or request.user.verified
+ and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"]
+ )
+
+ def has_object_read_permission(self, request):
+ if not super().has_object_read_permission(request):
+ return False
+ return (
+ request.user.is_superuser
+ or (
+ self.patient.facility
+ and request.user in self.patient.facility.users.all()
+ )
+ or (
+ self.assigned_to == request.user
+ or request.user == self.patient.assigned_to
+ )
+ or (
+ request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]
+ and (
+ self.patient.facility
+ and request.user.district == self.patient.facility.district
+ )
+ )
+ or (
+ request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]
+ and (
+ self.patient.facility
+ and request.user.state == self.patient.facility.state
+ )
+ )
+ )
+
+ def has_object_update_permission(self, request):
+ if not super().has_object_update_permission(request):
+ return False
+ return (
+ request.user.is_superuser
+ or (
+ self.patient.facility
+ and request.user in self.patient.facility.users.all()
+ )
+ or (
+ self.assigned_to == request.user
+ or request.user == self.patient.assigned_to
+ )
+ or (
+ request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]
+ and (
+ self.patient.facility
+ and request.user.district == self.patient.facility.district
+ )
+ )
+ or (
+ request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]
+ and (
+ self.patient.facility
+ and request.user.state == self.patient.facility.state
+ )
+ )
+ )
+
def has_object_discharge_patient_permission(self, request):
return self.has_object_update_permission(request)
diff --git a/care/facility/static_data/icd11.py b/care/facility/static_data/icd11.py
index ceb4aa48de..dbdcb176a7 100644
--- a/care/facility/static_data/icd11.py
+++ b/care/facility/static_data/icd11.py
@@ -15,9 +15,10 @@ def fetch_from_db():
"id": str(diagnosis["id"]),
"label": diagnosis["label"],
"is_leaf": diagnosis["is_leaf"],
+ "chapter": diagnosis["meta_chapter_short"],
}
for diagnosis in ICD11Diagnosis.objects.filter().values(
- "id", "label", "is_leaf"
+ "id", "label", "is_leaf", "meta_chapter_short"
)
]
return []
@@ -29,10 +30,16 @@ def fetch_from_db():
ICDDiseases.create_index("id", unique=True)
+def get_icd11_diagnosis_object_by_id(diagnosis_id, as_dict=False):
+ obj = None
+ with contextlib.suppress(BaseException):
+ obj = ICDDiseases.by.id[str(diagnosis_id)]
+ return obj and (obj.__dict__ if as_dict else obj)
+
+
def get_icd11_diagnoses_objects_by_ids(diagnoses_ids):
diagnosis_objects = []
for diagnosis in diagnoses_ids:
with contextlib.suppress(BaseException):
- diagnosis_object = ICDDiseases.by.id[diagnosis].__dict__
- diagnosis_objects.append(diagnosis_object)
+ diagnosis_objects.append(ICDDiseases.by.id[str(diagnosis)].__dict__)
return diagnosis_objects
diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py
index 2ca154bfe5..fb33cfaeb8 100644
--- a/care/facility/tests/test_patient_consultation_api.py
+++ b/care/facility/tests/test_patient_consultation_api.py
@@ -4,6 +4,10 @@
from rest_framework import status
from rest_framework.test import APITestCase
+from care.facility.models.icd11_diagnosis import (
+ ConditionVerificationStatus,
+ ICD11Diagnosis,
+)
from care.facility.models.patient_consultation import (
CATEGORY_CHOICES,
PatientConsultation,
@@ -47,15 +51,32 @@ def create_admission_consultation(self, patient=None, **kwargs):
{
"patient": patient.external_id,
"facility": self.facility.external_id,
+ "create_diagnoses": [
+ {
+ "diagnosis": ICD11Diagnosis.objects.first().id,
+ "is_principal": False,
+ "verification_status": ConditionVerificationStatus.CONFIRMED,
+ }
+ ],
}
)
data.update(kwargs)
- res = self.client.post(self.get_url(), data)
+ res = self.client.post(self.get_url(), data, format="json")
return PatientConsultation.objects.get(external_id=res.data["id"])
def update_consultation(self, consultation, **kwargs):
return self.client.patch(self.get_url(consultation), kwargs, "json")
+ def add_diagnosis(self, consultation, **kwargs):
+ return self.client.post(
+ f"{self.get_url(consultation)}diagnoses/", kwargs, "json"
+ )
+
+ def edit_diagnosis(self, consultation, id, **kwargs):
+ return self.client.patch(
+ f"{self.get_url(consultation)}diagnoses/{id}/", kwargs, "json"
+ )
+
def discharge(self, consultation, **kwargs):
return self.client.post(
f"{self.get_url(consultation)}discharge_patient/", kwargs, "json"
@@ -307,3 +328,139 @@ def test_update_consultation_after_discharge(self):
consultation, symptoms=[1, 2], category="MILD", suggestion="A"
)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_add_diagnoses_and_duplicate_diagnoses(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ diagnosis = ICD11Diagnosis.objects.all()[0].id
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=diagnosis,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.CONFIRMED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=diagnosis,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.PROVISIONAL,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_add_diagnosis_inactive(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ diagnosis = ICD11Diagnosis.objects.first().id
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=diagnosis,
+ is_principal=False,
+ verification_status=ConditionVerificationStatus.ENTERED_IN_ERROR,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=diagnosis,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.REFUTED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def mark_inactive_diagnosis_as_principal(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ diagnosis = ICD11Diagnosis.objects.first().id
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=diagnosis,
+ is_principal=False,
+ verification_status=ConditionVerificationStatus.CONFIRMED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ res = self.edit_diagnosis(
+ consultation,
+ res.data["id"],
+ verification_status=ConditionVerificationStatus.REFUTED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ res = self.edit_diagnosis(
+ consultation,
+ res.data["id"],
+ is_principal=True,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_change_diagnosis(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=ICD11Diagnosis.objects.all()[0].id,
+ is_principal=False,
+ verification_status=ConditionVerificationStatus.CONFIRMED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ res = self.edit_diagnosis(
+ consultation,
+ res.data["id"],
+ diagnosis=ICD11Diagnosis.objects.all()[1].id,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.PROVISIONAL,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_add_multiple_principal_diagnosis(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=ICD11Diagnosis.objects.all()[0].id,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.CONFIRMED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=ICD11Diagnosis.objects.all()[1].id,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.PROVISIONAL,
+ )
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_add_principal_edit_as_inactive_add_principal(self):
+ consultation = self.create_admission_consultation(
+ suggestion="A",
+ admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)),
+ )
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=ICD11Diagnosis.objects.all()[0].id,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.CONFIRMED,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ res = self.edit_diagnosis(
+ consultation,
+ res.data["id"],
+ verification_status=ConditionVerificationStatus.ENTERED_IN_ERROR,
+ )
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertFalse(res.data["is_principal"])
+ res = self.add_diagnosis(
+ consultation,
+ diagnosis=ICD11Diagnosis.objects.all()[1].id,
+ is_principal=True,
+ verification_status=ConditionVerificationStatus.PROVISIONAL,
+ )
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py
index 072ca43266..48af9e6c66 100644
--- a/care/facility/utils/reports/discharge_summary.py
+++ b/care/facility/utils/reports/discharge_summary.py
@@ -21,6 +21,10 @@
PrescriptionType,
)
from care.facility.models.file_upload import FileUpload
+from care.facility.models.icd11_diagnosis import (
+ ACTIVE_CONDITION_VERIFICATION_STATUSES,
+ ConditionVerificationStatus,
+)
from care.facility.static_data.icd11 import get_icd11_diagnoses_objects_by_ids
from care.hcx.models.policy import Policy
@@ -45,6 +49,46 @@ def clear_lock(consultation_ext_id: str):
cache.delete(lock_key(consultation_ext_id))
+def get_diagnoses_data(consultation: PatientConsultation):
+ entries = (
+ consultation.diagnoses.filter(
+ verification_status__in=ACTIVE_CONDITION_VERIFICATION_STATUSES
+ )
+ .order_by("-created_date")
+ .values_list(
+ "diagnosis_id",
+ "verification_status",
+ "is_principal",
+ )
+ )
+
+ # retrieve diagnosis objects from in-memory table
+ diagnoses = get_icd11_diagnoses_objects_by_ids([entry[0] for entry in entries])
+
+ principal, unconfirmed, provisional, differential, confirmed = [], [], [], [], []
+
+ for diagnosis, record in zip(diagnoses, entries):
+ _, verification_status, is_principal = record
+ if is_principal:
+ principal.append(diagnosis)
+ if verification_status == ConditionVerificationStatus.UNCONFIRMED:
+ unconfirmed.append(diagnosis)
+ if verification_status == ConditionVerificationStatus.PROVISIONAL:
+ provisional.append(diagnosis)
+ if verification_status == ConditionVerificationStatus.DIFFERENTIAL:
+ differential.append(diagnosis)
+ if verification_status == ConditionVerificationStatus.CONFIRMED:
+ confirmed.append(diagnosis)
+
+ return {
+ "principal": principal,
+ "unconfirmed": unconfirmed,
+ "provisional": provisional,
+ "differential": differential,
+ "confirmed": confirmed,
+ }
+
+
def get_discharge_summary_data(consultation: PatientConsultation):
logger.info(f"fetching discharge summary data for {consultation.external_id}")
samples = PatientSample.objects.filter(
@@ -52,15 +96,7 @@ def get_discharge_summary_data(consultation: PatientConsultation):
)
hcx = Policy.objects.filter(patient=consultation.patient)
daily_rounds = DailyRound.objects.filter(consultation=consultation)
- diagnosis = get_icd11_diagnoses_objects_by_ids(consultation.icd11_diagnoses)
- provisional_diagnosis = get_icd11_diagnoses_objects_by_ids(
- consultation.icd11_provisional_diagnoses
- )
- principal_diagnosis = get_icd11_diagnoses_objects_by_ids(
- [consultation.icd11_principal_diagnosis]
- if consultation.icd11_principal_diagnosis
- else []
- )
+ diagnoses = get_diagnoses_data(consultation)
investigations = InvestigationValue.objects.filter(
Q(consultation=consultation.id)
& (Q(value__isnull=False) | Q(notes__isnull=False))
@@ -97,9 +133,11 @@ def get_discharge_summary_data(consultation: PatientConsultation):
"patient": consultation.patient,
"samples": samples,
"hcx": hcx,
- "diagnosis": diagnosis,
- "provisional_diagnosis": provisional_diagnosis,
- "principal_diagnosis": principal_diagnosis,
+ "principal_diagnoses": diagnoses["principal"],
+ "unconfirmed_diagnoses": diagnoses["unconfirmed"],
+ "provisional_diagnoses": diagnoses["provisional"],
+ "differential_diagnoses": diagnoses["differential"],
+ "confirmed_diagnoses": diagnoses["confirmed"],
"consultation": consultation,
"prescriptions": prescriptions,
"prn_prescriptions": prn_prescriptions,
diff --git a/care/hcx/api/viewsets/gateway.py b/care/hcx/api/viewsets/gateway.py
index e584cad958..e04f92ebe2 100644
--- a/care/hcx/api/viewsets/gateway.py
+++ b/care/hcx/api/viewsets/gateway.py
@@ -12,8 +12,9 @@
from rest_framework.viewsets import GenericViewSet
from care.facility.models.file_upload import FileUpload
+from care.facility.models.icd11_diagnosis import ConditionVerificationStatus
from care.facility.models.patient_consultation import PatientConsultation
-from care.facility.static_data.icd11 import ICDDiseases
+from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id
from care.facility.utils.reports.discharge_summary import (
generate_discharge_report_signed_url,
)
@@ -139,40 +140,17 @@ def make_claim(self, request):
)
diagnoses = []
- if len(consultation.icd11_diagnoses):
- diagnoses = list(
- map(
- lambda diagnosis: {
- "id": str(uuid()),
- "label": diagnosis.label.split(" ", 1)[1],
- "code": diagnosis.label.split(" ", 1)[0] or "00",
- "type": "clinical",
- },
- list(
- map(
- lambda icd_id: ICDDiseases.by.id[icd_id],
- consultation.icd11_diagnoses,
- )
- ),
- )
- )
-
- if len(consultation.icd11_provisional_diagnoses):
- diagnoses = list(
- map(
- lambda diagnosis: {
- "id": str(uuid()),
- "label": diagnosis.label.split(" ", 1)[1],
- "code": diagnosis.label.split(" ", 1)[0] or "00",
- "type": "admitting",
- },
- list(
- map(
- lambda icd_id: ICDDiseases.by.id[icd_id],
- consultation.icd11_provisional_diagnoses,
- )
- ),
- )
+ for diagnosis_id, is_principal in consultation.diagnoses.filter(
+ verification_status=ConditionVerificationStatus.CONFIRMED
+ ).values_list("diagnosis_id", "is_principal"):
+ diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id)
+ diagnoses.append(
+ {
+ "id": str(uuid()),
+ "label": diagnosis.label.split(" ", 1)[1],
+ "code": diagnosis.label.split(" ", 1)[0] or "00",
+ "type": "principal" if is_principal else "clinical",
+ }
)
previous_claim = (
diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html
index 2b1ed15156..238d142a20 100644
--- a/care/templates/reports/patient_discharge_summary_pdf.html
+++ b/care/templates/reports/patient_discharge_summary_pdf.html
@@ -181,7 +181,73 @@
{% endif %}
- {% if provisional_diagnosis %}
+ {% if principal_diagnosis %}
+
+ Principal Diagnosis (as per ICD-11 recommended by WHO):
+
+
+
+
+
+
+ ID
+ |
+
+ Name
+ |
+
+
+
+ {% for disease in principal_diagnosis %}
+
+
+ {{disease.id}}
+ |
+
+ {{disease.label}}
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {% if unconfirmed_diagnoses %}
+
+ Unconfirmed Diagnoses (as per ICD-11 recommended by WHO):
+
+
+
+
+
+
+ ID
+ |
+
+ Name
+ |
+
+
+
+ {% for disease in unconfirmed_diagnoses %}
+
+
+ {{disease.id}}
+ |
+
+ {{disease.label}}
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {% if provisional_diagnoses %}
Provisional Diagnosis (as per ICD-11 recommended by WHO):
@@ -199,7 +265,7 @@
- {% for disease in provisional_diagnosis %}
+ {% for disease in provisional_diagnoses %}
{{disease.id}}
@@ -214,9 +280,9 @@
{% endif %}
- {% if diagnosis %}
+ {% if differential_diagnoses %}
- Diagnosis (as per ICD-11 recommended by WHO):
+ Differential Diagnoses (as per ICD-11 recommended by WHO):
@@ -232,7 +298,7 @@
- {% for disease in diagnosis %}
+ {% for disease in differential_diagnoses %}
{{disease.id}}
@@ -247,9 +313,9 @@
{% endif %}
- {% if principal_diagnosis %}
+ {% if confirmed_diagnoses %}
- Principal Diagnosis (as per ICD-11 recommended by WHO):
+ Confirmed Diagnoses (as per ICD-11 recommended by WHO):
@@ -265,7 +331,7 @@
- {% for disease in principal_diagnosis %}
+ {% for disease in confirmed_diagnoses %}
{{disease.id}}
diff --git a/config/api_router.py b/config/api_router.py
index 289df33b2a..cb1b98dbed 100644
--- a/config/api_router.py
+++ b/config/api_router.py
@@ -25,6 +25,9 @@
ConsultationBedViewSet,
PatientAssetBedViewSet,
)
+from care.facility.api.viewsets.consultation_diagnosis import (
+ ConsultationDiagnosisViewSet,
+)
from care.facility.api.viewsets.daily_round import DailyRoundsViewSet
from care.facility.api.viewsets.facility import AllFacilityViewSet, FacilityViewSet
from care.facility.api.viewsets.facility_capacity import FacilityCapacityViewSet
@@ -205,6 +208,7 @@
router, r"consultation", lookup="consultation"
)
consultation_nested_router.register(r"daily_rounds", DailyRoundsViewSet)
+consultation_nested_router.register(r"diagnoses", ConsultationDiagnosisViewSet)
consultation_nested_router.register(r"investigation", InvestigationValueViewSet)
consultation_nested_router.register(r"prescriptions", ConsultationPrescriptionViewSet)
consultation_nested_router.register(
| | |