diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index d46316ae29..aa7abe0f50 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -33,6 +33,7 @@ from care.facility.models.patient import PatientNotesEdit from care.facility.models.patient_base import ( BLOOD_GROUP_CHOICES, + CANCER_TYPE, DISEASE_STATUS_CHOICES, DiseaseStatusEnum, NewDischargeReasonEnum, @@ -158,6 +159,9 @@ class PatientDetailSerializer(PatientListSerializer): class MedicalHistorySerializer(serializers.Serializer): disease = ChoiceField(choices=DISEASE_CHOICES) details = serializers.CharField(required=False, allow_blank=True) + status = serializers.CharField(required=False, allow_null=True) + duration = serializers.CharField(required=False, allow_null=True) + type = serializers.CharField(required=False, allow_null=True) facility = ExternalIdSerializerField( queryset=Facility.objects.all(), required=False @@ -272,6 +276,46 @@ def validate(self, attrs): if validated.get("vaccine_name") is None: raise serializers.ValidationError("Vaccine name cannot be null") + if medical_history := validated.get("medical_history"): + + for disease in medical_history: + disease_id = disease.get("disease") + if disease_id == 1: + continue + + # Handle cancer cases + if disease_id == 7: + cancer_type = disease.get("type") + if cancer_type: + gender = validated.get("gender") + if cancer_type not in CANCER_TYPE: + raise serializers.ValidationError("Invalid cancer type") + + if (gender == 1 and CANCER_TYPE[cancer_type] in {"1", "5"}) or ( + gender == 2 and CANCER_TYPE[cancer_type] == "8" + ): + raise serializers.ValidationError( + "Invalid cancer type for specified gender" + ) + + # Handle TB cases + elif disease_id == 14: + status = disease.get("status") + if status and status not in {"Active", "Old"}: + raise serializers.ValidationError("Invalid TB status") + + if duration := disease.get("duration"): + try: + duration = float(duration) + if duration < 0: + raise serializers.ValidationError( + "TB duration cannot be negative" + ) + except ValueError: + raise serializers.ValidationError( + "Duration must be a number" + ) + return validated def check_external_entry(self, srf_id): diff --git a/care/facility/migrations/0450_disease_duration_disease_status_disease_type_and_more.py b/care/facility/migrations/0450_disease_duration_disease_status_disease_type_and_more.py new file mode 100755 index 0000000000..041309a1d9 --- /dev/null +++ b/care/facility/migrations/0450_disease_duration_disease_status_disease_type_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.10 on 2024-08-23 07:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0449_merge_20240822_1343"), + ] + + operations = [ + migrations.AddField( + model_name="disease", + name="duration", + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="disease", + name="status", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="disease", + name="type", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name="disease", + name="disease", + field=models.IntegerField( + choices=[ + (1, "NO"), + (2, "Diabetes"), + (3, "Heart Disease"), + (4, "HyperTension"), + (5, "Chronic Renal Disease"), + (6, "Other Chronic Lung Diseases"), + (7, "Cancer"), + (8, "OTHER"), + (9, "COPD"), + (10, "Bronchitis"), + (11, "Chronic Neurological Or Neuromuscular Disease"), + (12, "Immunocompromised Condition"), + (13, "Liver Disease"), + (14, "TB"), + (15, "Asthma"), + ] + ), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index b3d42bef01..cdb48c627b 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -686,6 +686,9 @@ class Disease(models.Model): ) disease = models.IntegerField(choices=DISEASE_CHOICES) details = models.TextField(blank=True, null=True) + status = models.TextField(blank=True, null=True) + duration = models.IntegerField(blank=True, null=True) + type = models.TextField(blank=True, null=True) deleted = models.BooleanField(default=False) objects = BaseManager() diff --git a/care/facility/models/patient_base.py b/care/facility/models/patient_base.py index 976b38d320..ee6559701c 100644 --- a/care/facility/models/patient_base.py +++ b/care/facility/models/patient_base.py @@ -48,16 +48,41 @@ def reverse_choices(choices): (34, "NEW LOSS OF SMELL"), ] +CANCER_TYPE = { + "Breast": "1", + "Lung": "2", + "Skin": "3", + "Colorectal": "4", + "Uterus": "5", + "Leukaemia": "6", + "Bladder": "7", + "Prostate": "8", + "Melanoma": "9", + "Lymphoma": "10", + "Brain": "11", + "Liver": "12", + "Thyroid": "13", + "Others": "14", +} + DISEASE_CHOICES_MAP = { "NO": 1, "Diabetes": 2, "Heart Disease": 3, "HyperTension": 4, - "Kidney Diseases": 5, - "Lung Diseases/Asthma": 6, + "Chronic Renal Disease": 5, + "Other Chronic Lung Diseases": 6, "Cancer": 7, "OTHER": 8, + "COPD": 9, + "Bronchitis": 10, + "Chronic Neurological Or Neuromuscular Disease": 11, + "Immunocompromised Condition": 12, + "Liver Disease": 13, + "TB": 14, + "Asthma": 15, } + DISEASE_CHOICES = [(v, k) for k, v in DISEASE_CHOICES_MAP.items()] COVID_CATEGORY_CHOICES = [ diff --git a/care/facility/models/tests/test_patient.py b/care/facility/models/tests/test_patient.py index ab403e61a3..dddddc5a5e 100644 --- a/care/facility/models/tests/test_patient.py +++ b/care/facility/models/tests/test_patient.py @@ -65,3 +65,123 @@ def test_year_of_birth_validation(self): response = self.client.post("/api/v1/patient/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn("year_of_birth", response.data) + + def test_cancer_type_is_invalid(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "facility": self.facility.external_id, + "blood_group": "AB+", + "gender": 1, + "date_of_birth": None, + "year_of_birth": now().year - 2, + "disease_status": "NEGATIVE", + "emergency_phone_number": "+919000000666", + "is_vaccinated": "false", + "number_of_doses": 0, + "phone_number": "+919000044343", + "medical_history": { + "disease": "Cancer", + "details": "", + "type": "not_cancer", + }, + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/patient/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("medical_history", response.data) + + def test_cancer_type_wrong_for_gender(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "facility": self.facility.external_id, + "blood_group": "AB+", + "gender": 1, + "date_of_birth": None, + "year_of_birth": now().year - 2, + "disease_status": "NEGATIVE", + "emergency_phone_number": "+919000000666", + "is_vaccinated": "false", + "number_of_doses": 0, + "phone_number": "+919000044343", + "medical_history": { + "disease": "Cancer", + "details": "", + "type": "Breast", + }, + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/patient/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("medical_history", response.data) + + def test_tb_type_invalid(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "facility": self.facility.external_id, + "blood_group": "AB+", + "gender": 1, + "date_of_birth": None, + "year_of_birth": now().year - 2, + "disease_status": "NEGATIVE", + "emergency_phone_number": "+919000000666", + "is_vaccinated": "false", + "number_of_doses": 0, + "phone_number": "+919000044343", + "medical_history": { + "disease": "TB", + "details": "", + "status": "not_tb", + }, + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/patient/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("medical_history", response.data) + + def test_tb_duration_not_numeric(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "facility": self.facility.external_id, + "blood_group": "AB+", + "gender": 1, + "date_of_birth": None, + "year_of_birth": now().year - 2, + "disease_status": "NEGATIVE", + "emergency_phone_number": "+919000000666", + "is_vaccinated": "false", + "number_of_doses": 0, + "phone_number": "+919000044343", + "medical_history": { + "disease": "TB", + "details": "", + "duration": "not_numeric", + }, + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/patient/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("medical_history", response.data) + + def test_tb_duration_not_negative(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "facility": self.facility.external_id, + "blood_group": "AB+", + "gender": 1, + "date_of_birth": None, + "year_of_birth": now().year - 2, + "disease_status": "NEGATIVE", + "emergency_phone_number": "+919000000666", + "is_vaccinated": "false", + "number_of_doses": 0, + "phone_number": "+919000044343", + "medical_history": { + "disease": "TB", + "details": "", + "duration": -1, + }, + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/patient/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("medical_history", response.data) diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html old mode 100644 new mode 100755 index 2a2125c788..ef9079615e --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -414,10 +414,24 @@