diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 52e9c5fea5..3ea9a0f9ac 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -90,8 +90,8 @@ def _practioner(self): id = str(uuid()) name = ( ( - self.consultation.verified_by - and f"{self.consultation.verified_by.first_name} {self.consultation.verified_by.last_name}" + self.consultation.treating_physician + and f"{self.consultation.treating_physician.first_name} {self.consultation.treating_physician.last_name}" ) or self.consultation.deprecated_verified_by or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 7c58788329..b8f869015b 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -7,6 +7,7 @@ from care.abdm.utils.api_call import AbdmGateway from care.facility.api.serializers import TIMESTAMP_FIELDS +from care.facility.api.serializers.asset import AssetLocationSerializer from care.facility.api.serializers.bed import ConsultationBedSerializer from care.facility.api.serializers.consultation_diagnosis import ( ConsultationCreateDiagnosisSerializer, @@ -22,6 +23,7 @@ Prescription, PrescriptionType, ) +from care.facility.models.asset import AssetLocation from care.facility.models.bed import Bed, ConsultationBed from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -31,6 +33,7 @@ from care.facility.models.patient_base import ( DISCHARGE_REASON_CHOICES, SYMPTOM_CHOICES, + RouteToFacility, SuggestionChoices, ) from care.facility.models.patient_consultation import PatientConsultation @@ -70,6 +73,29 @@ class PatientConsultationSerializer(serializers.ModelSerializer): referred_to_external = serializers.CharField( required=False, allow_null=True, allow_blank=True ) + + referred_from_facility_object = FacilityBasicInfoSerializer( + source="referred_from_facility", read_only=True + ) + referred_from_facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), + required=False, + ) + referred_from_facility_external = serializers.CharField( + required=False, allow_null=True, allow_blank=True + ) + referred_by_external = serializers.CharField( + required=False, allow_null=True, allow_blank=True + ) + + transferred_from_location_object = AssetLocationSerializer( + source="transferred_from_location", read_only=True + ) + transferred_from_location = ExternalIdSerializerField( + queryset=AssetLocation.objects.all(), + required=False, + ) + patient = ExternalIdSerializerField(queryset=PatientRegistration.objects.all()) facility = ExternalIdSerializerField(read_only=True) @@ -78,8 +104,10 @@ class PatientConsultationSerializer(serializers.ModelSerializer): queryset=User.objects.all(), required=False, allow_null=True ) - verified_by_object = UserBaseMinimumSerializer(source="verified_by", read_only=True) - verified_by = serializers.PrimaryKeyRelatedField( + treating_physician_object = UserBaseMinimumSerializer( + source="treating_physician", read_only=True + ) + treating_physician = serializers.PrimaryKeyRelatedField( queryset=User.objects.all(), required=False, allow_null=True ) @@ -230,6 +258,58 @@ def update(self, instance, validated_data): return consultation def create(self, validated_data): + if route_to_facility := validated_data.get("route_to_facility"): + if route_to_facility == RouteToFacility.OUTPATIENT: + validated_data["icu_admission_date"] = None + validated_data["transferred_from_location"] = None + validated_data["referred_from_facility"] = None + validated_data["referred_from_facility_external"] = "" + validated_data["referred_by_external"] = "" + + if route_to_facility == RouteToFacility.INTRA_FACILITY_TRANSFER: + validated_data["referred_from_facility"] = None + validated_data["referred_from_facility_external"] = "" + validated_data["referred_by_external"] = "" + + if not validated_data.get("transferred_from_location"): + raise ValidationError( + { + "transferred_from_location": [ + "This field is required as the patient has been transferred from another location." + ] + } + ) + + if route_to_facility == RouteToFacility.INTER_FACILITY_TRANSFER: + validated_data["transferred_from_location"] = None + + if not validated_data.get( + "referred_from_facility" + ) and not validated_data.get("referred_from_facility_external"): + raise ValidationError( + { + "referred_from_facility": [ + "This field is required as the patient has been referred from another facility." + ] + } + ) + + if validated_data.get("referred_from_facility") and validated_data.get( + "referred_from_facility_external" + ): + raise ValidationError( + { + "referred_from_facility": [ + "Only one of referred_from_facility and referred_from_facility_external can be set" + ], + "referred_from_facility_external": [ + "Only one of referred_from_facility and referred_from_facility_external can be set" + ], + } + ) + else: + raise ValidationError({"route_to_facility": "This field is required"}) + create_diagnosis = validated_data.pop("create_diagnoses") action = -1 review_interval = -1 @@ -401,15 +481,18 @@ def validate(self, attrs): "suggestion" in validated and validated["suggestion"] != SuggestionChoices.DD ): - if "verified_by" not in validated: + if "treating_physician" not in validated: raise ValidationError( { - "verified_by": [ + "treating_physician": [ "This field is required as the suggestion is not 'Declared Death'" ] } ) - if not validated["verified_by"].user_type == User.TYPE_VALUE_MAP["Doctor"]: + if ( + not validated["treating_physician"].user_type + == User.TYPE_VALUE_MAP["Doctor"] + ): raise ValidationError("Only Doctors can verify a Consultation") facility = ( @@ -418,8 +501,8 @@ def validate(self, attrs): or validated["patient"].facility ) if ( - validated["verified_by"].home_facility - and validated["verified_by"].home_facility != facility + validated["treating_physician"].home_facility + and validated["treating_physician"].home_facility != facility ): raise ValidationError( "Home Facility of the Doctor must be the same as the Consultation Facility" diff --git a/care/facility/migrations/0394_rename_consultation_status_patientconsultation_route_to_facility_and_more.py b/care/facility/migrations/0394_rename_consultation_status_patientconsultation_route_to_facility_and_more.py new file mode 100644 index 0000000000..51c6db6345 --- /dev/null +++ b/care/facility/migrations/0394_rename_consultation_status_patientconsultation_route_to_facility_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.5 on 2023-11-14 06:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "facility", + "0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more", + ), + ] + + operations = [ + migrations.RenameField( + model_name="patientconsultation", + old_name="consultation_status", + new_name="route_to_facility", + ), + migrations.RenameField( + model_name="patientconsultation", + old_name="verified_by", + new_name="treating_physician", + ), + migrations.AddField( + model_name="patientconsultation", + name="icu_admission_date", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="patientconsultation", + name="referred_by_external", + field=models.TextField(blank=True, default="", null=True), + ), + migrations.AddField( + model_name="patientconsultation", + name="referred_from_facility", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="facility.facility", + ), + ), + migrations.AddField( + model_name="patientconsultation", + name="referred_from_facility_external", + field=models.TextField(blank=True, default="", null=True), + ), + migrations.AddField( + model_name="patientconsultation", + name="transferred_from_location", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="facility.assetlocation", + ), + ), + ] diff --git a/care/facility/migrations/0395_alter_patientconsultation_route_to_facility.py b/care/facility/migrations/0395_alter_patientconsultation_route_to_facility.py new file mode 100644 index 0000000000..1d65685adb --- /dev/null +++ b/care/facility/migrations/0395_alter_patientconsultation_route_to_facility.py @@ -0,0 +1,94 @@ +# Generated by Django 4.2.5 on 2023-11-14 06:23 + +import datetime + +from django.db import migrations, models +from django.db.models import DurationField, ExpressionWrapper, F +from django.db.models.functions import TruncDay +from django.utils import timezone + + +class Migration(migrations.Migration): + dependencies = [ + ( + "facility", + "0394_rename_consultation_status_patientconsultation_route_to_facility_and_more", + ), + ] + + def clean_admission_date(apps, schema_editor): + """ + Clean admission_date field to be 00:00:00 IST + + For example: + + `2023-10-06 06:00:00 +05:30 IST` (`2023-10-06 00:30:00 +00:00 UTC`) would be updated to + `2023-10-06 00:00:00 +05:30 IST` (`2023-10-05 18:30:00 +00:00 UTC`) + + Equivalent to the following SQL: + + ```sql + UPDATE facility_patientconsultation + SET admission_date = + timezone('IST', admission_date) AT TIME ZONE 'UTC' + + (date_trunc('day', timezone('IST', admission_date)) - timezone('IST', admission_date)) + + (interval '-5 hours -30 minutes') + WHERE admission_date IS NOT NULL; + ``` + """ + + current_timezone = timezone.get_current_timezone() + tz_offset = timezone.timedelta( + minutes=current_timezone.utcoffset(datetime.datetime.utcnow()).seconds / 60 + ) + + PatientConsultation = apps.get_model("facility", "PatientConsultation") + PatientConsultation.objects.filter(admission_date__isnull=False).update( + admission_date=ExpressionWrapper( + # Convert the admission_date to UTC by subtracting the current offset + F("admission_date") - tz_offset + + # Get the day part of the admission_date and subtract the actual admission_date from it + (TruncDay(F("admission_date")) - F("admission_date")), + output_field=DurationField(), + ) + ) + + def migrate_route_to_facility(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + qs = PatientConsultation.objects.all() + + # Unknown -> None + qs.filter(route_to_facility=0).update(route_to_facility=None) + # Brought Dead/Outpatient -> Outpatient/Emergency Room + qs.filter(models.Q(route_to_facility=1) | models.Q(route_to_facility=5)).update( + route_to_facility=10 + ) + # Transferred from Ward/ICU -> Internal Transfer within facility + qs.filter(models.Q(route_to_facility=2) | models.Q(route_to_facility=3)).update( + route_to_facility=30 + ) + # Referred from other hospital -> Referred from another facility + qs.filter(route_to_facility=4).update(route_to_facility=20) + + operations = [ + migrations.RunPython( + clean_admission_date, reverse_code=migrations.RunPython.noop + ), + migrations.AlterField( + model_name="patientconsultation", + name="route_to_facility", + field=models.SmallIntegerField( + blank=True, + choices=[ + (None, "(Unknown)"), + (10, "Outpatient/Emergency Room"), + (20, "Referred from another facility"), + (30, "Internal Transfer within the facility"), + ], + null=True, + ), + ), + migrations.RunPython( + migrate_route_to_facility, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 6778153505..4590b67f90 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -31,8 +31,8 @@ BLOOD_GROUP_CHOICES, DISEASE_STATUS_CHOICES, REVERSE_CATEGORY_CHOICES, - REVERSE_CONSULTATION_STATUS_CHOICES, REVERSE_DISCHARGE_REASON_CHOICES, + REVERSE_ROUTE_TO_FACILITY_CHOICES, ) from care.facility.models.patient_consultation import PatientConsultation from care.facility.static_data.icd11 import get_icd11_diagnoses_objects_by_ids @@ -515,7 +515,7 @@ def annotate_diagnosis_ids(*args, **kwargs): "created_date": "Date of Registration", "created_date__time": "Time of Registration", # Last Consultation Details - "last_consultation__consultation_status": "Status during consultation", + "last_consultation__route_to_facility": "Route to Facility", "last_consultation__created_date": "Date of first consultation", "last_consultation__created_date__time": "Time of first consultation", # Diagnosis Details @@ -555,8 +555,8 @@ def format_diagnoses(diagnosis_ids): "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("_", " ") + "last_consultation__route_to_facility": ( + lambda x: REVERSE_ROUTE_TO_FACILITY_CHOICES.get(x, "-") ), "last_consultation__category": lambda x: REVERSE_CATEGORY_CHOICES.get(x, "-"), "last_consultation__discharge_reason": ( @@ -689,7 +689,7 @@ class FacilityPatientStatsHistory(FacilityBaseModel, FacilityRelatedPermissionMi "facilitypatientstatshistory__num_patients_visited": "Vistited Patients", "facilitypatientstatshistory__num_patients_home_quarantine": "Home Quarantined Patients", "facilitypatientstatshistory__num_patients_isolation": "Patients Isolated", - "facilitypatientstatshistory__num_patient_referred": "Patients Reffered", + "facilitypatientstatshistory__num_patient_referred": "Patients Referred", "facilitypatientstatshistory__num_patient_confirmed_positive": "Patients Confirmed Positive", } diff --git a/care/facility/models/patient_base.py b/care/facility/models/patient_base.py index 250630a998..0fd2d40eab 100644 --- a/care/facility/models/patient_base.py +++ b/care/facility/models/patient_base.py @@ -1,6 +1,9 @@ import enum from types import SimpleNamespace +from django.db.models import IntegerChoices +from django.utils.translation import gettext_lazy as _ + def reverse_choices(choices): output = {} @@ -106,16 +109,11 @@ class DiseaseStatusEnum(enum.IntEnum): SuggestionChoices = SimpleNamespace(HI="HI", A="A", R="R", OP="OP", DC="DC", DD="DD") -class ConsultationStatusEnum(enum.Enum): - UNKNOWN = 0 - BROUGHT_DEAD = 1 - TRANSFERRED_FROM_WARD = 2 - TRANSFERRED_FROM_ICU = 3 - REFERRED_FROM_OTHER_HOSPITAL = 4 - OUT_PATIENT = 5 - - -ConsultationStatusChoices = [(e.value, e.name) for e in ConsultationStatusEnum] +class RouteToFacility(IntegerChoices): + OUTPATIENT = 10, _("Outpatient/Emergency Room") + INTER_FACILITY_TRANSFER = 20, _("Referred from another facility") + INTRA_FACILITY_TRANSFER = 30, _("Internal Transfer within the facility") + __empty__ = _("(Unknown)") class BedType(enum.Enum): @@ -134,7 +132,6 @@ class BedType(enum.Enum): REVERSE_DISEASE_STATUS_CHOICES = reverse_choices(DISEASE_STATUS_CHOICES) REVERSE_COVID_CATEGORY_CHOICES = reverse_choices(COVID_CATEGORY_CHOICES) # Deprecated REVERSE_CATEGORY_CHOICES = reverse_choices(CATEGORY_CHOICES) -# REVERSE_ADMIT_CHOICES = reverse_choices(ADMIT_CHOICES) REVERSE_BED_TYPE_CHOICES = reverse_choices(BedTypeChoices) -REVERSE_CONSULTATION_STATUS_CHOICES = reverse_choices(ConsultationStatusChoices) +REVERSE_ROUTE_TO_FACILITY_CHOICES = reverse_choices(RouteToFacility.choices) REVERSE_DISCHARGE_REASON_CHOICES = reverse_choices(DISCHARGE_REASON_CHOICES) diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 0b920757c2..b5e1bb02b1 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -18,8 +18,7 @@ REVERSE_CATEGORY_CHOICES, REVERSE_COVID_CATEGORY_CHOICES, SYMPTOM_CHOICES, - ConsultationStatusChoices, - ConsultationStatusEnum, + RouteToFacility, SuggestionChoices, reverse_choices, ) @@ -97,9 +96,8 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): prescriptions = JSONField(default=dict) # Deprecated procedure = JSONField(default=dict) suggestion = models.CharField(max_length=4, choices=SUGGESTION_CHOICES) - consultation_status = models.IntegerField( - default=ConsultationStatusEnum.UNKNOWN.value, - choices=ConsultationStatusChoices, + route_to_facility = models.SmallIntegerField( + choices=RouteToFacility.choices, blank=True, null=True ) review_interval = models.IntegerField(default=-1) referred_to = models.ForeignKey( @@ -108,11 +106,28 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): blank=True, on_delete=models.PROTECT, related_name="referred_patients", - ) # Deprecated - is_readmission = models.BooleanField(default=False) + ) referred_to_external = models.TextField(default="", null=True, blank=True) + transferred_from_location = models.ForeignKey( + "AssetLocation", + null=True, + blank=True, + on_delete=models.PROTECT, + ) + referred_from_facility = models.ForeignKey( + "Facility", + null=True, + blank=True, + on_delete=models.PROTECT, + ) + referred_from_facility_external = models.TextField( + default="", null=True, blank=True + ) + referred_by_external = models.TextField(default="", null=True, blank=True) + is_readmission = models.BooleanField(default=False) admitted = models.BooleanField(default=False) # Deprecated admission_date = models.DateTimeField(null=True, blank=True) # Deprecated + icu_admission_date = models.DateTimeField(null=True, blank=True) discharge_date = models.DateTimeField(null=True, blank=True) discharge_reason = models.CharField( choices=DISCHARGE_REASON_CHOICES, @@ -147,8 +162,10 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): medico_legal_case = models.BooleanField(default=False) - deprecated_verified_by = models.TextField(default="", null=True, blank=True) - verified_by = models.ForeignKey( + deprecated_verified_by = models.TextField( + default="", null=True, blank=True + ) # Deprecated + treating_physician = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True ) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index fb33cfaeb8..e5921283db 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -23,6 +23,7 @@ def setUpTestData(cls) -> None: cls.local_body = cls.create_local_body(cls.district) cls.super_user = cls.create_super_user("su", cls.district) cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.location = cls.create_asset_location(cls.facility) cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility) cls.doctor = cls.create_user( "doctor", cls.district, home_facility=cls.facility, user_type=15 @@ -30,13 +31,21 @@ def setUpTestData(cls) -> None: def get_default_data(self): return { + "route_to_facility": 10, "symptoms": [1], "category": CATEGORY_CHOICES[0][0], "examination_details": "examination_details", "history_of_present_illness": "history_of_present_illness", "treatment_plan": "treatment_plan", "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], - "verified_by": self.doctor.id, + "treating_physician": self.doctor.id, + "create_diagnoses": [ + { + "diagnosis": ICD11Diagnosis.objects.first().id, + "is_principal": False, + "verification_status": ConditionVerificationStatus.CONFIRMED, + } + ], } def get_url(self, consultation=None): @@ -44,6 +53,21 @@ def get_url(self, consultation=None): return f"/api/v1/consultation/{consultation.external_id}/" return "/api/v1/consultation/" + def create_route_to_facility_consultation( + self, patient=None, route_to_facility=10, **kwargs + ): + patient = patient or self.create_patient(self.district, self.facility) + data = self.get_default_data().copy() + kwargs.update( + { + "patient": patient.external_id, + "facility": self.facility.external_id, + "route_to_facility": route_to_facility, + } + ) + data.update(kwargs) + return self.client.post(self.get_url(), data, format="json") + def create_admission_consultation(self, patient=None, **kwargs): patient = patient or self.create_patient(self.district, self.facility) data = self.get_default_data().copy() @@ -51,13 +75,6 @@ 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) @@ -82,13 +99,13 @@ def discharge(self, consultation, **kwargs): f"{self.get_url(consultation)}discharge_patient/", kwargs, "json" ) - def test_create_consultation_verified_by_invalid_user(self): + def test_create_consultation_treating_physician_invalid_user(self): consultation = self.create_admission_consultation( suggestion="A", admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.update_consultation( - consultation, verified_by=self.doctor.id, suggestion="A" + consultation, treating_physician=self.doctor.id, suggestion="A" ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) @@ -278,6 +295,40 @@ def test_referred_to_external_valid_value(self): ) self.assertEqual(res.status_code, status.HTTP_200_OK) + def test_route_to_facility_referred_from_facility_empty(self): + res = self.create_route_to_facility_consultation(route_to_facility=20) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_route_to_facility_referred_from_facility_external(self): + res = self.create_route_to_facility_consultation( + route_to_facility=20, referred_from_facility_external="Test" + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_route_to_facility_referred_from_facility(self): + res = self.create_route_to_facility_consultation( + route_to_facility=20, referred_from_facility=self.facility.external_id + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_route_to_facility_referred_from_facility_and_external_together(self): + res = self.create_route_to_facility_consultation( + route_to_facility=20, + referred_from_facility="123", + referred_from_facility_external="Test", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_route_to_facility_transfer_within_facility_empty(self): + res = self.create_route_to_facility_consultation(route_to_facility=30) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_route_to_facility_transfer_within_facility(self): + res = self.create_route_to_facility_consultation( + route_to_facility=30, transferred_from_location=self.location.external_id + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + def test_medico_legal_case(self): consultation = self.create_admission_consultation( medico_legal_case=True, diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html index 238d142a20..22fc3101b2 100644 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -71,8 +71,8 @@
- Status during consultation: - {{consultation.get_consultation_status_display|field_name_to_label}} + Route to Facility: + {{consultation.get_route_to_facility_display|field_name_to_label}}
Decision after consultation: @@ -85,7 +85,7 @@
- Reffered to: + Referred to: {{consultation.referred_to.name}}
{% elif consultation.suggestion == 'DD' %} @@ -121,7 +121,7 @@Symptoms at admission: @@ -392,7 +392,7 @@
History of present illness: {{consultation.history_of_present_illness}} @@ -1011,8 +1011,8 @@