From 332401e4b9cc0686c2d67b56fefb74ad44b19b62 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 29 Nov 2024 02:29:17 +0530 Subject: [PATCH 01/32] added service request related valuesets, model and specs --- care/emr/api/viewsets/service_request.py | 23 +++ care/emr/migrations/0005_servicerequest.py | 50 +++++ care/emr/models/service_request.py | 36 ++++ .../emr/resources/service_request/__init__.py | 0 care/emr/resources/service_request/spec.py | 182 ++++++++++++++++++ .../emr/resources/service_request/valueset.py | 64 ++++++ config/api_router.py | 3 + 7 files changed, 358 insertions(+) create mode 100644 care/emr/api/viewsets/service_request.py create mode 100644 care/emr/migrations/0005_servicerequest.py create mode 100644 care/emr/models/service_request.py create mode 100644 care/emr/resources/service_request/__init__.py create mode 100644 care/emr/resources/service_request/spec.py create mode 100644 care/emr/resources/service_request/valueset.py diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py new file mode 100644 index 0000000000..a866490fde --- /dev/null +++ b/care/emr/api/viewsets/service_request.py @@ -0,0 +1,23 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view + +from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.models.service_request import ServiceRequest +from care.emr.resources.service_request.spec import ( + ServiceRequestReadSpec, + ServiceRequestSpec, +) + + +@extend_schema_view( + create=extend_schema(request=ServiceRequestSpec), +) +class ServiceRequestViewSet(EMRModelViewSet): + database_model = ServiceRequest + pydantic_model = ServiceRequestSpec + pydantic_read_model = ServiceRequestReadSpec + + def clean_create_data(self, request, *args, **kwargs): + clean_data = super().clean_create_data(request, *args, **kwargs) + + clean_data["requester"] = request.user.external_id + return clean_data diff --git a/care/emr/migrations/0005_servicerequest.py b/care/emr/migrations/0005_servicerequest.py new file mode 100644 index 0000000000..cbccb63c06 --- /dev/null +++ b/care/emr/migrations/0005_servicerequest.py @@ -0,0 +1,50 @@ +# Generated by Django 5.1.1 on 2024-11-28 19:52 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0004_questionnaire'), + ('facility', '0466_camera_presets'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ServiceRequest', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('intent', models.CharField(blank=True, max_length=100, null=True)), + ('priority', models.CharField(blank=True, max_length=100, null=True)), + ('category', models.JSONField(blank=True, null=True)), + ('code', models.JSONField(default=dict)), + ('do_not_perform', models.BooleanField(default=False)), + ('occurrence_datetime', models.DateTimeField(blank=True, null=True)), + ('occurrence_timing', models.JSONField(blank=True, default=dict, null=True)), + ('as_needed', models.BooleanField(default=False)), + ('as_needed_for', models.JSONField(blank=True, null=True)), + ('authored_on', models.DateTimeField(blank=True, null=True)), + ('note', models.JSONField(blank=True, default=list, null=True)), + ('patient_instruction', models.TextField(blank=True, null=True)), + ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientconsultation')), + ('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), + ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/models/service_request.py b/care/emr/models/service_request.py new file mode 100644 index 0000000000..4cf0370111 --- /dev/null +++ b/care/emr/models/service_request.py @@ -0,0 +1,36 @@ +from django.db import models + +from care.emr.models.base import EMRBaseModel + + +class ServiceRequest(EMRBaseModel): + status = models.CharField(max_length=100, null=True, blank=True) + intent = models.CharField(max_length=100, null=True, blank=True) + priority = models.CharField(max_length=100, null=True, blank=True) + + category = models.JSONField(null=True, blank=True) + code = models.JSONField(default=dict, null=False, blank=False) + + do_not_perform = models.BooleanField(default=False) + + subject = models.ForeignKey( + "facility.PatientRegistration", on_delete=models.CASCADE + ) + encounter = models.ForeignKey( + "facility.PatientConsultation", on_delete=models.CASCADE + ) + + occurrence_datetime = models.DateTimeField(null=True, blank=True) + occurrence_timing = models.JSONField(default=dict, null=True, blank=True) + as_needed = models.BooleanField(default=False) + as_needed_for = models.JSONField(null=True, blank=True) + + authored_on = models.DateTimeField(null=True, blank=True) + requester = models.ForeignKey("users.User", on_delete=models.CASCADE) + + note = models.JSONField(default=list, null=True, blank=True) + patient_instruction = models.TextField(null=True, blank=True) + + replaces = models.ForeignKey( + "self", on_delete=models.CASCADE, null=True, blank=True + ) diff --git a/care/emr/resources/service_request/__init__.py b/care/emr/resources/service_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py new file mode 100644 index 0000000000..0e4f2093bb --- /dev/null +++ b/care/emr/resources/service_request/spec.py @@ -0,0 +1,182 @@ +from datetime import UTC, datetime +from enum import Enum + +from pydantic import UUID4, Field, field_validator + +from care.emr.fhir.schema.base import Annotation, Coding, Timing +from care.emr.models.service_request import ServiceRequest +from care.emr.resources.base import EMRResource +from care.emr.resources.care_valueset.care_valueset import validate_valueset +from care.emr.resources.service_request.valueset import ( + CARE_LAB_ORDER_CODE_VALUESET, + CARE_MEDICATION_AS_NEEDED_REASON_VALUESET, + CARE_SERVICE_REQUEST_CATEGORY_VALUESET, +) +from care.facility.models.ambulance import User +from care.facility.models.patient_consultation import PatientConsultation + + +class BaseServiceRequestSpec(EMRResource): + __model__ = ServiceRequest + __exclude__ = ["subject", "encounter", "requester"] + id: UUID4 = None + + +class StatusChoices(str, Enum): + draft = "draft" + active = "active" + on_hold = "on-hold" + revoked = "revoked" + completed = "completed" + entered_in_error = "entered-in-error" + unknown = "unknown" + + +class IntentChoices(str, Enum): + proposal = "proposal" + plan = "plan" + directive = "directive" + order = "order" + + +class PriorityChoices(str, Enum): + routine = "routine" + urgent = "urgent" + asap = "asap" + stat = "stat" + + +class ServiceRequestSpec(BaseServiceRequestSpec): + status: StatusChoices = Field( + default=StatusChoices.draft, + description="Indicates the status of the request, used internally to track the lifecycle of the request", + ) + intent: IntentChoices = Field( + default=IntentChoices.order, + description="Indicates the level of authority/intentionality associated with the request", + ) + priority: PriorityChoices = Field( + default=PriorityChoices.routine, + description="Indicates the urgency of the request", + ) + + category: Coding | None = Field( + default=None, + json_schema_extra={"slug": CARE_SERVICE_REQUEST_CATEGORY_VALUESET.slug}, + description="Identifies the broad category of service that is to be performed", + ) + code: Coding = Field( + ..., + json_schema_extra={ + "slug": CARE_LAB_ORDER_CODE_VALUESET.slug + }, # TODO: consider using a broader value set (https://build.fhir.org/valueset-procedure-code.html) + description="Identifies the service or product to be supplied", + ) + + do_not_perform: bool = Field( + default=False, + description="If true indicates that the service/procedure should NOT be performed", + ) + + subject: UUID4 = Field( + ..., + description="The patient for whom the service/procedure is being requested", + ) + encounter: UUID4 = Field( + ..., + description="The encounter within which this service request was created", + ) + + occurrence_datetime: datetime | None = Field( + default=None, + description="The datetime at which the requested service should occur", + ) + occurrence_timing: Timing | None = Field( + default=None, + description="The timing schedule for the requested service, used when the occurrence repeats", + ) + as_needed: bool = Field( + default=False, + description="If true indicates that the service/procedure can be performed as needed", + ) + as_needed_for: Coding | None = Field( + default=None, + json_schema_extra={"slug": CARE_MEDICATION_AS_NEEDED_REASON_VALUESET.slug}, + description="The condition under which the service/procedure should be performed", + ) + + authored_on: datetime = Field( + default=datetime.now(UTC), + description="The date when the request was made", + ) + requester: UUID4 = Field( + ..., + description="The individual who initiated the request and has responsibility for its activation", + ) + + note: list[Annotation] = Field( + default=[], + description="Comments made about the service request by the requester, performer, subject, or other participants", + ) + patient_instruction: str = Field( + default="", + description="Instructions for the patient on how the service should be performed", + ) + + replaces: UUID4 | None = Field( + None, + description="The request that is being replaced by this request, used in the case of re-orders", + ) + + @field_validator("category") + @classmethod + def validate_category(cls, category: int) -> int: + return validate_valueset( + "category", cls.model_fields["category"].json_schema_extra["slug"], category + ) + + @field_validator("code") + @classmethod + def validate_code(cls, code: int) -> int: + return validate_valueset( + "code", cls.model_fields["code"].json_schema_extra["slug"], code + ) + + @field_validator("as_needed_for") + @classmethod + def validate_as_needed_for(cls, as_needed_for: int) -> int: + return validate_valueset( + "as_needed_for", + cls.model_fields["as_needed_for"].json_schema_extra["slug"], + as_needed_for, + ) + + def perform_extra_deserialization(self, is_update, obj): + if not is_update: + obj.encounter = PatientConsultation.objects.get(external_id=self.encounter) + obj.subject = obj.encounter.patient + obj.requester = User.objects.get(external_id=self.requester) + + +class ServiceRequestReadSpec(BaseServiceRequestSpec): + status: str + intent: str + priority: str + + category: Coding | None + code: Coding + + do_not_perform: bool + + occurrence_datetime: datetime | None + occurrence_timing: Timing | None + as_needed: bool + as_needed_for: Coding | None + + authored_on: datetime + requester: UUID4 + + note: list[Annotation] + patient_instruction: str + + replaces: UUID4 | None diff --git a/care/emr/resources/service_request/valueset.py b/care/emr/resources/service_request/valueset.py new file mode 100644 index 0000000000..644af33977 --- /dev/null +++ b/care/emr/resources/service_request/valueset.py @@ -0,0 +1,64 @@ +from care.emr.fhir.schema.valueset.valueset import ValueSetCompose, ValueSetInclude +from care.emr.resources.care_valueset.care_valueset import CareValueset +from care.emr.resources.valueset.spec import ValueSetStatusOptions + +CARE_LAB_ORDER_CODE_VALUESET = CareValueset( + "Lab Order", "system-lab-order-code", ValueSetStatusOptions.active +) + +CARE_LAB_ORDER_CODE_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://loinc.org", + filter=[{"property": "ancestor", "op": "is-a", "value": "LP29693-6"}], + ) + ] + ) +) + +CARE_LAB_ORDER_CODE_VALUESET.register_as_system() + +CARE_SERVICE_REQUEST_CATEGORY_VALUESET = CareValueset( + "Service Request Category", + "system-service-request-category", + ValueSetStatusOptions.active, +) + +CARE_SERVICE_REQUEST_CATEGORY_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://snomed.info/sct", + concept=[ + {"code": "108252007", "display": "Laboratory procedure"}, + {"code": "363679005", "display": "Imaging"}, + {"code": "409063005", "display": "Counselling"}, + {"code": "409073007", "display": "Education"}, + {"code": "387713003", "display": "Surgical procedure"}, + ], + ) + ] + ) +) + +CARE_SERVICE_REQUEST_CATEGORY_VALUESET.register_as_system() + +CARE_MEDICATION_AS_NEEDED_REASON_VALUESET = CareValueset( + "Medication As Needed Reason", + "system-medication-as-needed-reason", + ValueSetStatusOptions.active, +) + +CARE_MEDICATION_AS_NEEDED_REASON_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://snomed.info/sct", + filter=[{"property": "concept", "op": "is-a", "value": "404684003"}], + ) + ] + ) +) + +CARE_MEDICATION_AS_NEEDED_REASON_VALUESET.register_as_system() diff --git a/config/api_router.py b/config/api_router.py index 7ebf8ed6c8..abedc2e03c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -6,6 +6,7 @@ from care.emr.api.viewsets.allergy_intolerance import AllergyIntoleranceViewSet from care.emr.api.viewsets.batch_request import BatchRequestView from care.emr.api.viewsets.questionnaire import QuestionnaireViewSet +from care.emr.api.viewsets.service_request import ServiceRequestViewSet from care.emr.api.viewsets.valueset import ValueSetViewSet from care.facility.api.viewsets.ambulance import AmbulanceViewSet from care.facility.api.viewsets.asset import ( @@ -147,6 +148,8 @@ router.register("questionnaire", QuestionnaireViewSet, basename="questionnaire") +router.register("service_request", ServiceRequestViewSet, basename="service-request") + # District Summary router.register( "district_patient_summary", From 3b388c31e4dd690e639d0ed0609872ae4dc556ec Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 1 Dec 2024 00:26:16 +0530 Subject: [PATCH 02/32] added specimen related valuesets, model and specs --- care/emr/api/viewsets/base.py | 20 ++- care/emr/api/viewsets/specimen.py | 14 ++ care/emr/migrations/0006_specimen.py | 42 ++++++ care/emr/models/specimen.py | 25 ++++ care/emr/resources/service_request/spec.py | 12 +- .../emr/resources/service_request/valueset.py | 10 +- care/emr/resources/specimen/__init__.py | 0 care/emr/resources/specimen/spec.py | 128 ++++++++++++++++++ care/emr/resources/specimen/valueset.py | 39 ++++++ config/api_router.py | 2 + 10 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 care/emr/api/viewsets/specimen.py create mode 100644 care/emr/migrations/0006_specimen.py create mode 100644 care/emr/models/specimen.py create mode 100644 care/emr/resources/specimen/__init__.py create mode 100644 care/emr/resources/specimen/spec.py create mode 100644 care/emr/resources/specimen/valueset.py diff --git a/care/emr/api/viewsets/base.py b/care/emr/api/viewsets/base.py index 2874e175f4..48df590314 100644 --- a/care/emr/api/viewsets/base.py +++ b/care/emr/api/viewsets/base.py @@ -57,6 +57,22 @@ def create(self, request, *args, **kwargs): ) +class EMRUpdateMixin: + def clean_update_data(self, request, *args, **kwargs): + return request.data + + def update(self, request, *args, **kwargs): + instance = self.get_object() + clean_data = self.clean_update_data(request, *args, **kwargs) + instance = self.pydantic_model(**clean_data).de_serialize(instance) + instance.save() + return Response( + self.get_read_pydantic_model() + .serialize(instance) + .model_dump(exclude=["meta"]) + ) + + class EMRListMixin: def list(self, request, *args, **kwargs): queryset = self.get_queryset() @@ -100,9 +116,6 @@ def get_object(self): queryset, **{self.lookup_field: self.kwargs[self.lookup_field]} ) - def update(self, request, *args, **kwargs): - return Response({"update": "working"}) - def delete(self, request, *args, **kwargs): return Response({"delete": "working"}) @@ -111,6 +124,7 @@ class EMRModelViewSet( EMRCreateMixin, EMRRetrieveMixin, EMRListMixin, + EMRUpdateMixin, EMRQuestionnaireMixin, EMRBaseViewSet, ): diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py new file mode 100644 index 0000000000..f94501fdc2 --- /dev/null +++ b/care/emr/api/viewsets/specimen.py @@ -0,0 +1,14 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view + +from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.models.specimen import Specimen +from care.emr.resources.specimen.spec import SpecimenReadSpec, SpecimenSpec + + +@extend_schema_view( + create=extend_schema(request=SpecimenSpec), +) +class SpecimenViewSet(EMRModelViewSet): + database_model = Specimen + pydantic_model = SpecimenSpec + pydantic_read_model = SpecimenReadSpec diff --git a/care/emr/migrations/0006_specimen.py b/care/emr/migrations/0006_specimen.py new file mode 100644 index 0000000000..c29b76032a --- /dev/null +++ b/care/emr/migrations/0006_specimen.py @@ -0,0 +1,42 @@ +# Generated by Django 5.1.1 on 2024-11-30 18:19 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0005_servicerequest'), + ('facility', '0466_camera_presets'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Specimen', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('accession_identifier', models.CharField(blank=True, max_length=100, null=True)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('type', models.JSONField(default=dict)), + ('collected_at', models.DateTimeField(blank=True, null=True)), + ('processing', models.JSONField(blank=True, default=list, null=True)), + ('note', models.JSONField(blank=True, default=list, null=True)), + ('collected_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/models/specimen.py b/care/emr/models/specimen.py new file mode 100644 index 0000000000..1f4b66292f --- /dev/null +++ b/care/emr/models/specimen.py @@ -0,0 +1,25 @@ +from django.db import models + +from care.emr.models.base import EMRBaseModel + + +class Specimen(EMRBaseModel): + accession_identifier = models.CharField(max_length=100, null=True, blank=True) + + status = models.CharField(max_length=100, null=True, blank=True) + + type = models.JSONField(default=dict, null=False, blank=False) + + subject = models.ForeignKey( + "facility.PatientRegistration", on_delete=models.CASCADE + ) + request = models.ForeignKey("emr.ServiceRequest", on_delete=models.CASCADE) + + collected_by = models.ForeignKey( + "users.User", on_delete=models.CASCADE, null=True, blank=True + ) + collected_at = models.DateTimeField(null=True, blank=True) + + processing = models.JSONField(default=list, null=True, blank=True) + + note = models.JSONField(default=list, null=True, blank=True) diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index 0e4f2093bb..f5e7868d63 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -130,25 +130,25 @@ class ServiceRequestSpec(BaseServiceRequestSpec): @field_validator("category") @classmethod - def validate_category(cls, category: int) -> int: + def validate_category(cls, value: str): return validate_valueset( - "category", cls.model_fields["category"].json_schema_extra["slug"], category + "category", cls.model_fields["category"].json_schema_extra["slug"], value ) @field_validator("code") @classmethod - def validate_code(cls, code: int) -> int: + def validate_code(cls, value: str): return validate_valueset( - "code", cls.model_fields["code"].json_schema_extra["slug"], code + "code", cls.model_fields["code"].json_schema_extra["slug"], value ) @field_validator("as_needed_for") @classmethod - def validate_as_needed_for(cls, as_needed_for: int) -> int: + def validate_as_needed_for(cls, value: str): return validate_valueset( "as_needed_for", cls.model_fields["as_needed_for"].json_schema_extra["slug"], - as_needed_for, + value, ) def perform_extra_deserialization(self, is_update, obj): diff --git a/care/emr/resources/service_request/valueset.py b/care/emr/resources/service_request/valueset.py index 644af33977..c15342a4ab 100644 --- a/care/emr/resources/service_request/valueset.py +++ b/care/emr/resources/service_request/valueset.py @@ -31,11 +31,11 @@ ValueSetInclude( system="http://snomed.info/sct", concept=[ - {"code": "108252007", "display": "Laboratory procedure"}, - {"code": "363679005", "display": "Imaging"}, - {"code": "409063005", "display": "Counselling"}, - {"code": "409073007", "display": "Education"}, - {"code": "387713003", "display": "Surgical procedure"}, + {"code": "108252007"}, + {"code": "363679005"}, + {"code": "409063005"}, + {"code": "409073007"}, + {"code": "387713003"}, ], ) ] diff --git a/care/emr/resources/specimen/__init__.py b/care/emr/resources/specimen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py new file mode 100644 index 0000000000..19e24804cd --- /dev/null +++ b/care/emr/resources/specimen/spec.py @@ -0,0 +1,128 @@ +from datetime import datetime +from enum import Enum + +from pydantic import UUID4, Field, field_validator + +from care.emr.fhir.schema.base import Annotation, Coding +from care.emr.models.service_request import ServiceRequest +from care.emr.models.specimen import Specimen +from care.emr.resources.base import EMRResource +from care.emr.resources.care_valueset.care_valueset import validate_valueset +from care.emr.resources.specimen.valueset import ( + CARE_SPECIMEN_PROCESSING_METHOD_VALUESET, + CARE_SPECIMEN_TYPE_VALUESET, +) + + +class BaseSpecimenSpec(EMRResource): + __model__ = Specimen + __exclude__ = ["subject", "request"] + id: UUID4 = None + + +class SpecimenProcessingSpec(EMRResource): + description: str | None = Field( + default=None, + description="A description of the processing step", + ) + method: Coding = Field( + ..., + json_schema_extra={"slug": CARE_SPECIMEN_PROCESSING_METHOD_VALUESET.slug}, + description="The treatment/processing step applied to the specimen", + ) + time: datetime = Field( + ..., + description="The datetime at which the processing step was performed", + ) + performer: UUID4 = Field( + ..., + description="References user who performed the processing step", + ) + + @field_validator("method") + @classmethod + def validate_method(cls, value: str): + return validate_valueset( + "method", + cls.model_fields["method"].json_schema_extra["slug"], + value, + ) + + +class StatusChoices(str, Enum): + available = "available" + unavailable = "unavailable" + unsatisfactory = "unsatisfactory" + entered_in_error = "entered-in-error" + + +class SpecimenSpec(BaseSpecimenSpec): + accession_identifier: str | None = Field( + default=None, + description="The identifier assigned to the specimen by the laboratory", + ) + + status: StatusChoices | None = Field( + default=None, + description="Indicates the status of the specimen, used internally to track the lifecycle of the specimen, None indicates that the specimen is not yet collected", + ) + + type: Coding = Field( + ..., + json_schema_extra={"slug": CARE_SPECIMEN_TYPE_VALUESET.slug}, + description="Indicates the type of specimen being collected", + ) + + subject: UUID4 = Field( + ..., + description="The patient from whom the specimen is collected", + ) + request: UUID4 = Field( + ..., + description="The service request that initiated the collection of the specimen", + ) + + collected_by: UUID4 | None = Field( + default=None, + description="References the user who collected the specimen", + ) + collected_at: datetime | None = Field( + default=None, + description="The datetime at which the specimen was collected", + ) + + processing: list[SpecimenProcessingSpec] = Field( + default=[], + description="The processing steps that have been performed on the specimen", + ) + + note: list[Annotation] = Field( + default=[], + description="Comments made about the service request by the requester, performer, subject, or other participants", + ) + + @field_validator("type") + @classmethod + def validate_type(cls, value: str): + return validate_valueset( + "type", cls.model_fields["type"].json_schema_extra["slug"], value + ) + + def perform_extra_deserialization(self, is_update, obj): + if not is_update: + obj.request = ServiceRequest.objects.get(external_id=self.request) + obj.subject = obj.request.subject + + +class SpecimenReadSpec(BaseSpecimenSpec): + accession_identifier: str | None + + status: str | None + + type: Coding + + collected_at: datetime | None + + processing: list[SpecimenProcessingSpec] + + note: list[Annotation] diff --git a/care/emr/resources/specimen/valueset.py b/care/emr/resources/specimen/valueset.py new file mode 100644 index 0000000000..4bdf13a8cd --- /dev/null +++ b/care/emr/resources/specimen/valueset.py @@ -0,0 +1,39 @@ +from care.emr.fhir.schema.valueset.valueset import ValueSetCompose, ValueSetInclude +from care.emr.resources.care_valueset.care_valueset import CareValueset +from care.emr.resources.valueset.spec import ValueSetStatusOptions + +CARE_SPECIMEN_TYPE_VALUESET = CareValueset( + "Specimen Type", "system-specimen-type", ValueSetStatusOptions.active +) # https://nrces.in/ndhm/fhir/r4/ValueSet/ndhm-specimen-types + +CARE_SPECIMEN_TYPE_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://snomed.info/sct", + filter=[{"property": "concept", "op": "is-a", "value": "123038009"}], + ) + ] + ) +) + +CARE_SPECIMEN_TYPE_VALUESET.register_as_system() + +CARE_SPECIMEN_PROCESSING_METHOD_VALUESET = CareValueset( + "Specimen Processing Method", + "system-specimen-processing-method", + ValueSetStatusOptions.active, +) + +CARE_SPECIMEN_PROCESSING_METHOD_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://snomed.info/sct", + filter=[{"property": "concept", "op": "is-a", "value": "9265001"}], + ) + ] + ) +) + +CARE_SPECIMEN_PROCESSING_METHOD_VALUESET.register_as_system() diff --git a/config/api_router.py b/config/api_router.py index abedc2e03c..c7e5d0c69e 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -7,6 +7,7 @@ from care.emr.api.viewsets.batch_request import BatchRequestView from care.emr.api.viewsets.questionnaire import QuestionnaireViewSet from care.emr.api.viewsets.service_request import ServiceRequestViewSet +from care.emr.api.viewsets.specimen import SpecimenViewSet from care.emr.api.viewsets.valueset import ValueSetViewSet from care.facility.api.viewsets.ambulance import AmbulanceViewSet from care.facility.api.viewsets.asset import ( @@ -149,6 +150,7 @@ router.register("questionnaire", QuestionnaireViewSet, basename="questionnaire") router.register("service_request", ServiceRequestViewSet, basename="service-request") +router.register("specimen", SpecimenViewSet, basename="specimen") # District Summary router.register( From d4d57be6b66bd36238fe24306b14973fc0451391 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 4 Dec 2024 10:06:12 +0530 Subject: [PATCH 03/32] added signal to auto create specimen on creation of service request --- care/emr/apps.py | 3 + care/emr/fhir/resources/concept_map.py | 91 ++++++++++++++++++++++++ care/emr/fhir/utils.py | 14 ++++ care/emr/signals/__init__.py | 1 + care/emr/signals/auto_create_specimen.py | 61 ++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 care/emr/fhir/resources/concept_map.py create mode 100644 care/emr/signals/__init__.py create mode 100644 care/emr/signals/auto_create_specimen.py diff --git a/care/emr/apps.py b/care/emr/apps.py index 52b407ce5d..cda99e52c6 100644 --- a/care/emr/apps.py +++ b/care/emr/apps.py @@ -5,3 +5,6 @@ class EMRConfig(AppConfig): name = "care.emr" verbose_name = _("Electronic Medical Record") + + def ready(self): + import care.emr.signals # noqa diff --git a/care/emr/fhir/resources/concept_map.py b/care/emr/fhir/resources/concept_map.py new file mode 100644 index 0000000000..11268fb4b5 --- /dev/null +++ b/care/emr/fhir/resources/concept_map.py @@ -0,0 +1,91 @@ +from enum import Enum + +from pydantic.main import BaseModel + +from care.emr.fhir.resources.base import ResourceManger +from care.emr.fhir.utils import parse_fhir_parameter_output + + +class ConceptMapResource(ResourceManger): + allowed_properties = ["system", "code"] + resource = "ConceptMap" + + def serialize_lookup(self, result): + structured_output = parse_fhir_parameter_output(result) + + return ConceptMapResult( + result=structured_output["result"], + match=[ + ConceptMapMatch( + equivalence=match["equivalence"], + concept=ConceptMapConcept( + display=match["concept"]["display"], + code=match["concept"]["code"], + ), + source=match["source"], + ) + for match in structured_output["match"] + ], + ) + + def translate(self): + if "system" not in self._filters or "code" not in self._filters: + err = "Both system and code are required" + raise ValueError(err) + full_result = self.query("GET", "ConceptMap/$translate", self._filters) + + return self.serialize_lookup(full_result["parameter"]) + + +class ConceptMapConcept(BaseModel): + display: str + code: str + + +class ConceptMapEquivalence(str, Enum): + def __new__(cls, value, priority=None): + obj = str.__new__(cls, value) + obj._value_ = value + obj.priority = priority + return obj + + # This is the strongest form of equality. + equal = "equal", 1 + + # Indicates equivalence, almost identical. + equivalent = "equivalent", 2 + + # Concepts are related, exact nature unspecified. + relatedto = "relatedto", 3 + + # The source concept is more specific than the target. + specializes = "specializes", 4 + + # The source concept is more general and subsumes the target. + subsumes = "subsumes", 5 + + # The source is broader in meaning than the target. + wider = "wider", 6 + + # The source is narrower in meaning than the target. + narrower = "narrower", 7 + + # The relationship is approximate but not exact. + inexact = "inexact", 8 + + # Indicates a complete lack of relationship or overlap. + disjoint = "disjoint", 9 + + # No match exists for the source concept in the target system. + unmatched = "unmatched", 10 + + +class ConceptMapMatch(BaseModel): + equivalence: ConceptMapEquivalence + concept: ConceptMapConcept + source: str + + +class ConceptMapResult(BaseModel): + result: bool + match: list[ConceptMapMatch] diff --git a/care/emr/fhir/utils.py b/care/emr/fhir/utils.py index 958177e829..49cca33cc9 100644 --- a/care/emr/fhir/utils.py +++ b/care/emr/fhir/utils.py @@ -19,12 +19,26 @@ def parse_fhir_parameter_output(parameters): response = {} for parameter in parameters: value = "" + if "valueString" in parameter: value = parameter["valueString"] + if "valueBoolean" in parameter: + value = parameter["valueBoolean"] + if "valueCode" in parameter: + value = parameter["valueCode"] + if "valueCoding" in parameter: + value = parameter["valueCoding"] + if parameter["name"] == "property": if "property" not in response: response["property"] = {} response["property"].update(parse_fhir_property_part(parameter["part"])) + elif parameter["name"] == "match": + if "match" not in response: + response["match"] = [] + + response["match"].append(parse_fhir_parameter_output(parameter["part"])) else: response[parameter["name"]] = value + return response diff --git a/care/emr/signals/__init__.py b/care/emr/signals/__init__.py new file mode 100644 index 0000000000..9578be6b34 --- /dev/null +++ b/care/emr/signals/__init__.py @@ -0,0 +1 @@ +from .auto_create_specimen import * # noqa diff --git a/care/emr/signals/auto_create_specimen.py b/care/emr/signals/auto_create_specimen.py new file mode 100644 index 0000000000..28a37c4e44 --- /dev/null +++ b/care/emr/signals/auto_create_specimen.py @@ -0,0 +1,61 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from rest_framework.exceptions import ValidationError + +from care.emr.fhir.resources.code_concept import CodeConceptResource +from care.emr.fhir.resources.concept_map import ConceptMapResource +from care.emr.models.service_request import ServiceRequest +from care.emr.resources.specimen.spec import SpecimenSpec + + +@receiver(post_save, sender=ServiceRequest) +def create_specimen(sender, instance: ServiceRequest, created: bool, **kwargs): + """ + Auto create a Specimen resource when a ServiceRequest is created. + + TODO: Change the trigger later to when the billing is done. + """ + + if not created: + return None + + code_concept = ( + CodeConceptResource() + .filter(system="http://loinc.org", code=instance.code.get("code")) + .get() + ) + + loinc_specimen_code = code_concept.property.get("system-core", {}).get("code") + concept_map = ( + ConceptMapResource() + .filter(system="http://loinc.org", code=loinc_specimen_code) + .translate() + ) + + specimen_matches = list( + filter( + lambda x: "(specimen)" in x.concept.display, + concept_map.match, + ) + ) + specimen_matches.sort(key=lambda x: x.equivalence.priority) + + if len(specimen_matches) == 0: + return ValidationError( + f"No Specimen found for the given Service Request code {instance.code}" + ) + + specimen_coding = specimen_matches[0].concept + + specimen = SpecimenSpec( + type={ + "code": specimen_coding.code, + "display": specimen_coding.display, + "system": "http://snomed.info/sct", + }, + request=instance.external_id, + subject=instance.subject.external_id, + ).de_serialize() + specimen.save() + + return specimen From a38f83d6b73eb45b771cb7bf29a5000d7b0f46bd Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 8 Dec 2024 16:35:19 +0530 Subject: [PATCH 04/32] resolved migration conflict and import path errors --- ..._0006_specimen_0017_alter_questionnaire_slug.py | 14 ++++++++++++++ care/emr/resources/service_request/spec.py | 2 +- care/emr/resources/service_request/valueset.py | 2 +- care/emr/resources/specimen/spec.py | 2 +- care/emr/resources/specimen/valueset.py | 2 +- 5 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py diff --git a/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py b/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py new file mode 100644 index 0000000000..96fc5eb5e6 --- /dev/null +++ b/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.1 on 2024-12-08 10:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0006_specimen'), + ('emr', '0017_alter_questionnaire_slug'), + ] + + operations = [ + ] diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index f5e7868d63..d12484e87d 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -5,8 +5,8 @@ from care.emr.fhir.schema.base import Annotation, Coding, Timing from care.emr.models.service_request import ServiceRequest +from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource -from care.emr.resources.care_valueset.care_valueset import validate_valueset from care.emr.resources.service_request.valueset import ( CARE_LAB_ORDER_CODE_VALUESET, CARE_MEDICATION_AS_NEEDED_REASON_VALUESET, diff --git a/care/emr/resources/service_request/valueset.py b/care/emr/resources/service_request/valueset.py index c15342a4ab..9262b4cba7 100644 --- a/care/emr/resources/service_request/valueset.py +++ b/care/emr/resources/service_request/valueset.py @@ -1,5 +1,5 @@ from care.emr.fhir.schema.valueset.valueset import ValueSetCompose, ValueSetInclude -from care.emr.resources.care_valueset.care_valueset import CareValueset +from care.emr.registries.care_valueset.care_valueset import CareValueset from care.emr.resources.valueset.spec import ValueSetStatusOptions CARE_LAB_ORDER_CODE_VALUESET = CareValueset( diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 19e24804cd..5267f3b7d7 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -6,8 +6,8 @@ from care.emr.fhir.schema.base import Annotation, Coding from care.emr.models.service_request import ServiceRequest from care.emr.models.specimen import Specimen +from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource -from care.emr.resources.care_valueset.care_valueset import validate_valueset from care.emr.resources.specimen.valueset import ( CARE_SPECIMEN_PROCESSING_METHOD_VALUESET, CARE_SPECIMEN_TYPE_VALUESET, diff --git a/care/emr/resources/specimen/valueset.py b/care/emr/resources/specimen/valueset.py index 4bdf13a8cd..a17144b220 100644 --- a/care/emr/resources/specimen/valueset.py +++ b/care/emr/resources/specimen/valueset.py @@ -1,5 +1,5 @@ from care.emr.fhir.schema.valueset.valueset import ValueSetCompose, ValueSetInclude -from care.emr.resources.care_valueset.care_valueset import CareValueset +from care.emr.registries.care_valueset.care_valueset import CareValueset from care.emr.resources.valueset.spec import ValueSetStatusOptions CARE_SPECIMEN_TYPE_VALUESET = CareValueset( From ed50d6acf2e9b93d07c087c4a4a21d9ece738f14 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 8 Dec 2024 18:12:43 +0530 Subject: [PATCH 05/32] added diagnostic report related valuesets, model and specs --- care/emr/api/viewsets/diagnostic_report.py | 17 ++ care/emr/migrations/0019_diagnosticreport.py | 48 +++++ care/emr/models/diagnostic_report.py | 35 ++++ .../resources/diagnostic_report/__init__.py | 0 care/emr/resources/diagnostic_report/spec.py | 179 ++++++++++++++++++ .../resources/diagnostic_report/valueset.py | 39 ++++ config/api_router.py | 4 + 7 files changed, 322 insertions(+) create mode 100644 care/emr/api/viewsets/diagnostic_report.py create mode 100644 care/emr/migrations/0019_diagnosticreport.py create mode 100644 care/emr/models/diagnostic_report.py create mode 100644 care/emr/resources/diagnostic_report/__init__.py create mode 100644 care/emr/resources/diagnostic_report/spec.py create mode 100644 care/emr/resources/diagnostic_report/valueset.py diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py new file mode 100644 index 0000000000..5b5add287a --- /dev/null +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -0,0 +1,17 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view + +from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.models.diagnostic_report import DiagnosticReport +from care.emr.resources.diagnostic_report.spec import ( + DiagnosticReportReadSpec, + DiagnosticReportSpec, +) + + +@extend_schema_view( + create=extend_schema(request=DiagnosticReportSpec), +) +class DiagnosticReportViewSet(EMRModelViewSet): + database_model = DiagnosticReport + pydantic_model = DiagnosticReportSpec + pydantic_read_model = DiagnosticReportReadSpec diff --git a/care/emr/migrations/0019_diagnosticreport.py b/care/emr/migrations/0019_diagnosticreport.py new file mode 100644 index 0000000000..fe04239e35 --- /dev/null +++ b/care/emr/migrations/0019_diagnosticreport.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.1 on 2024-12-08 12:41 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0018_merge_0006_specimen_0017_alter_questionnaire_slug'), + ('facility', '0466_camera_presets'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DiagnosticReport', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('category', models.JSONField(blank=True, null=True)), + ('code', models.JSONField(default=dict)), + ('issued', models.DateTimeField(blank=True, null=True)), + ('effective_period', models.JSONField(blank=True, null=True)), + ('media', models.JSONField(blank=True, default=list, null=True)), + ('conclusion', models.TextField(blank=True, null=True)), + ('note', models.JSONField(blank=True, default=list, null=True)), + ('based_on', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), + ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientconsultation')), + ('performer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('result', models.ManyToManyField(blank=True, to='emr.observation')), + ('results_interpreter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('specimen', models.ManyToManyField(blank=True, to='emr.specimen')), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/models/diagnostic_report.py b/care/emr/models/diagnostic_report.py new file mode 100644 index 0000000000..7f9f0faef3 --- /dev/null +++ b/care/emr/models/diagnostic_report.py @@ -0,0 +1,35 @@ +from django.db import models + +from care.emr.models.base import EMRBaseModel + + +class DiagnosticReport(EMRBaseModel): + status = models.CharField(max_length=100, null=True, blank=True) + + category = models.JSONField(null=True, blank=True) + code = models.JSONField(default=dict, null=False, blank=False) + + based_on = models.ForeignKey( + "emr.ServiceRequest", on_delete=models.CASCADE + ) # TODO: Make it GenericForeignKey when needed + subject = models.ForeignKey( + "facility.PatientRegistration", on_delete=models.CASCADE + ) + encounter = models.ForeignKey( + "facility.PatientConsultation", on_delete=models.CASCADE + ) + + performer = models.ForeignKey("users.User", on_delete=models.CASCADE) + results_interpreter = models.ForeignKey("users.User", on_delete=models.CASCADE) + + issued = models.DateTimeField(null=True, blank=True) + effective_period = models.JSONField(null=True, blank=True) + + specimen = models.ManyToManyField("emr.Specimen", blank=True) + result = models.ManyToManyField("emr.Observation", blank=True) + + media = models.JSONField(default=list, null=True, blank=True) + + conclusion = models.TextField(null=True, blank=True) + + note = models.JSONField(default=list, null=True, blank=True) diff --git a/care/emr/resources/diagnostic_report/__init__.py b/care/emr/resources/diagnostic_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py new file mode 100644 index 0000000000..0ecd33304d --- /dev/null +++ b/care/emr/resources/diagnostic_report/spec.py @@ -0,0 +1,179 @@ +from datetime import datetime +from enum import Enum + +from pydantic import UUID4, Field, field_validator + +from care.emr.fhir.schema.base import Annotation, Coding, Period +from care.emr.models.diagnostic_report import DiagnosticReport +from care.emr.models.service_request import ServiceRequest +from care.emr.registries.care_valueset.care_valueset import validate_valueset +from care.emr.resources.base import EMRResource +from care.emr.resources.diagnostic_report.valueset import ( + CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET, + CARE_DIAGNOSTIC_REPORT_CODE_VALUESET, +) +from care.users.models import User + + +class BaseDiagnosticReportSpec(EMRResource): + __model__ = DiagnosticReport + __exclude__ = [ + "subject", + "based_on", + "encounter", + "performer", + "results_interpreter", + ] + id: UUID4 = None + + +class DiagnosticReportMedia(EMRResource): + comment: str | None = Field( + default=None, + description="A description or comment about the media file", + ) + link: UUID4 = Field( + ..., + description="References the FileUpload object that contains the media file", + ) + + +class StatusChoices(str, Enum): + registered = "registered" + partial = "partial" + preliminary = "preliminary" + modified = "modified" + final = "final" + amended = "amended" + corrected = "corrected" + appended = "appended" + cancelled = "cancelled" + entered_in_error = "entered-in-error" + unknown = "unknown" + + +class DiagnosticReportSpec(BaseDiagnosticReportSpec): + status: StatusChoices | None = Field( + default=None, + description="Indicates the status of the report, used internally to track the lifecycle of the report", + ) + + category: Coding | None = Field( + default=None, + json_schema_extra={"slug": CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET.slug}, + description="Identifies the broad category of service that is to be performed", + ) + code: Coding = Field( + ..., + json_schema_extra={"slug": CARE_DIAGNOSTIC_REPORT_CODE_VALUESET.slug}, + description="Name/Code for this diagnostic report", + ) + + based_on: UUID4 = Field( + ..., + description="The resource that this report is based on, this can be a service request, a medication request, or other resource", + ) + subject: UUID4 = Field( + ..., + description="The patient this report is about", + ) + encounter: UUID4 = Field( + ..., + description="The encounter within which this report was created", + ) + + performer: UUID4 | None = Field( + default=None, + description="The user that is responsible for the report", + ) + results_interpreter: UUID4 | None = Field( + default=None, + description="The primary result interpreter", + ) + + issued: datetime | None = Field( + default=None, + description="The datetime at which the report was issued", + ) + effective_period: Period | None = Field( + default=None, + description="The period during which the report is valid", + ) + + specimen: list[UUID4] = Field( + default=[], + description="The specimens on which this report is based", + ) + result: list[UUID4] = Field( + default=[], + description="The observations that are part of this report", + ) + + media: list[DiagnosticReportMedia] = Field( + default=[], + description="Media files associated with the report", + ) + + conclusion: str | None = Field( + default=None, + description="The clinical conclusion of the report", + ) + + note: list[Annotation] = Field( + default=[], + description="Comments made about the service request by the requester, performer, subject, or other participants", + ) + + @field_validator("category") + @classmethod + def validate_category(cls, value: str): + return validate_valueset( + "category", cls.model_fields["category"].json_schema_extra["slug"], value + ) + + @field_validator("code") + @classmethod + def validate_code(cls, value: str): + return validate_valueset( + "code", cls.model_fields["code"].json_schema_extra["slug"], value + ) + + def perform_extra_deserialization(self, is_update, obj): + if not is_update: + obj.based_on = ServiceRequest.objects.get(external_id=self.request) + obj.subject = obj.request.subject + obj.encounter = obj.request.encounter + + if self.performer: + obj.performer = User.objects.get(external_id=self.performer) + + if self.results_interpreter: + obj.results_interpreter = User.objects.get( + external_id=self.results_interpreter + ) + + +class DiagnosticReportReadSpec(BaseDiagnosticReportSpec): + status: str + + category: Coding | None + code: Coding + + based_on: UUID4 + subject: UUID4 + encounter: UUID4 + + performer: UUID4 | None + results_interpreter: UUID4 | None + + issued: datetime | None + effective_period: Period | None + + specimen: list[UUID4] + result: list[UUID4] + + media: list[DiagnosticReportMedia] + + conclusion: str | None + + note: list[Annotation] diff --git a/care/emr/resources/diagnostic_report/valueset.py b/care/emr/resources/diagnostic_report/valueset.py new file mode 100644 index 0000000000..a3804400d7 --- /dev/null +++ b/care/emr/resources/diagnostic_report/valueset.py @@ -0,0 +1,39 @@ +from care.emr.fhir.schema.valueset.valueset import ValueSetCompose, ValueSetInclude +from care.emr.registries.care_valueset.care_valueset import CareValueset +from care.emr.resources.valueset.spec import ValueSetStatusOptions + +CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET = CareValueset( + "Diagnostic Report Category", + "system-diagnostic-report-category", + ValueSetStatusOptions.active, +) + +CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://terminology.hl7.org/CodeSystem/v2-0074", + ) + ] + ) +) + +CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET.register_as_system() + +CARE_DIAGNOSTIC_REPORT_CODE_VALUESET = CareValueset( + "Diagnostic Report Code", + "system-diagnostic-report-code", + ValueSetStatusOptions.active, +) + +CARE_DIAGNOSTIC_REPORT_CODE_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://loinc.org", + ) + ] + ) +) + +CARE_DIAGNOSTIC_REPORT_CODE_VALUESET.register_as_system() diff --git a/config/api_router.py b/config/api_router.py index 81795a5f07..5179668100 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -6,6 +6,7 @@ from care.emr.api.viewsets.allergy_intolerance import AllergyIntoleranceViewSet from care.emr.api.viewsets.batch_request import BatchRequestView from care.emr.api.viewsets.codition import ConditionViewSet +from care.emr.api.viewsets.diagnostic_report import DiagnosticReportViewSet from care.emr.api.viewsets.encounter import EncounterViewSet from care.emr.api.viewsets.medication_request import MedicationRequestViewSet from care.emr.api.viewsets.observation import ObservationViewSet @@ -162,6 +163,9 @@ router.register("service_request", ServiceRequestViewSet, basename="service-request") router.register("specimen", SpecimenViewSet, basename="specimen") +router.register( + "diagnostic_report", DiagnosticReportViewSet, basename="diagnostic-report" +) # District Summary router.register( From 97a0fcd7d7aa0a7f8a1c2f7a529ac9665e48cd06 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 9 Dec 2024 11:27:28 +0530 Subject: [PATCH 06/32] added endpoints to facilitate the labs flow --- care/emr/api/viewsets/diagnostic_report.py | 40 +++++ care/emr/api/viewsets/specimen.py | 130 +++++++++++++++- ...st_location_specimen_condition_and_more.py | 122 +++++++++++++++ care/emr/models/diagnostic_report.py | 43 ++++-- care/emr/models/service_request.py | 19 ++- care/emr/models/specimen.py | 40 ++++- care/emr/resources/diagnostic_report/spec.py | 96 +++++++++--- care/emr/resources/service_request/spec.py | 36 ++++- care/emr/resources/specimen/spec.py | 140 +++++++++++++++++- care/emr/resources/specimen/valueset.py | 18 +++ 10 files changed, 636 insertions(+), 48 deletions(-) create mode 100644 care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 5b5add287a..114229cc7f 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -1,11 +1,16 @@ from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework.decorators import action +from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet from care.emr.models.diagnostic_report import DiagnosticReport +from care.emr.models.observation import Observation from care.emr.resources.diagnostic_report.spec import ( + DiagnosticReportObservationRequest, DiagnosticReportReadSpec, DiagnosticReportSpec, ) +from care.emr.resources.observation.spec import Performer, PerformerType @extend_schema_view( @@ -15,3 +20,38 @@ class DiagnosticReportViewSet(EMRModelViewSet): database_model = DiagnosticReport pydantic_model = DiagnosticReportSpec pydantic_read_model = DiagnosticReportReadSpec + + @extend_schema( + request=DiagnosticReportObservationRequest, + responses={200: DiagnosticReportReadSpec}, + tags=["diagnostic_report"], + ) + @action(detail=True, methods=["POST"]) + def observations(self, request, *args, **kwargs): + data = DiagnosticReportObservationRequest(**request.data) + report: DiagnosticReport = self.get_object() + + observations = [] + for observation in data.observations: + if not observation.performer: + observation.performer = Performer( + type=PerformerType.user, + id=str(request.user.external_id), + ) + + observation_instance = observation.de_serialize() + observation_instance.subject_id = report.subject.id + observation_instance.encounter = report.encounter + observation_instance.patient = report.subject + + observations.append(observation_instance) + + observation_instances = Observation.objects.bulk_create(observations) + report.result.set(observation_instances) + report.save() + + return Response( + self.get_read_pydantic_model() + .serialize(report) + .model_dump(exclude=["meta"]), + ) diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index f94501fdc2..3480ee0c86 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -1,8 +1,24 @@ +from datetime import UTC, datetime + +from django.db.models import Q from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.fhir.schema.base import DateTime from care.emr.models.specimen import Specimen -from care.emr.resources.specimen.spec import SpecimenReadSpec, SpecimenSpec +from care.emr.resources.specimen.spec import ( + SpecimenCollectRequest, + SpecimenProcessRequest, + SpecimenReadSpec, + SpecimenReceiveAtLabRequest, + SpecimenSendToLabRequest, + SpecimenSpec, + StatusChoices, +) @extend_schema_view( @@ -12,3 +28,115 @@ class SpecimenViewSet(EMRModelViewSet): database_model = Specimen pydantic_model = SpecimenSpec pydantic_read_model = SpecimenReadSpec + + def get_object(self) -> Specimen: + return get_object_or_404( + self.get_queryset(), + Q(external_id__iexact=self.kwargs[self.lookup_field]) + | Q(identifier=self.kwargs[self.lookup_field]) + | Q(accession_identifier=self.kwargs[self.lookup_field]), + ) + + @extend_schema( + request=SpecimenCollectRequest, + responses={200: SpecimenReadSpec}, + tags=["specimen"], + ) + @action(detail=True, methods=["POST"]) + def collect(self, request, *args, **kwargs): + data = SpecimenCollectRequest(**request.data) + specimen = self.get_object() + + specimen.identifier = data.identifier + specimen.status = StatusChoices.available + specimen.collected_at = datetime.now(UTC) + specimen.collected_by = request.user + specimen.save() + + return Response( + self.get_read_pydantic_model() + .serialize(specimen) + .model_dump(exclude=["meta"]), + ) + + @extend_schema( + request=SpecimenSendToLabRequest, + responses={200: SpecimenReadSpec}, + tags=["specimen"], + ) + @action(detail=True, methods=["POST"]) + def send_to_lab(self, request, *args, **kwargs): + data = SpecimenSendToLabRequest(**request.data) + specimen = self.get_object() + service_request = specimen.request + + service_request.location = data.lab + specimen.dispatched_at = datetime.now(UTC) + specimen.dispatched_by = request.user + service_request.save() + specimen.save() + + return Response( + self.get_read_pydantic_model() + .serialize(specimen) + .model_dump(exclude=["meta"]), + status=status.HTTP_200_OK, + ) + + @extend_schema( + request=SpecimenReceiveAtLabRequest, + responses={200: SpecimenReadSpec}, + tags=["specimen"], + ) + @action(detail=True, methods=["POST"]) + def receive_at_lab(self, request, *args, **kwargs): + data = SpecimenReceiveAtLabRequest(**request.data) + specimen = self.get_object() + note = data.note + + specimen.accession_identifier = data.accession_identifier + specimen.condition = data.condition + specimen.received_at = datetime.now(UTC) + specimen.received_by = request.user + if note: + note.authorReference = {"id": request.user.external_id} + note.time = DateTime(datetime.now(UTC).isoformat()) + specimen.note.append(note.model_dump(mode="json")) + specimen.save() + + return Response( + self.get_read_pydantic_model() + .serialize(specimen) + .model_dump(exclude=["meta"]), + status=status.HTTP_200_OK, + ) + + @extend_schema( + request=SpecimenProcessRequest, + responses={200: SpecimenReadSpec}, + tags=["specimen"], + ) + @action(detail=True, methods=["POST"]) + def process(self, request, *args, **kwargs): + data = SpecimenProcessRequest(**request.data) + specimen = self.get_object() + + processes = [] + for process in data.process: + if not process.time: + process.time = datetime.now(UTC) + + if not process.performer: + process.performer = request.user.external_id + + processes.append(process.model_dump(mode="json")) + + specimen.processing.extend(processes) + specimen.save() + + return Response( + self.get_read_pydantic_model() + .serialize(specimen) + .model_dump(exclude=["meta"]), + status=status.HTTP_200_OK, + ) diff --git a/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py b/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py new file mode 100644 index 0000000000..e301687b2e --- /dev/null +++ b/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py @@ -0,0 +1,122 @@ +# Generated by Django 5.1.1 on 2024-12-08 21:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0019_diagnosticreport'), + ('facility', '0466_camera_presets'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='servicerequest', + name='location', + field=models.UUIDField(blank=True, null=True), + ), + migrations.AddField( + model_name='specimen', + name='condition', + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name='specimen', + name='dispatched_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='specimen', + name='dispatched_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dispatched_specimen', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='specimen', + name='identifier', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='specimen', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.specimen'), + ), + migrations.AddField( + model_name='specimen', + name='received_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='specimen', + name='received_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_specimen', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='based_on', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.servicerequest'), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='encounter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='facility.patientconsultation'), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='performer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='performed_diagnostic_report', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='result', + field=models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.observation'), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='results_interpreter', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interpreted_diagnostic_report', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='specimen', + field=models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.specimen'), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='facility.patientregistration'), + ), + migrations.AlterField( + model_name='servicerequest', + name='encounter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='facility.patientconsultation'), + ), + migrations.AlterField( + model_name='servicerequest', + name='requester', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_service_request', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='servicerequest', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='facility.patientregistration'), + ), + migrations.AlterField( + model_name='specimen', + name='collected_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collected_specimen', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='specimen', + name='request', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.servicerequest'), + ), + migrations.AlterField( + model_name='specimen', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='facility.patientregistration'), + ), + ] diff --git a/care/emr/models/diagnostic_report.py b/care/emr/models/diagnostic_report.py index 7f9f0faef3..a1501059de 100644 --- a/care/emr/models/diagnostic_report.py +++ b/care/emr/models/diagnostic_report.py @@ -1,6 +1,12 @@ from django.db import models from care.emr.models.base import EMRBaseModel +from care.emr.models.observation import Observation +from care.emr.models.service_request import ServiceRequest +from care.emr.models.specimen import Specimen +from care.facility.models.patient import PatientRegistration +from care.facility.models.patient_consultation import PatientConsultation +from care.users.models import User class DiagnosticReport(EMRBaseModel): @@ -10,26 +16,45 @@ class DiagnosticReport(EMRBaseModel): code = models.JSONField(default=dict, null=False, blank=False) based_on = models.ForeignKey( - "emr.ServiceRequest", on_delete=models.CASCADE + ServiceRequest, on_delete=models.CASCADE, related_name="diagnostic_report" ) # TODO: Make it GenericForeignKey when needed subject = models.ForeignKey( - "facility.PatientRegistration", on_delete=models.CASCADE + PatientRegistration, + on_delete=models.CASCADE, + related_name="diagnostic_report", ) encounter = models.ForeignKey( - "facility.PatientConsultation", on_delete=models.CASCADE + PatientConsultation, + on_delete=models.CASCADE, + related_name="diagnostic_report", ) - performer = models.ForeignKey("users.User", on_delete=models.CASCADE) - results_interpreter = models.ForeignKey("users.User", on_delete=models.CASCADE) + performer = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="performed_diagnostic_report", + ) + results_interpreter = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="interpreted_diagnostic_report", + ) issued = models.DateTimeField(null=True, blank=True) effective_period = models.JSONField(null=True, blank=True) - specimen = models.ManyToManyField("emr.Specimen", blank=True) - result = models.ManyToManyField("emr.Observation", blank=True) + specimen = models.ManyToManyField( + Specimen, blank=True, related_name="diagnostic_report" + ) + result = models.ManyToManyField( + Observation, blank=True, related_name="diagnostic_report" + ) media = models.JSONField(default=list, null=True, blank=True) - conclusion = models.TextField(null=True, blank=True) - note = models.JSONField(default=list, null=True, blank=True) + conclusion = models.TextField(null=True, blank=True) diff --git a/care/emr/models/service_request.py b/care/emr/models/service_request.py index 4cf0370111..8bf61a2e4e 100644 --- a/care/emr/models/service_request.py +++ b/care/emr/models/service_request.py @@ -1,6 +1,9 @@ from django.db import models from care.emr.models.base import EMRBaseModel +from care.facility.models.patient import PatientRegistration +from care.facility.models.patient_consultation import PatientConsultation +from care.users.models import User class ServiceRequest(EMRBaseModel): @@ -14,10 +17,14 @@ class ServiceRequest(EMRBaseModel): do_not_perform = models.BooleanField(default=False) subject = models.ForeignKey( - "facility.PatientRegistration", on_delete=models.CASCADE + PatientRegistration, + on_delete=models.CASCADE, + related_name="service_request", ) encounter = models.ForeignKey( - "facility.PatientConsultation", on_delete=models.CASCADE + PatientConsultation, + on_delete=models.CASCADE, + related_name="service_request", ) occurrence_datetime = models.DateTimeField(null=True, blank=True) @@ -26,7 +33,13 @@ class ServiceRequest(EMRBaseModel): as_needed_for = models.JSONField(null=True, blank=True) authored_on = models.DateTimeField(null=True, blank=True) - requester = models.ForeignKey("users.User", on_delete=models.CASCADE) + requester = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="requested_service_request" + ) + + location = models.UUIDField( + null=True, blank=True + ) # TODO: Make this a foreign key of emr.Organization once it is created note = models.JSONField(default=list, null=True, blank=True) patient_instruction = models.TextField(null=True, blank=True) diff --git a/care/emr/models/specimen.py b/care/emr/models/specimen.py index 1f4b66292f..42e0f04d68 100644 --- a/care/emr/models/specimen.py +++ b/care/emr/models/specimen.py @@ -1,9 +1,13 @@ from django.db import models from care.emr.models.base import EMRBaseModel +from care.emr.models.service_request import ServiceRequest +from care.facility.models.patient import PatientRegistration +from care.users.models import User class Specimen(EMRBaseModel): + identifier = models.CharField(max_length=100, null=True, blank=True) accession_identifier = models.CharField(max_length=100, null=True, blank=True) status = models.CharField(max_length=100, null=True, blank=True) @@ -11,15 +15,45 @@ class Specimen(EMRBaseModel): type = models.JSONField(default=dict, null=False, blank=False) subject = models.ForeignKey( - "facility.PatientRegistration", on_delete=models.CASCADE + PatientRegistration, + on_delete=models.CASCADE, + related_name="specimen", + ) + request = models.ForeignKey( + ServiceRequest, on_delete=models.CASCADE, related_name="specimen" ) - request = models.ForeignKey("emr.ServiceRequest", on_delete=models.CASCADE) collected_by = models.ForeignKey( - "users.User", on_delete=models.CASCADE, null=True, blank=True + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="collected_specimen", ) collected_at = models.DateTimeField(null=True, blank=True) + dispatched_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="dispatched_specimen", + ) + dispatched_at = models.DateTimeField(null=True, blank=True) + + received_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="received_specimen", + ) + received_at = models.DateTimeField(null=True, blank=True) + + condition = models.JSONField(null=True, blank=True) + processing = models.JSONField(default=list, null=True, blank=True) note = models.JSONField(default=list, null=True, blank=True) + + parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 0ecd33304d..3e7d3b9261 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -1,17 +1,26 @@ from datetime import datetime from enum import Enum -from pydantic import UUID4, Field, field_validator +from pydantic import UUID4, BaseModel, Field, field_validator from care.emr.fhir.schema.base import Annotation, Coding, Period from care.emr.models.diagnostic_report import DiagnosticReport from care.emr.models.service_request import ServiceRequest +from care.emr.models.specimen import Specimen from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource from care.emr.resources.diagnostic_report.valueset import ( CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET, CARE_DIAGNOSTIC_REPORT_CODE_VALUESET, ) +from care.emr.resources.observation.spec import ObservationSpec +from care.emr.resources.service_request.spec import ServiceRequestReadSpec +from care.emr.resources.specimen.spec import SpecimenReadSpec +from care.facility.api.serializers.patient import PatientDetailSerializer +from care.facility.api.serializers.patient_consultation import ( + PatientConsultationSerializer, +) +from care.users.api.serializers.user import UserBaseMinimumSerializer from care.users.models import User @@ -53,8 +62,8 @@ class StatusChoices(str, Enum): class DiagnosticReportSpec(BaseDiagnosticReportSpec): - status: StatusChoices | None = Field( - default=None, + status: StatusChoices = Field( + default=StatusChoices.registered, description="Indicates the status of the report, used internally to track the lifecycle of the report", ) @@ -64,7 +73,7 @@ class DiagnosticReportSpec(BaseDiagnosticReportSpec): description="Identifies the broad category of service that is to be performed", ) code: Coding = Field( - ..., + default=None, json_schema_extra={"slug": CARE_DIAGNOSTIC_REPORT_CODE_VALUESET.slug}, description="Name/Code for this diagnostic report", ) @@ -74,11 +83,11 @@ class DiagnosticReportSpec(BaseDiagnosticReportSpec): description="The resource that this report is based on, this can be a service request, a medication request, or other resource", ) subject: UUID4 = Field( - ..., + default=None, description="The patient this report is about", ) encounter: UUID4 = Field( - ..., + default=None, description="The encounter within which this report was created", ) @@ -114,15 +123,14 @@ class DiagnosticReportSpec(BaseDiagnosticReportSpec): description="Media files associated with the report", ) - conclusion: str | None = Field( - default=None, - description="The clinical conclusion of the report", - ) - note: list[Annotation] = Field( default=[], description="Comments made about the service request by the requester, performer, subject, or other participants", ) + conclusion: str | None = Field( + default=None, + description="The clinical conclusion of the report", + ) @field_validator("category") @classmethod @@ -140,9 +148,12 @@ def validate_code(cls, value: str): def perform_extra_deserialization(self, is_update, obj): if not is_update: - obj.based_on = ServiceRequest.objects.get(external_id=self.request) - obj.subject = obj.request.subject - obj.encounter = obj.request.encounter + obj.based_on = ServiceRequest.objects.get(external_id=self.based_on) + obj.subject = obj.based_on.subject + obj.encounter = obj.based_on.encounter + + if not obj.code: + obj.code = obj.based_on.code if self.performer: obj.performer = User.objects.get(external_id=self.performer) @@ -152,28 +163,69 @@ def perform_extra_deserialization(self, is_update, obj): external_id=self.results_interpreter ) + obj.save() + + if self.specimen: + specimens = Specimen.objects.filter(external_id__in=self.specimen) + if specimens.count() != len(self.specimen): + message = "One or more specimens are not found in the database" + raise ValueError(message) + obj.specimen.set(specimens) + class DiagnosticReportReadSpec(BaseDiagnosticReportSpec): + __exclude__ = [] + status: str category: Coding | None code: Coding - based_on: UUID4 - subject: UUID4 - encounter: UUID4 + based_on: ServiceRequestReadSpec + subject: dict + encounter: dict - performer: UUID4 | None - results_interpreter: UUID4 | None + performer: dict | None + results_interpreter: dict | None issued: datetime | None effective_period: Period | None - specimen: list[UUID4] - result: list[UUID4] + specimen: list[SpecimenReadSpec] + result: list[ObservationSpec] media: list[DiagnosticReportMedia] + note: list[Annotation] conclusion: str | None - note: list[Annotation] + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + + mapping["subject"] = PatientDetailSerializer(obj.subject).data + mapping["encounter"] = PatientConsultationSerializer(obj.encounter).data + + if obj.performer: + mapping["performer"] = UserBaseMinimumSerializer(obj.performer).data + + if obj.results_interpreter: + mapping["results_interpreter"] = UserBaseMinimumSerializer( + obj.results_interpreter + ).data + + mapping["specimen"] = [ + SpecimenReadSpec.serialize(specimen).model_dump(exclude=["meta"]) + for specimen in obj.specimen.all() + ] + mapping["result"] = [ + ObservationSpec.serialize(observation).model_dump(exclude=["meta"]) + for observation in obj.result.all() + ] + + +class DiagnosticReportObservationRequest(BaseModel): + observations: list[ObservationSpec] = Field( + default=[], + description="List of observations that are part of the diagnostic report", + ) diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index d12484e87d..8b72a484ea 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -12,8 +12,13 @@ CARE_MEDICATION_AS_NEEDED_REASON_VALUESET, CARE_SERVICE_REQUEST_CATEGORY_VALUESET, ) +from care.facility.api.serializers.patient import PatientDetailSerializer +from care.facility.api.serializers.patient_consultation import ( + PatientConsultationSerializer, +) from care.facility.models.ambulance import User from care.facility.models.patient_consultation import PatientConsultation +from care.users.api.serializers.user import UserBaseMinimumSerializer class BaseServiceRequestSpec(EMRResource): @@ -114,6 +119,11 @@ class ServiceRequestSpec(BaseServiceRequestSpec): description="The individual who initiated the request and has responsibility for its activation", ) + location: UUID4 | None = Field( + default=None, + description="The location where the service will be performed", + ) + note: list[Annotation] = Field( default=[], description="Comments made about the service request by the requester, performer, subject, or other participants", @@ -159,6 +169,8 @@ def perform_extra_deserialization(self, is_update, obj): class ServiceRequestReadSpec(BaseServiceRequestSpec): + __exclude__ = [] + status: str intent: str priority: str @@ -168,15 +180,35 @@ class ServiceRequestReadSpec(BaseServiceRequestSpec): do_not_perform: bool + subject: dict + encounter: dict + occurrence_datetime: datetime | None occurrence_timing: Timing | None as_needed: bool as_needed_for: Coding | None authored_on: datetime - requester: UUID4 + requester: dict + + location: dict | None note: list[Annotation] patient_instruction: str - replaces: UUID4 | None + replaces: dict | None + + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + + mapping["subject"] = PatientDetailSerializer(obj.subject).data + mapping["encounter"] = PatientConsultationSerializer(obj.encounter).data + + mapping["requester"] = UserBaseMinimumSerializer(obj.requester).data + + mapping["replaces"] = ( + ServiceRequestReadSpec.serialize(obj.replaces).model_dump(exclude=["meta"]) + if obj.replaces + else None + ) diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 5267f3b7d7..3f96f2aa98 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -1,17 +1,21 @@ -from datetime import datetime +from datetime import UTC, datetime from enum import Enum -from pydantic import UUID4, Field, field_validator +from pydantic import UUID4, BaseModel, Field, field_validator from care.emr.fhir.schema.base import Annotation, Coding from care.emr.models.service_request import ServiceRequest from care.emr.models.specimen import Specimen from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource +from care.emr.resources.service_request.spec import ServiceRequestReadSpec from care.emr.resources.specimen.valueset import ( + CARE_SPECIMEN_CONDITION_VALUESET, CARE_SPECIMEN_PROCESSING_METHOD_VALUESET, CARE_SPECIMEN_TYPE_VALUESET, ) +from care.facility.api.serializers.patient import PatientDetailSerializer +from care.users.api.serializers.user import UserBaseMinimumSerializer class BaseSpecimenSpec(EMRResource): @@ -25,17 +29,17 @@ class SpecimenProcessingSpec(EMRResource): default=None, description="A description of the processing step", ) - method: Coding = Field( - ..., + method: Coding | None = Field( + default=None, json_schema_extra={"slug": CARE_SPECIMEN_PROCESSING_METHOD_VALUESET.slug}, description="The treatment/processing step applied to the specimen", ) time: datetime = Field( - ..., + default_factory=lambda: datetime.now(UTC), description="The datetime at which the processing step was performed", ) - performer: UUID4 = Field( - ..., + performer: UUID4 | None = Field( + default=None, description="References user who performed the processing step", ) @@ -57,6 +61,10 @@ class StatusChoices(str, Enum): class SpecimenSpec(BaseSpecimenSpec): + identifier: str | None = Field( + default=None, + description="The unique identifier assigned to the specimen after collection", + ) accession_identifier: str | None = Field( default=None, description="The identifier assigned to the specimen by the laboratory", @@ -91,6 +99,30 @@ class SpecimenSpec(BaseSpecimenSpec): description="The datetime at which the specimen was collected", ) + dispatched_by: UUID4 | None = Field( + default=None, + description="References the user who dispatched the specimen to the laboratory", + ) + dispatched_at: datetime | None = Field( + default=None, + description="The datetime at which the specimen was dispatched to the laboratory", + ) + + received_by: UUID4 | None = Field( + default=None, + description="References the user who received the specimen at the laboratory", + ) + received_at: datetime | None = Field( + default=None, + description="The datetime at which the specimen was received at the laboratory", + ) + + condition: Coding | None = Field( + default=None, + json_schema_extra={"slug": CARE_SPECIMEN_CONDITION_VALUESET.slug}, + description="The condition of the specimen while received at the laboratory", + ) + processing: list[SpecimenProcessingSpec] = Field( default=[], description="The processing steps that have been performed on the specimen", @@ -101,6 +133,11 @@ class SpecimenSpec(BaseSpecimenSpec): description="Comments made about the service request by the requester, performer, subject, or other participants", ) + parent: UUID4 | None = Field( + default=None, + description="References the parent specimen from which this specimen was derived, used for aliquots and derived specimens", + ) + @field_validator("type") @classmethod def validate_type(cls, value: str): @@ -108,6 +145,13 @@ def validate_type(cls, value: str): "type", cls.model_fields["type"].json_schema_extra["slug"], value ) + @field_validator("condition") + @classmethod + def validate_condition(cls, value: str): + return validate_valueset( + "condition", cls.model_fields["condition"].json_schema_extra["slug"], value + ) + def perform_extra_deserialization(self, is_update, obj): if not is_update: obj.request = ServiceRequest.objects.get(external_id=self.request) @@ -115,14 +159,94 @@ def perform_extra_deserialization(self, is_update, obj): class SpecimenReadSpec(BaseSpecimenSpec): + __exclude__ = [] + + identifier: str | None accession_identifier: str | None - status: str | None + status: StatusChoices | None type: Coding + subject: dict + request: ServiceRequestReadSpec + + collected_by: dict | None collected_at: datetime | None + dispatched_by: dict | None + dispatched_at: datetime | None + + received_by: dict | None + received_at: datetime | None + + condition: Coding | None + processing: list[SpecimenProcessingSpec] note: list[Annotation] + + parent: dict | None + + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + + mapping["subject"] = PatientDetailSerializer(obj.subject).data + + mapping["collected_by"] = UserBaseMinimumSerializer(obj.collected_by).data + mapping["dispatched_by"] = UserBaseMinimumSerializer(obj.dispatched_by).data + mapping["received_by"] = UserBaseMinimumSerializer(obj.received_by).data + + mapping["parent"] = ( + SpecimenReadSpec().serialize(obj.parent).model_dump(exclude=["meta"]) + if obj.parent + else None + ) + + +class SpecimenCollectRequest(BaseModel): + identifier: str | None = Field( + default=None, + description="The identifier assigned to the specimen while collecting, this can be barcode or any other identifier", + ) + + +class SpecimenSendToLabRequest(BaseModel): + lab: UUID4 = Field( + ..., + description="The laboratory to which the specimen is being sent", + ) + + +class SpecimenReceiveAtLabRequest(BaseModel): + accession_identifier: str | None = Field( + default=None, + description="The identifier assigned to the specimen by the laboratory", + ) + + condition: Coding | None = Field( + default=None, + description="The condition of the specimen while received at the laboratory", + ) + + note: Annotation = Field( + default=None, + description="Comments made about the specimen while received at the laboratory", + ) + + @field_validator("condition") + @classmethod + def validate_condition(cls, value: str): + return validate_valueset( + "condition", + SpecimenSpec.model_fields["condition"].json_schema_extra["slug"], + value, + ) + + +class SpecimenProcessRequest(BaseModel): + process: list[SpecimenProcessingSpec] = Field( + ..., + description="The processing steps that have been performed on the specimen", + ) diff --git a/care/emr/resources/specimen/valueset.py b/care/emr/resources/specimen/valueset.py index a17144b220..14fcf3813c 100644 --- a/care/emr/resources/specimen/valueset.py +++ b/care/emr/resources/specimen/valueset.py @@ -37,3 +37,21 @@ ) CARE_SPECIMEN_PROCESSING_METHOD_VALUESET.register_as_system() + +CARE_SPECIMEN_CONDITION_VALUESET = CareValueset( + "Specimen Condition", + "system-specimen-condition", + ValueSetStatusOptions.active, +) + +CARE_SPECIMEN_CONDITION_VALUESET.register_valueset( + ValueSetCompose( + include=[ + ValueSetInclude( + system="http://terminology.hl7.org/CodeSystem/v2-0493", + ) + ] + ) +) + +CARE_SPECIMEN_CONDITION_VALUESET.register_as_system() From 96fbfd71080c8e2f000c700a191184abb6f12fd6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 12 Dec 2024 05:50:26 +0530 Subject: [PATCH 07/32] added status flow filter in specimen --- care/emr/api/viewsets/service_request.py | 9 ++++++ care/emr/api/viewsets/specimen.py | 35 +++++++++++++++++++++- care/emr/resources/service_request/spec.py | 3 ++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index a866490fde..084606cf66 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -1,3 +1,5 @@ +from django_filters import FilterSet, UUIDFilter +from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from care.emr.api.viewsets.base import EMRModelViewSet @@ -8,6 +10,11 @@ ) +class ServiceRequestFilters(FilterSet): + subject = UUIDFilter(field_name="subject__external_id") + encounter = UUIDFilter(field_name="encounter__external_id") + + @extend_schema_view( create=extend_schema(request=ServiceRequestSpec), ) @@ -15,6 +22,8 @@ class ServiceRequestViewSet(EMRModelViewSet): database_model = ServiceRequest pydantic_model = ServiceRequestSpec pydantic_read_model = ServiceRequestReadSpec + filter_backends = [DjangoFilterBackend] + filterset_class = ServiceRequestFilters def clean_create_data(self, request, *args, **kwargs): clean_data = super().clean_create_data(request, *args, **kwargs) diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 3480ee0c86..808c2443a1 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -1,6 +1,9 @@ from datetime import UTC, datetime +from enum import Enum -from django.db.models import Q +from django.db.models import Case, CharField, Q, Value, When +from django_filters import CharFilter, FilterSet, UUIDFilter +from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import status from rest_framework.decorators import action @@ -21,6 +24,34 @@ ) +class FlowStatusChoices(str, Enum): + ordered = "ordered" + collected = "collected" + sent = "sent" + received = "received" + in_process = "in_process" + + +class SpecimenFilters(FilterSet): + request = UUIDFilter(field_name="request__external_id") + encounter = UUIDFilter(field_name="request__encounter__external_id") + flow_status = CharFilter( + method="filter_flow_status" + ) # TODO: make this a choice filter + + def filter_flow_status(self, queryset, name, value): + return queryset.annotate( + flow_status=Case( + When(processing__gt=[], then=Value("in_process")), + When(received_at__isnull=False, then=Value("received")), + When(dispatched_at__isnull=False, then=Value("sent")), + When(collected_at__isnull=False, then=Value("collected")), + default=Value("ordered"), + output_field=CharField(), + ) + ).filter(flow_status=value) + + @extend_schema_view( create=extend_schema(request=SpecimenSpec), ) @@ -28,6 +59,8 @@ class SpecimenViewSet(EMRModelViewSet): database_model = Specimen pydantic_model = SpecimenSpec pydantic_read_model = SpecimenReadSpec + filter_backends = [DjangoFilterBackend] + filterset_class = SpecimenFilters def get_object(self) -> Specimen: return get_object_or_404( diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index 8b72a484ea..c43816b674 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -170,6 +170,9 @@ def perform_extra_deserialization(self, is_update, obj): class ServiceRequestReadSpec(BaseServiceRequestSpec): __exclude__ = [] + external_id: ( + UUID4 # TODO: remove this field and do a model dump when accessing any models + ) status: str intent: str From 9e57884bc02143202c38dd7dc83c59a2f78290a6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 15 Dec 2024 11:24:52 +0530 Subject: [PATCH 08/32] added verify and review apis in diagnostic report --- care/emr/api/viewsets/diagnostic_report.py | 70 ++++++++++++++++++++ care/emr/resources/diagnostic_report/spec.py | 18 +++++ 2 files changed, 88 insertions(+) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 114229cc7f..6d5a2101f7 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -1,3 +1,5 @@ +from django_filters import FilterSet, UUIDFilter +from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework.decorators import action from rest_framework.response import Response @@ -8,11 +10,19 @@ from care.emr.resources.diagnostic_report.spec import ( DiagnosticReportObservationRequest, DiagnosticReportReadSpec, + DiagnosticReportReviewRequest, DiagnosticReportSpec, + DiagnosticReportVerifyRequest, + StatusChoices, ) from care.emr.resources.observation.spec import Performer, PerformerType +class DiagnosticReportFilters(FilterSet): + subject = UUIDFilter(field_name="subject__external_id") + encounter = UUIDFilter(field_name="encounter__external_id") + + @extend_schema_view( create=extend_schema(request=DiagnosticReportSpec), ) @@ -20,6 +30,8 @@ class DiagnosticReportViewSet(EMRModelViewSet): database_model = DiagnosticReport pydantic_model = DiagnosticReportSpec pydantic_read_model = DiagnosticReportReadSpec + filter_backends = [DjangoFilterBackend] + filterset_class = DiagnosticReportFilters @extend_schema( request=DiagnosticReportObservationRequest, @@ -48,6 +60,64 @@ def observations(self, request, *args, **kwargs): observation_instances = Observation.objects.bulk_create(observations) report.result.set(observation_instances) + report.status = StatusChoices.partial + report.save() + + return Response( + self.get_read_pydantic_model() + .serialize(report) + .model_dump(exclude=["meta"]), + ) + + @extend_schema( + request=DiagnosticReportVerifyRequest, + responses={200: DiagnosticReportReadSpec}, + tags=["diagnostic_report"], + ) + @action(detail=True, methods=["POST"]) + def verify(self, request, *args, **kwargs): + data = DiagnosticReportVerifyRequest(**request.data) + report: DiagnosticReport = self.get_object() + + if data.is_approved: + report.status = StatusChoices.preliminary + else: + report.status = StatusChoices.cancelled + + report.save() + + return Response( + self.get_read_pydantic_model() + .serialize(report) + .model_dump(exclude=["meta"]), + ) + + @extend_schema( + request=DiagnosticReportReviewRequest, + responses={200: DiagnosticReportReadSpec}, + tags=["diagnostic_report"], + ) + @action(detail=True, methods=["POST"]) + def review(self, request, *args, **kwargs): + data = DiagnosticReportReviewRequest(**request.data) + report: DiagnosticReport = self.get_object() + + if ( + report.results_interpreter + and report.results_interpreter.external_id != request.user.external_id + ): + return Response( + {"detail": "This report is assigned to a different user for review."}, + status=403, + ) + + if data.is_approved: + report.status = StatusChoices.final + else: + report.status = StatusChoices.cancelled + + report.conclusion = data.conclusion + report.results_interpreter = request.user report.save() return Response( diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 3e7d3b9261..0d85fc8de6 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -229,3 +229,21 @@ class DiagnosticReportObservationRequest(BaseModel): default=[], description="List of observations that are part of the diagnostic report", ) + + +class DiagnosticReportVerifyRequest(BaseModel): + is_approved: bool = Field( + ..., + description="Indicates whether the diagnostic report is approved or rejected", + ) + + +class DiagnosticReportReviewRequest(BaseModel): + is_approved: bool = Field( + ..., + description="Indicates whether the diagnostic report is approved or rejected", + ) + conclusion: str | None = Field( + default=None, + description="Additional notes about the review", + ) From 6daf082f1aba79ac014d3c5071b7400139c2c163 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 15 Dec 2024 16:39:48 +0530 Subject: [PATCH 09/32] added phase filters for diagnostic report --- care/emr/api/viewsets/diagnostic_report.py | 46 +++++++++++++- care/emr/api/viewsets/service_request.py | 9 ++- care/emr/api/viewsets/specimen.py | 71 ++++++++++++++++------ 3 files changed, 103 insertions(+), 23 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 6d5a2101f7..f94118aab4 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -1,4 +1,8 @@ -from django_filters import FilterSet, UUIDFilter +from datetime import UTC, datetime + +from django.db import models +from django.db.models import Case, CharField, Value, When +from django_filters import ChoiceFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework.decorators import action @@ -18,9 +22,38 @@ from care.emr.resources.observation.spec import Performer, PerformerType +class PhaseChoices(models.TextChoices): + IN_PROCESS = "in_process", "In Process" + VERIFICATION_REQUIRED = "verification_required", "Verification Required" + REVIEW_REQUIRED = "review_required", "Review Required" + REVIEWED = "reviewed", "Reviewed" + + class DiagnosticReportFilters(FilterSet): - subject = UUIDFilter(field_name="subject__external_id") - encounter = UUIDFilter(field_name="encounter__external_id") + phase = ChoiceFilter(choices=PhaseChoices.choices, method="filter_phase") + status = ChoiceFilter( + choices=[(status.value, status.value) for status in StatusChoices] + ) + specimen = UUIDFilter(field_name="specimen__external_id") + based_on = UUIDFilter(field_name="based_on__external_id") + + ordering = OrderingFilter( + fields=( + ("created_date", "created_date"), + ("modified_date", "modified_date"), + ) + ) + + def filter_phase(self, queryset, name, value): + return queryset.annotate( + phase=Case( + When(status=StatusChoices.final, then=Value("reviewed")), + When(status=StatusChoices.preliminary, then=Value("review_required")), + When(status=StatusChoices.partial, then=Value("verification_required")), + default=Value("in_process"), + output_field=CharField(), + ) + ).filter(phase=value) @extend_schema_view( @@ -33,6 +66,12 @@ class DiagnosticReportViewSet(EMRModelViewSet): filter_backends = [DjangoFilterBackend] filterset_class = DiagnosticReportFilters + def clean_create_data(self, request, *args, **kwargs): + clean_data = super().clean_create_data(request, *args, **kwargs) + + clean_data["performer"] = request.user.external_id + return clean_data + @extend_schema( request=DiagnosticReportObservationRequest, responses={200: DiagnosticReportReadSpec}, @@ -84,6 +123,7 @@ def verify(self, request, *args, **kwargs): else: report.status = StatusChoices.cancelled + report.issued = datetime.now(UTC) report.save() return Response( diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index 084606cf66..54c4963ec4 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -1,4 +1,4 @@ -from django_filters import FilterSet, UUIDFilter +from django_filters import FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view @@ -14,6 +14,13 @@ class ServiceRequestFilters(FilterSet): subject = UUIDFilter(field_name="subject__external_id") encounter = UUIDFilter(field_name="encounter__external_id") + ordering = OrderingFilter( + fields=( + ("created_date", "created_date"), + ("modified_date", "modified_date"), + ) + ) + @extend_schema_view( create=extend_schema(request=ServiceRequestSpec), diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 808c2443a1..0d5aa0065b 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -1,8 +1,8 @@ from datetime import UTC, datetime -from enum import Enum +from django.db import models from django.db.models import Case, CharField, Q, Value, When -from django_filters import CharFilter, FilterSet, UUIDFilter +from django_filters import ChoiceFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import status @@ -24,32 +24,65 @@ ) -class FlowStatusChoices(str, Enum): - ordered = "ordered" - collected = "collected" - sent = "sent" - received = "received" - in_process = "in_process" +class PhaseChoices(models.TextChoices): + ORDERED = "ordered", "Ordered" + COLLECTED = "collected", "Collected" + SENT = "sent", "Sent" + RECEIVED = "received", "Received" + IN_PROCESS = "in_process", "In Process" class SpecimenFilters(FilterSet): request = UUIDFilter(field_name="request__external_id") encounter = UUIDFilter(field_name="request__encounter__external_id") - flow_status = CharFilter( - method="filter_flow_status" - ) # TODO: make this a choice filter + phase = ChoiceFilter(choices=PhaseChoices.choices, method="filter_phase") - def filter_flow_status(self, queryset, name, value): + ordering = OrderingFilter( + fields=( + ("created_date", "created_date"), + ("modified_date", "modified_date"), + ) + ) + + def filter_phase(self, queryset, name, value): return queryset.annotate( - flow_status=Case( - When(processing__gt=[], then=Value("in_process")), - When(received_at__isnull=False, then=Value("received")), - When(dispatched_at__isnull=False, then=Value("sent")), - When(collected_at__isnull=False, then=Value("collected")), - default=Value("ordered"), + phase=Case( + When( + Q(processing__gt=[]) + & ( + Q(diagnostic_report__isnull=True) + | Q(diagnostic_report__status__in=["registered", "partial"]) + ), + then=Value("in_process"), + ), + When( + diagnostic_report__isnull=True, + received_at__isnull=False, + then=Value("received"), + ), + When( + diagnostic_report__isnull=True, + received_at__isnull=True, + dispatched_at__isnull=False, + then=Value("sent"), + ), + When( + diagnostic_report__isnull=True, + received_at__isnull=True, + dispatched_at__isnull=True, + collected_at__isnull=False, + then=Value("collected"), + ), + When( + diagnostic_report__isnull=True, + received_at__isnull=True, + dispatched_at__isnull=True, + collected_at__isnull=True, + then=Value("ordered"), + ), output_field=CharField(), ) - ).filter(flow_status=value) + ).filter(phase=value) @extend_schema_view( From f46595c257bec7dbfb909ca84a3da8c66efa4e58 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 5 Jan 2025 13:04:14 +0530 Subject: [PATCH 10/32] fixed migration conflict --- .../migrations/0062_merge_20250104_1141.py | 14 ++++++ ...63_diagnosticreport_created_by_and_more.py | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 care/emr/migrations/0062_merge_20250104_1141.py create mode 100644 care/emr/migrations/0063_diagnosticreport_created_by_and_more.py diff --git a/care/emr/migrations/0062_merge_20250104_1141.py b/care/emr/migrations/0062_merge_20250104_1141.py new file mode 100644 index 0000000000..8e4e3aea49 --- /dev/null +++ b/care/emr/migrations/0062_merge_20250104_1141.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.1 on 2025-01-04 06:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0020_servicerequest_location_specimen_condition_and_more'), + ('emr', '0061_rename_resource_schedulableuserresource_user_and_more'), + ] + + operations = [ + ] diff --git a/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py b/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py new file mode 100644 index 0000000000..5949f0e0c1 --- /dev/null +++ b/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 5.1.1 on 2025-01-04 06:12 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0062_merge_20250104_1141'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='diagnosticreport', + name='created_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='diagnosticreport', + name='updated_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='servicerequest', + name='created_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='servicerequest', + name='updated_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='specimen', + name='created_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='specimen', + name='updated_by', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), + ), + ] From 20514ae7d237b80d80005a52fb85a1ac516cabd3 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 5 Jan 2025 22:15:09 +0530 Subject: [PATCH 11/32] updated the model references to emr models | created separate specs for list, retrieve, create and update --- care/emr/api/viewsets/diagnostic_report.py | 37 +++-- care/emr/api/viewsets/service_request.py | 19 ++- care/emr/api/viewsets/specimen.py | 45 +++-- care/emr/fhir/resources/concept_map.py | 10 +- care/emr/fhir/utils.py | 29 +++- ..._alter_servicerequest_category_and_more.py | 40 +++++ ...ter_diagnosticreport_encounter_and_more.py | 39 +++++ care/emr/models/diagnostic_report.py | 10 +- care/emr/models/service_request.py | 19 +-- care/emr/models/specimen.py | 6 +- care/emr/resources/diagnostic_report/spec.py | 130 ++++++++------- care/emr/resources/service_request/spec.py | 156 +++++++++--------- care/emr/resources/specimen/spec.py | 102 ++++++------ care/emr/signals/auto_create_specimen.py | 8 +- 14 files changed, 379 insertions(+), 271 deletions(-) create mode 100644 care/emr/migrations/0064_alter_servicerequest_category_and_more.py create mode 100644 care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index f94118aab4..14f46db401 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -12,10 +12,12 @@ from care.emr.models.diagnostic_report import DiagnosticReport from care.emr.models.observation import Observation from care.emr.resources.diagnostic_report.spec import ( + DiagnosticReportCreateSpec, + DiagnosticReportListSpec, DiagnosticReportObservationRequest, - DiagnosticReportReadSpec, + DiagnosticReportRetrieveSpec, DiagnosticReportReviewRequest, - DiagnosticReportSpec, + DiagnosticReportUpdateSpec, DiagnosticReportVerifyRequest, StatusChoices, ) @@ -57,24 +59,29 @@ def filter_phase(self, queryset, name, value): @extend_schema_view( - create=extend_schema(request=DiagnosticReportSpec), + create=extend_schema(request=DiagnosticReportCreateSpec), + update=extend_schema(request=DiagnosticReportUpdateSpec), + list=extend_schema(request=DiagnosticReportListSpec), + retrieve=extend_schema(request=DiagnosticReportRetrieveSpec), ) class DiagnosticReportViewSet(EMRModelViewSet): database_model = DiagnosticReport - pydantic_model = DiagnosticReportSpec - pydantic_read_model = DiagnosticReportReadSpec + pydantic_model = DiagnosticReportCreateSpec + pydantic_update_model = DiagnosticReportUpdateSpec + pydantic_read_model = DiagnosticReportListSpec + pydantic_retrieve_model = DiagnosticReportRetrieveSpec filter_backends = [DjangoFilterBackend] filterset_class = DiagnosticReportFilters def clean_create_data(self, request, *args, **kwargs): clean_data = super().clean_create_data(request, *args, **kwargs) - clean_data["performer"] = request.user.external_id + clean_data["performer"] = self.request.user.external_id return clean_data @extend_schema( request=DiagnosticReportObservationRequest, - responses={200: DiagnosticReportReadSpec}, + responses={200: DiagnosticReportRetrieveSpec}, tags=["diagnostic_report"], ) @action(detail=True, methods=["POST"]) @@ -103,14 +110,12 @@ def observations(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model() - .serialize(report) - .model_dump(exclude=["meta"]), + self.get_read_pydantic_model().serialize(report).to_json(), ) @extend_schema( request=DiagnosticReportVerifyRequest, - responses={200: DiagnosticReportReadSpec}, + responses={200: DiagnosticReportRetrieveSpec}, tags=["diagnostic_report"], ) @action(detail=True, methods=["POST"]) @@ -127,14 +132,12 @@ def verify(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model() - .serialize(report) - .model_dump(exclude=["meta"]), + self.get_read_pydantic_model().serialize(report).to_json(), ) @extend_schema( request=DiagnosticReportReviewRequest, - responses={200: DiagnosticReportReadSpec}, + responses={200: DiagnosticReportRetrieveSpec}, tags=["diagnostic_report"], ) @action(detail=True, methods=["POST"]) @@ -161,7 +164,5 @@ def review(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model() - .serialize(report) - .model_dump(exclude=["meta"]), + self.get_read_pydantic_model().serialize(report).to_json(), ) diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index 54c4963ec4..b99d81c068 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -5,8 +5,10 @@ from care.emr.api.viewsets.base import EMRModelViewSet from care.emr.models.service_request import ServiceRequest from care.emr.resources.service_request.spec import ( - ServiceRequestReadSpec, - ServiceRequestSpec, + ServiceRequestCreateSpec, + ServiceRequestListSpec, + ServiceRequestRetrieveSpec, + ServiceRequestUpdateSpec, ) @@ -23,17 +25,22 @@ class ServiceRequestFilters(FilterSet): @extend_schema_view( - create=extend_schema(request=ServiceRequestSpec), + create=extend_schema(request=ServiceRequestCreateSpec), + update=extend_schema(request=ServiceRequestUpdateSpec), + list=extend_schema(request=ServiceRequestListSpec), + retrieve=extend_schema(request=ServiceRequestRetrieveSpec), ) class ServiceRequestViewSet(EMRModelViewSet): database_model = ServiceRequest - pydantic_model = ServiceRequestSpec - pydantic_read_model = ServiceRequestReadSpec + pydantic_model = ServiceRequestCreateSpec + pydantic_update_model = ServiceRequestUpdateSpec + pydantic_read_model = ServiceRequestListSpec + pydantic_retrieve_model = ServiceRequestRetrieveSpec filter_backends = [DjangoFilterBackend] filterset_class = ServiceRequestFilters def clean_create_data(self, request, *args, **kwargs): clean_data = super().clean_create_data(request, *args, **kwargs) - clean_data["requester"] = request.user.external_id + clean_data["requester"] = self.request.user.external_id return clean_data diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 0d5aa0065b..c41c940e0a 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -5,7 +5,6 @@ from django_filters import ChoiceFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema, extend_schema_view -from rest_framework import status from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -15,11 +14,13 @@ from care.emr.models.specimen import Specimen from care.emr.resources.specimen.spec import ( SpecimenCollectRequest, + SpecimenCreateSpec, + SpecimenListSpec, SpecimenProcessRequest, - SpecimenReadSpec, SpecimenReceiveAtLabRequest, + SpecimenRetrieveSpec, SpecimenSendToLabRequest, - SpecimenSpec, + SpecimenUpdateSpec, StatusChoices, ) @@ -86,12 +87,17 @@ def filter_phase(self, queryset, name, value): @extend_schema_view( - create=extend_schema(request=SpecimenSpec), + create=extend_schema(request=SpecimenCreateSpec), + update=extend_schema(request=SpecimenUpdateSpec), + list=extend_schema(request=SpecimenListSpec), + retrieve=extend_schema(request=SpecimenRetrieveSpec), ) class SpecimenViewSet(EMRModelViewSet): database_model = Specimen - pydantic_model = SpecimenSpec - pydantic_read_model = SpecimenReadSpec + pydantic_model = SpecimenCreateSpec + pydantic_update_model = SpecimenUpdateSpec + pydantic_read_model = SpecimenListSpec + pydantic_retrieve_model = SpecimenRetrieveSpec filter_backends = [DjangoFilterBackend] filterset_class = SpecimenFilters @@ -105,7 +111,7 @@ def get_object(self) -> Specimen: @extend_schema( request=SpecimenCollectRequest, - responses={200: SpecimenReadSpec}, + responses={200: SpecimenRetrieveSpec}, tags=["specimen"], ) @action(detail=True, methods=["POST"]) @@ -120,14 +126,12 @@ def collect(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model() - .serialize(specimen) - .model_dump(exclude=["meta"]), + self.get_read_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( request=SpecimenSendToLabRequest, - responses={200: SpecimenReadSpec}, + responses={200: SpecimenRetrieveSpec}, tags=["specimen"], ) @action(detail=True, methods=["POST"]) @@ -143,15 +147,12 @@ def send_to_lab(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model() - .serialize(specimen) - .model_dump(exclude=["meta"]), - status=status.HTTP_200_OK, + self.get_read_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( request=SpecimenReceiveAtLabRequest, - responses={200: SpecimenReadSpec}, + responses={200: SpecimenRetrieveSpec}, tags=["specimen"], ) @action(detail=True, methods=["POST"]) @@ -171,15 +172,12 @@ def receive_at_lab(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model() - .serialize(specimen) - .model_dump(exclude=["meta"]), - status=status.HTTP_200_OK, + self.get_read_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( request=SpecimenProcessRequest, - responses={200: SpecimenReadSpec}, + responses={200: SpecimenRetrieveSpec}, tags=["specimen"], ) @action(detail=True, methods=["POST"]) @@ -201,8 +199,5 @@ def process(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model() - .serialize(specimen) - .model_dump(exclude=["meta"]), - status=status.HTTP_200_OK, + self.get_read_pydantic_model().serialize(specimen).to_json(), ) diff --git a/care/emr/fhir/resources/concept_map.py b/care/emr/fhir/resources/concept_map.py index 11268fb4b5..41192e2a72 100644 --- a/care/emr/fhir/resources/concept_map.py +++ b/care/emr/fhir/resources/concept_map.py @@ -14,17 +14,17 @@ def serialize_lookup(self, result): structured_output = parse_fhir_parameter_output(result) return ConceptMapResult( - result=structured_output["result"], + result=structured_output.get("metadata", {}).get("result", False), match=[ ConceptMapMatch( - equivalence=match["equivalence"], + equivalence=match.get("equivalence"), concept=ConceptMapConcept( - display=match["concept"]["display"], - code=match["concept"]["code"], + display=match.get("concept", {}).get("display"), + code=match.get("concept", {}).get("code"), ), source=match["source"], ) - for match in structured_output["match"] + for match in structured_output.get("match", []) ], ) diff --git a/care/emr/fhir/utils.py b/care/emr/fhir/utils.py index db60d969e5..10942794b9 100644 --- a/care/emr/fhir/utils.py +++ b/care/emr/fhir/utils.py @@ -63,6 +63,23 @@ def parse_fhir_property_part(parts: list[dict[str, Any]]) -> dict[str, Any]: return {code: value} if code else {} +def parse_fhir_match_part(parameters: list[dict[str, Any]]) -> list[dict[str, Any]]: + """ + Parse FHIR match response into a structured format. + + Args: + matches: List of FHIR matches + + Returns: + List containing parsed FHIR data + """ + response = {} + for parameter in parameters: + name = parameter.get("name") + if name in ["equivalence", "concept", "source"]: + response[name] = parse_value_field(parameter) + return response + def parse_fhir_parameter_output(parameters: list[dict[str, Any]]) -> dict[str, Any]: """ @@ -80,7 +97,15 @@ def parse_fhir_parameter_output(parameters: list[dict[str, Any]]) -> dict[str, A name = parameter.get("name") # Handle basic fields - if name in ["code", "display", "system", "version", "name", "inactive"]: + if name in [ + "code", + "display", + "system", + "version", + "name", + "inactive", + "result", + ]: response["metadata"][name] = parse_value_field(parameter) # Handle property fields @@ -108,5 +133,5 @@ def parse_fhir_parameter_output(parameters: list[dict[str, Any]]) -> dict[str, A elif name == "match": if "match" not in response: response["match"] = [] - response["match"].append(parse_fhir_parameter_output(parameter["part"])) + response["match"].append(parse_fhir_match_part(parameter["part"])) return response diff --git a/care/emr/migrations/0064_alter_servicerequest_category_and_more.py b/care/emr/migrations/0064_alter_servicerequest_category_and_more.py new file mode 100644 index 0000000000..56122c3fa5 --- /dev/null +++ b/care/emr/migrations/0064_alter_servicerequest_category_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.1 on 2025-01-05 13:22 + +import care.emr.models.organization +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0063_diagnosticreport_created_by_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='servicerequest', + name='category', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='servicerequest', + name='encounter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.encounter'), + ), + migrations.AlterField( + model_name='servicerequest', + name='location', + field=models.UUIDField(blank=True, null=True, verbose_name=care.emr.models.organization.Organization), + ), + migrations.AlterField( + model_name='servicerequest', + name='note', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='servicerequest', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.patient'), + ), + ] diff --git a/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py b/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py new file mode 100644 index 0000000000..d9355adfd9 --- /dev/null +++ b/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1.1 on 2025-01-05 16:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0064_alter_servicerequest_category_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='diagnosticreport', + name='encounter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.encounter'), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='note', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='diagnosticreport', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.patient'), + ), + migrations.AlterField( + model_name='specimen', + name='note', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='specimen', + name='subject', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.patient'), + ), + ] diff --git a/care/emr/models/diagnostic_report.py b/care/emr/models/diagnostic_report.py index a1501059de..dfe6e78032 100644 --- a/care/emr/models/diagnostic_report.py +++ b/care/emr/models/diagnostic_report.py @@ -1,11 +1,11 @@ from django.db import models from care.emr.models.base import EMRBaseModel +from care.emr.models.encounter import Encounter from care.emr.models.observation import Observation +from care.emr.models.patient import Patient from care.emr.models.service_request import ServiceRequest from care.emr.models.specimen import Specimen -from care.facility.models.patient import PatientRegistration -from care.facility.models.patient_consultation import PatientConsultation from care.users.models import User @@ -19,12 +19,12 @@ class DiagnosticReport(EMRBaseModel): ServiceRequest, on_delete=models.CASCADE, related_name="diagnostic_report" ) # TODO: Make it GenericForeignKey when needed subject = models.ForeignKey( - PatientRegistration, + Patient, on_delete=models.CASCADE, related_name="diagnostic_report", ) encounter = models.ForeignKey( - PatientConsultation, + Encounter, on_delete=models.CASCADE, related_name="diagnostic_report", ) @@ -56,5 +56,5 @@ class DiagnosticReport(EMRBaseModel): media = models.JSONField(default=list, null=True, blank=True) - note = models.JSONField(default=list, null=True, blank=True) + note = models.TextField(null=True, blank=True) conclusion = models.TextField(null=True, blank=True) diff --git a/care/emr/models/service_request.py b/care/emr/models/service_request.py index 8bf61a2e4e..1505634f55 100644 --- a/care/emr/models/service_request.py +++ b/care/emr/models/service_request.py @@ -1,8 +1,9 @@ from django.db import models from care.emr.models.base import EMRBaseModel -from care.facility.models.patient import PatientRegistration -from care.facility.models.patient_consultation import PatientConsultation +from care.emr.models.encounter import Encounter +from care.emr.models.organization import Organization +from care.emr.models.patient import Patient from care.users.models import User @@ -11,24 +12,24 @@ class ServiceRequest(EMRBaseModel): intent = models.CharField(max_length=100, null=True, blank=True) priority = models.CharField(max_length=100, null=True, blank=True) - category = models.JSONField(null=True, blank=True) + category = models.CharField(max_length=100, null=True, blank=True) code = models.JSONField(default=dict, null=False, blank=False) do_not_perform = models.BooleanField(default=False) subject = models.ForeignKey( - PatientRegistration, + Patient, on_delete=models.CASCADE, related_name="service_request", ) encounter = models.ForeignKey( - PatientConsultation, + Encounter, on_delete=models.CASCADE, related_name="service_request", ) occurrence_datetime = models.DateTimeField(null=True, blank=True) - occurrence_timing = models.JSONField(default=dict, null=True, blank=True) + occurrence_timing = models.JSONField(null=True, blank=True) as_needed = models.BooleanField(default=False) as_needed_for = models.JSONField(null=True, blank=True) @@ -37,11 +38,9 @@ class ServiceRequest(EMRBaseModel): User, on_delete=models.CASCADE, related_name="requested_service_request" ) - location = models.UUIDField( - null=True, blank=True - ) # TODO: Make this a foreign key of emr.Organization once it is created + location = models.UUIDField(Organization, null=True, blank=True) - note = models.JSONField(default=list, null=True, blank=True) + note = models.TextField(null=True, blank=True) patient_instruction = models.TextField(null=True, blank=True) replaces = models.ForeignKey( diff --git a/care/emr/models/specimen.py b/care/emr/models/specimen.py index 42e0f04d68..7c43bbbf13 100644 --- a/care/emr/models/specimen.py +++ b/care/emr/models/specimen.py @@ -1,8 +1,8 @@ from django.db import models from care.emr.models.base import EMRBaseModel +from care.emr.models.patient import Patient from care.emr.models.service_request import ServiceRequest -from care.facility.models.patient import PatientRegistration from care.users.models import User @@ -15,7 +15,7 @@ class Specimen(EMRBaseModel): type = models.JSONField(default=dict, null=False, blank=False) subject = models.ForeignKey( - PatientRegistration, + Patient, on_delete=models.CASCADE, related_name="specimen", ) @@ -54,6 +54,6 @@ class Specimen(EMRBaseModel): processing = models.JSONField(default=list, null=True, blank=True) - note = models.JSONField(default=list, null=True, blank=True) + note = models.TextField(null=True, blank=True) parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 0d85fc8de6..2911863f31 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -3,7 +3,7 @@ from pydantic import UUID4, BaseModel, Field, field_validator -from care.emr.fhir.schema.base import Annotation, Coding, Period +from care.emr.fhir.schema.base import Coding, Period from care.emr.models.diagnostic_report import DiagnosticReport from care.emr.models.service_request import ServiceRequest from care.emr.models.specimen import Specimen @@ -13,36 +13,21 @@ CARE_DIAGNOSTIC_REPORT_CATEGORY_VALUESET, CARE_DIAGNOSTIC_REPORT_CODE_VALUESET, ) +from care.emr.resources.encounter.spec import EncounterRetrieveSpec from care.emr.resources.observation.spec import ObservationSpec -from care.emr.resources.service_request.spec import ServiceRequestReadSpec -from care.emr.resources.specimen.spec import SpecimenReadSpec -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.api.serializers.patient_consultation import ( - PatientConsultationSerializer, -) -from care.users.api.serializers.user import UserBaseMinimumSerializer +from care.emr.resources.patient.spec import PatientRetrieveSpec +from care.emr.resources.service_request.spec import ServiceRequestRetrieveSpec +from care.emr.resources.specimen.spec import SpecimenRetrieveSpec +from care.emr.resources.user.spec import UserSpec from care.users.models import User -class BaseDiagnosticReportSpec(EMRResource): - __model__ = DiagnosticReport - __exclude__ = [ - "subject", - "based_on", - "encounter", - "performer", - "results_interpreter", - ] - id: UUID4 = None - - class DiagnosticReportMedia(EMRResource): comment: str | None = Field( default=None, description="A description or comment about the media file", ) link: UUID4 = Field( - ..., description="References the FileUpload object that contains the media file", ) @@ -61,7 +46,20 @@ class StatusChoices(str, Enum): unknown = "unknown" -class DiagnosticReportSpec(BaseDiagnosticReportSpec): +class DiagnosticReportSpec(EMRResource): + __model__ = DiagnosticReport + __exclude__ = [ + "subject", + "based_on", + "encounter", + "performer", + "results_interpreter", + "specimen", + "result", + ] + + id: UUID4 = None + status: StatusChoices = Field( default=StatusChoices.registered, description="Indicates the status of the report, used internally to track the lifecycle of the report", @@ -79,7 +77,6 @@ class DiagnosticReportSpec(BaseDiagnosticReportSpec): ) based_on: UUID4 = Field( - ..., description="The resource that this report is based on, this can be a service request, a medication request, or other resource", ) subject: UUID4 = Field( @@ -123,8 +120,8 @@ class DiagnosticReportSpec(BaseDiagnosticReportSpec): description="Media files associated with the report", ) - note: list[Annotation] = Field( - default=[], + note: str | None = Field( + default=None, description="Comments made about the service request by the requester, performer, subject, or other participants", ) conclusion: str | None = Field( @@ -146,6 +143,8 @@ def validate_code(cls, value: str): "code", cls.model_fields["code"].json_schema_extra["slug"], value ) + +class DiagnosticReportCreateSpec(DiagnosticReportSpec): def perform_extra_deserialization(self, is_update, obj): if not is_update: obj.based_on = ServiceRequest.objects.get(external_id=self.based_on) @@ -173,55 +172,64 @@ def perform_extra_deserialization(self, is_update, obj): obj.specimen.set(specimens) -class DiagnosticReportReadSpec(BaseDiagnosticReportSpec): - __exclude__ = [] - - status: str +class DiagnosticReportUpdateSpec(DiagnosticReportSpec): + class Config: + exclude_unset = True - category: Coding | None - code: Coding - based_on: ServiceRequestReadSpec - subject: dict - encounter: dict - - performer: dict | None - results_interpreter: dict | None +class DiagnosticReportListSpec(DiagnosticReportSpec): + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id - issued: datetime | None - effective_period: Period | None - specimen: list[SpecimenReadSpec] +class DiagnosticReportRetrieveSpec(DiagnosticReportSpec): + based_on: ServiceRequestRetrieveSpec + subject: PatientRetrieveSpec + encounter: EncounterRetrieveSpec + performer: UserSpec | None + results_interpreter: UserSpec | None + specimen: list[SpecimenRetrieveSpec] result: list[ObservationSpec] - media: list[DiagnosticReportMedia] - - note: list[Annotation] - conclusion: str | None + created_by: UserSpec | None + updated_by: UserSpec | None @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id - mapping["subject"] = PatientDetailSerializer(obj.subject).data - mapping["encounter"] = PatientConsultationSerializer(obj.encounter).data - + if obj.based_on: + mapping["based_on"] = ServiceRequestRetrieveSpec.serialize( + obj.based_on + ).to_json() + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() + if obj.encounter: + mapping["encounter"] = EncounterRetrieveSpec.serialize( + obj.encounter + ).to_json() if obj.performer: - mapping["performer"] = UserBaseMinimumSerializer(obj.performer).data - + mapping["performer"] = UserSpec.serialize(obj.performer).to_json() if obj.results_interpreter: - mapping["results_interpreter"] = UserBaseMinimumSerializer( + mapping["results_interpreter"] = UserSpec.serialize( obj.results_interpreter - ).data - - mapping["specimen"] = [ - SpecimenReadSpec.serialize(specimen).model_dump(exclude=["meta"]) - for specimen in obj.specimen.all() - ] - mapping["result"] = [ - ObservationSpec.serialize(observation).model_dump(exclude=["meta"]) - for observation in obj.result.all() - ] + ).to_json() + if len(obj.specimen): + mapping["specimen"] = [ + SpecimenRetrieveSpec.serialize(specimen).to_json() + for specimen in obj.specimen.all() + ] + if len(obj.result): + mapping["result"] = [ + ObservationSpec.serialize(observation).to_json() + for observation in obj.result.all() + ] + + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() + if obj.updated_by: + mapping["updated_by"] = UserSpec.serialize(obj.updated_by).to_json() class DiagnosticReportObservationRequest(BaseModel): @@ -233,14 +241,12 @@ class DiagnosticReportObservationRequest(BaseModel): class DiagnosticReportVerifyRequest(BaseModel): is_approved: bool = Field( - ..., description="Indicates whether the diagnostic report is approved or rejected", ) class DiagnosticReportReviewRequest(BaseModel): is_approved: bool = Field( - ..., description="Indicates whether the diagnostic report is approved or rejected", ) conclusion: str | None = Field( diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index c43816b674..e0662b4cd9 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -1,33 +1,27 @@ +from __future__ import annotations + from datetime import UTC, datetime from enum import Enum from pydantic import UUID4, Field, field_validator -from care.emr.fhir.schema.base import Annotation, Coding, Timing +from care.emr.fhir.schema.base import Coding, Timing +from care.emr.models.encounter import Encounter from care.emr.models.service_request import ServiceRequest from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource +from care.emr.resources.encounter.spec import EncounterRetrieveSpec +from care.emr.resources.organization.spec import OrganizationRetrieveSpec +from care.emr.resources.patient.spec import PatientRetrieveSpec from care.emr.resources.service_request.valueset import ( CARE_LAB_ORDER_CODE_VALUESET, CARE_MEDICATION_AS_NEEDED_REASON_VALUESET, - CARE_SERVICE_REQUEST_CATEGORY_VALUESET, -) -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.api.serializers.patient_consultation import ( - PatientConsultationSerializer, ) -from care.facility.models.ambulance import User -from care.facility.models.patient_consultation import PatientConsultation -from care.users.api.serializers.user import UserBaseMinimumSerializer +from care.emr.resources.user.spec import UserSpec +from care.users.models import User -class BaseServiceRequestSpec(EMRResource): - __model__ = ServiceRequest - __exclude__ = ["subject", "encounter", "requester"] - id: UUID4 = None - - -class StatusChoices(str, Enum): +class ServiceRequestStatusChoices(str, Enum): draft = "draft" active = "active" on_hold = "on-hold" @@ -37,41 +31,52 @@ class StatusChoices(str, Enum): unknown = "unknown" -class IntentChoices(str, Enum): +class ServiceRequestIntentChoices(str, Enum): proposal = "proposal" plan = "plan" directive = "directive" order = "order" -class PriorityChoices(str, Enum): +class ServiceRequestPriorityChoices(str, Enum): routine = "routine" urgent = "urgent" asap = "asap" stat = "stat" -class ServiceRequestSpec(BaseServiceRequestSpec): - status: StatusChoices = Field( - default=StatusChoices.draft, +class ServiceRequestCategoryChoices(str, Enum): + laboratory_procedure = "laboratory_procedure" + imaging = "imaging" + counselling = "counselling" + education = "education" + surgical_procedure = "surgical_procedure" + + +class ServiceRequestSpec(EMRResource): + __model__ = ServiceRequest + __exclude__ = ["subject", "encounter", "requester", "location", "replaces"] + + id: UUID4 | None = None + + status: ServiceRequestStatusChoices = Field( + default=ServiceRequestStatusChoices.draft, description="Indicates the status of the request, used internally to track the lifecycle of the request", ) - intent: IntentChoices = Field( - default=IntentChoices.order, + intent: ServiceRequestIntentChoices = Field( + default=ServiceRequestIntentChoices.order, description="Indicates the level of authority/intentionality associated with the request", ) - priority: PriorityChoices = Field( - default=PriorityChoices.routine, + priority: ServiceRequestPriorityChoices = Field( + default=ServiceRequestPriorityChoices.routine, description="Indicates the urgency of the request", ) - category: Coding | None = Field( + category: ServiceRequestCategoryChoices | None = Field( default=None, - json_schema_extra={"slug": CARE_SERVICE_REQUEST_CATEGORY_VALUESET.slug}, description="Identifies the broad category of service that is to be performed", ) code: Coding = Field( - ..., json_schema_extra={ "slug": CARE_LAB_ORDER_CODE_VALUESET.slug }, # TODO: consider using a broader value set (https://build.fhir.org/valueset-procedure-code.html) @@ -84,11 +89,9 @@ class ServiceRequestSpec(BaseServiceRequestSpec): ) subject: UUID4 = Field( - ..., description="The patient for whom the service/procedure is being requested", ) encounter: UUID4 = Field( - ..., description="The encounter within which this service request was created", ) @@ -115,7 +118,6 @@ class ServiceRequestSpec(BaseServiceRequestSpec): description="The date when the request was made", ) requester: UUID4 = Field( - ..., description="The individual who initiated the request and has responsibility for its activation", ) @@ -124,12 +126,12 @@ class ServiceRequestSpec(BaseServiceRequestSpec): description="The location where the service will be performed", ) - note: list[Annotation] = Field( - default=[], + note: str | None = Field( + default=None, description="Comments made about the service request by the requester, performer, subject, or other participants", ) - patient_instruction: str = Field( - default="", + patient_instruction: str | None = Field( + default=None, description="Instructions for the patient on how the service should be performed", ) @@ -138,13 +140,6 @@ class ServiceRequestSpec(BaseServiceRequestSpec): description="The request that is being replaced by this request, used in the case of re-orders", ) - @field_validator("category") - @classmethod - def validate_category(cls, value: str): - return validate_valueset( - "category", cls.model_fields["category"].json_schema_extra["slug"], value - ) - @field_validator("code") @classmethod def validate_code(cls, value: str): @@ -161,57 +156,58 @@ def validate_as_needed_for(cls, value: str): value, ) + +class ServiceRequestCreateSpec(ServiceRequestSpec): def perform_extra_deserialization(self, is_update, obj): if not is_update: - obj.encounter = PatientConsultation.objects.get(external_id=self.encounter) + obj.encounter = Encounter.objects.get(external_id=self.encounter) obj.subject = obj.encounter.patient obj.requester = User.objects.get(external_id=self.requester) -class ServiceRequestReadSpec(BaseServiceRequestSpec): - __exclude__ = [] - external_id: ( - UUID4 # TODO: remove this field and do a model dump when accessing any models - ) - - status: str - intent: str - priority: str - - category: Coding | None - code: Coding - - do_not_perform: bool +class ServiceRequestUpdateSpec(ServiceRequestSpec): + class Config: + exclude_unset = True - subject: dict - encounter: dict - occurrence_datetime: datetime | None - occurrence_timing: Timing | None - as_needed: bool - as_needed_for: Coding | None - - authored_on: datetime - requester: dict +class ServiceRequestListSpec(ServiceRequestSpec): + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id - location: dict | None - note: list[Annotation] - patient_instruction: str +class ServiceRequestRetrieveSpec(ServiceRequestSpec): + subject: PatientRetrieveSpec + encounter: EncounterRetrieveSpec + requester: UserSpec + location: OrganizationRetrieveSpec | None + replaces: ServiceRequestRetrieveSpec | None - replaces: dict | None + created_by: UserSpec | None + updated_by: UserSpec | None @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id - mapping["subject"] = PatientDetailSerializer(obj.subject).data - mapping["encounter"] = PatientConsultationSerializer(obj.encounter).data - - mapping["requester"] = UserBaseMinimumSerializer(obj.requester).data - - mapping["replaces"] = ( - ServiceRequestReadSpec.serialize(obj.replaces).model_dump(exclude=["meta"]) - if obj.replaces - else None - ) + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() + if obj.encounter: + mapping["encounter"] = EncounterRetrieveSpec.serialize( + obj.encounter + ).to_json() + if obj.requester: + mapping["requester"] = UserSpec.serialize(obj.requester).to_json() + if obj.location: + mapping["location"] = OrganizationRetrieveSpec.serialize( + obj.location + ).to_json() + if obj.replaces: + mapping["replaces"] = ServiceRequestRetrieveSpec.serialize( + obj.replaces + ).to_json() + + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() + if obj.updated_by: + mapping["updated_by"] = UserSpec.serialize(obj.updated_by).to_json() diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 3f96f2aa98..63996c06bb 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import UTC, datetime from enum import Enum @@ -8,20 +10,14 @@ from care.emr.models.specimen import Specimen from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource -from care.emr.resources.service_request.spec import ServiceRequestReadSpec +from care.emr.resources.patient.spec import PatientRetrieveSpec +from care.emr.resources.service_request.spec import ServiceRequestRetrieveSpec from care.emr.resources.specimen.valueset import ( CARE_SPECIMEN_CONDITION_VALUESET, CARE_SPECIMEN_PROCESSING_METHOD_VALUESET, CARE_SPECIMEN_TYPE_VALUESET, ) -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.users.api.serializers.user import UserBaseMinimumSerializer - - -class BaseSpecimenSpec(EMRResource): - __model__ = Specimen - __exclude__ = ["subject", "request"] - id: UUID4 = None +from care.emr.resources.user.spec import UserSpec class SpecimenProcessingSpec(EMRResource): @@ -60,7 +56,12 @@ class StatusChoices(str, Enum): entered_in_error = "entered-in-error" -class SpecimenSpec(BaseSpecimenSpec): +class SpecimenSpec(EMRResource): + __model__ = Specimen + __exclude__ = ["subject", "request"] + + id: UUID4 = None + identifier: str | None = Field( default=None, description="The unique identifier assigned to the specimen after collection", @@ -76,17 +77,14 @@ class SpecimenSpec(BaseSpecimenSpec): ) type: Coding = Field( - ..., json_schema_extra={"slug": CARE_SPECIMEN_TYPE_VALUESET.slug}, description="Indicates the type of specimen being collected", ) subject: UUID4 = Field( - ..., description="The patient from whom the specimen is collected", ) request: UUID4 = Field( - ..., description="The service request that initiated the collection of the specimen", ) @@ -128,8 +126,8 @@ class SpecimenSpec(BaseSpecimenSpec): description="The processing steps that have been performed on the specimen", ) - note: list[Annotation] = Field( - default=[], + note: str | None = Field( + default=None, description="Comments made about the service request by the requester, performer, subject, or other participants", ) @@ -152,57 +150,59 @@ def validate_condition(cls, value: str): "condition", cls.model_fields["condition"].json_schema_extra["slug"], value ) + +class SpecimenCreateSpec(SpecimenSpec): def perform_extra_deserialization(self, is_update, obj): if not is_update: obj.request = ServiceRequest.objects.get(external_id=self.request) obj.subject = obj.request.subject -class SpecimenReadSpec(BaseSpecimenSpec): - __exclude__ = [] - - identifier: str | None - accession_identifier: str | None - - status: StatusChoices | None - - type: Coding - - subject: dict - request: ServiceRequestReadSpec - - collected_by: dict | None - collected_at: datetime | None - - dispatched_by: dict | None - dispatched_at: datetime | None +class SpecimenUpdateSpec(SpecimenSpec): + class Config: + exclude_unset = True - received_by: dict | None - received_at: datetime | None - condition: Coding | None +class SpecimenListSpec(SpecimenSpec): + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id - processing: list[SpecimenProcessingSpec] - note: list[Annotation] +class SpecimenRetrieveSpec(SpecimenSpec): + request: ServiceRequestRetrieveSpec + subject: PatientRetrieveSpec + collected_by: UserSpec | None + dispatched_by: UserSpec | None + received_by: UserSpec | None + parent: SpecimenRetrieveSpec | None - parent: dict | None + created_by: UserSpec | None + updated_by: UserSpec | None @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id - mapping["subject"] = PatientDetailSerializer(obj.subject).data - - mapping["collected_by"] = UserBaseMinimumSerializer(obj.collected_by).data - mapping["dispatched_by"] = UserBaseMinimumSerializer(obj.dispatched_by).data - mapping["received_by"] = UserBaseMinimumSerializer(obj.received_by).data - - mapping["parent"] = ( - SpecimenReadSpec().serialize(obj.parent).model_dump(exclude=["meta"]) - if obj.parent - else None - ) + if obj.request: + mapping["request"] = ( + ServiceRequestRetrieveSpec().serialize(obj.request).to_json() + ) + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() + if obj.collected_by: + mapping["collected_by"] = UserSpec.serialize(obj.collected_by).to_json() + if obj.dispatched_by: + mapping["dispatched_by"] = UserSpec.serialize(obj.dispatched_by).to_json() + if obj.received_by: + mapping["received_by"] = UserSpec.serialize(obj.received_by).to_json() + if obj.parent: + mapping["parent"] = SpecimenRetrieveSpec.serialize(obj.parent).to_json() + + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() + if obj.updated_by: + mapping["updated_by"] = UserSpec.serialize(obj.updated_by).to_json() class SpecimenCollectRequest(BaseModel): @@ -214,7 +214,6 @@ class SpecimenCollectRequest(BaseModel): class SpecimenSendToLabRequest(BaseModel): lab: UUID4 = Field( - ..., description="The laboratory to which the specimen is being sent", ) @@ -247,6 +246,5 @@ def validate_condition(cls, value: str): class SpecimenProcessRequest(BaseModel): process: list[SpecimenProcessingSpec] = Field( - ..., description="The processing steps that have been performed on the specimen", ) diff --git a/care/emr/signals/auto_create_specimen.py b/care/emr/signals/auto_create_specimen.py index 28a37c4e44..9628128044 100644 --- a/care/emr/signals/auto_create_specimen.py +++ b/care/emr/signals/auto_create_specimen.py @@ -5,7 +5,7 @@ from care.emr.fhir.resources.code_concept import CodeConceptResource from care.emr.fhir.resources.concept_map import ConceptMapResource from care.emr.models.service_request import ServiceRequest -from care.emr.resources.specimen.spec import SpecimenSpec +from care.emr.resources.specimen.spec import SpecimenCreateSpec @receiver(post_save, sender=ServiceRequest) @@ -25,7 +25,9 @@ def create_specimen(sender, instance: ServiceRequest, created: bool, **kwargs): .get() ) - loinc_specimen_code = code_concept.property.get("system-core", {}).get("code") + loinc_specimen_code = ( + code_concept.get("properties", {}).get("system-core", {}).get("code") + ) concept_map = ( ConceptMapResource() .filter(system="http://loinc.org", code=loinc_specimen_code) @@ -47,7 +49,7 @@ def create_specimen(sender, instance: ServiceRequest, created: bool, **kwargs): specimen_coding = specimen_matches[0].concept - specimen = SpecimenSpec( + specimen = SpecimenCreateSpec( type={ "code": specimen_coding.code, "display": specimen_coding.display, From 8515c24c9bfe5c62bfac8748e61ae9428f009d70 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 5 Jan 2025 23:02:18 +0530 Subject: [PATCH 12/32] preserve default values if set manually --- care/emr/resources/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/emr/resources/base.py b/care/emr/resources/base.py index 9f5f4fa46a..e32f20df86 100644 --- a/care/emr/resources/base.py +++ b/care/emr/resources/base.py @@ -73,7 +73,7 @@ def de_serialize(self, obj=None): obj = self.__model__() database_fields = self.get_database_mapping() meta = {} - dump = self.model_dump(mode="json", exclude_defaults=True) + dump = self.model_dump(mode="json", exclude_unset=True) for field in dump: if ( field in database_fields From 45cee9d2795834cb25e082e488360f02b282debf Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Jan 2025 00:30:33 +0530 Subject: [PATCH 13/32] update condition type in specimen --- care/emr/resources/specimen/spec.py | 37 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 63996c06bb..94f39efb35 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -5,7 +5,7 @@ from pydantic import UUID4, BaseModel, Field, field_validator -from care.emr.fhir.schema.base import Annotation, Coding +from care.emr.fhir.schema.base import CodeableConcept, Coding from care.emr.models.service_request import ServiceRequest from care.emr.models.specimen import Specimen from care.emr.registries.care_valueset.care_valueset import validate_valueset @@ -115,7 +115,7 @@ class SpecimenSpec(EMRResource): description="The datetime at which the specimen was received at the laboratory", ) - condition: Coding | None = Field( + condition: list[CodeableConcept] | None = Field( default=None, json_schema_extra={"slug": CARE_SPECIMEN_CONDITION_VALUESET.slug}, description="The condition of the specimen while received at the laboratory", @@ -145,10 +145,23 @@ def validate_type(cls, value: str): @field_validator("condition") @classmethod - def validate_condition(cls, value: str): - return validate_valueset( - "condition", cls.model_fields["condition"].json_schema_extra["slug"], value - ) + def validate_condition(cls, concepts: list[CodeableConcept]): + if not concepts: + return concepts + + for concept in concepts: + codings = concept.coding + if not codings: + continue + + for coding in codings: + validate_valueset( + "condition", + cls.model_fields["condition"].json_schema_extra["slug"], + coding.code, + ) + + return concepts class SpecimenCreateSpec(SpecimenSpec): @@ -224,24 +237,20 @@ class SpecimenReceiveAtLabRequest(BaseModel): description="The identifier assigned to the specimen by the laboratory", ) - condition: Coding | None = Field( + condition: list[CodeableConcept] | None = Field( default=None, description="The condition of the specimen while received at the laboratory", ) - note: Annotation = Field( + note: str | None = Field( default=None, description="Comments made about the specimen while received at the laboratory", ) @field_validator("condition") @classmethod - def validate_condition(cls, value: str): - return validate_valueset( - "condition", - SpecimenSpec.model_fields["condition"].json_schema_extra["slug"], - value, - ) + def validate_condition(cls, value: CodeableConcept): + return SpecimenSpec.validate_condition(value) class SpecimenProcessRequest(BaseModel): From ac0cbc411d8a0848391fa8ecd6b1ec5b2f4b3b61 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Jan 2025 02:04:46 +0530 Subject: [PATCH 14/32] fixed required field issues while using specs in other specs --- care/emr/resources/diagnostic_report/spec.py | 26 +++++++-------- care/emr/resources/service_request/spec.py | 23 +++++++------ care/emr/resources/specimen/spec.py | 35 +++++++++++++------- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 2911863f31..26abaea041 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -172,29 +172,29 @@ def perform_extra_deserialization(self, is_update, obj): obj.specimen.set(specimens) -class DiagnosticReportUpdateSpec(DiagnosticReportSpec): +class DiagnosticReportUpdateSpec(DiagnosticReportCreateSpec): class Config: exclude_unset = True class DiagnosticReportListSpec(DiagnosticReportSpec): + based_on: ServiceRequestRetrieveSpec = {} + subject: PatientRetrieveSpec = {} + encounter: EncounterRetrieveSpec = {} + performer: UserSpec | None = None + results_interpreter: UserSpec | None = None + specimen: list[SpecimenRetrieveSpec] = [] + result: list[ObservationSpec] = [] + + created_by: UserSpec | None = None + updated_by: UserSpec | None = None + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id -class DiagnosticReportRetrieveSpec(DiagnosticReportSpec): - based_on: ServiceRequestRetrieveSpec - subject: PatientRetrieveSpec - encounter: EncounterRetrieveSpec - performer: UserSpec | None - results_interpreter: UserSpec | None - specimen: list[SpecimenRetrieveSpec] - result: list[ObservationSpec] - - created_by: UserSpec | None - updated_by: UserSpec | None - +class DiagnosticReportRetrieveSpec(DiagnosticReportListSpec): @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index e0662b4cd9..5e8d85ca75 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -165,27 +165,28 @@ def perform_extra_deserialization(self, is_update, obj): obj.requester = User.objects.get(external_id=self.requester) -class ServiceRequestUpdateSpec(ServiceRequestSpec): +class ServiceRequestUpdateSpec(ServiceRequestCreateSpec): class Config: exclude_unset = True class ServiceRequestListSpec(ServiceRequestSpec): + code: Coding = {} + subject: PatientRetrieveSpec = {} + encounter: EncounterRetrieveSpec = {} + requester: UserSpec = {} + location: OrganizationRetrieveSpec | None = None + replaces: ServiceRequestRetrieveSpec | None = None + + created_by: UserSpec | None = None + updated_by: UserSpec | None = None + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id -class ServiceRequestRetrieveSpec(ServiceRequestSpec): - subject: PatientRetrieveSpec - encounter: EncounterRetrieveSpec - requester: UserSpec - location: OrganizationRetrieveSpec | None - replaces: ServiceRequestRetrieveSpec | None - - created_by: UserSpec | None - updated_by: UserSpec | None - +class ServiceRequestRetrieveSpec(ServiceRequestListSpec): @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 94f39efb35..ced05b71e0 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -11,7 +11,10 @@ from care.emr.registries.care_valueset.care_valueset import validate_valueset from care.emr.resources.base import EMRResource from care.emr.resources.patient.spec import PatientRetrieveSpec -from care.emr.resources.service_request.spec import ServiceRequestRetrieveSpec +from care.emr.resources.service_request.spec import ( + ServiceRequestListSpec, + ServiceRequestRetrieveSpec, +) from care.emr.resources.specimen.valueset import ( CARE_SPECIMEN_CONDITION_VALUESET, CARE_SPECIMEN_PROCESSING_METHOD_VALUESET, @@ -171,28 +174,36 @@ def perform_extra_deserialization(self, is_update, obj): obj.subject = obj.request.subject -class SpecimenUpdateSpec(SpecimenSpec): +class SpecimenUpdateSpec(SpecimenCreateSpec): class Config: exclude_unset = True class SpecimenListSpec(SpecimenSpec): + type: Coding = {} + request: ServiceRequestRetrieveSpec = {} + subject: PatientRetrieveSpec = {} + collected_by: UserSpec | None = None + dispatched_by: UserSpec | None = None + received_by: UserSpec | None = None + parent: SpecimenRetrieveSpec | None = None + + created_by: UserSpec | None = None + updated_by: UserSpec | None = None + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id + if obj.request: + mapping["request"] = ( + ServiceRequestListSpec().serialize(obj.request).to_json() + ) + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() -class SpecimenRetrieveSpec(SpecimenSpec): - request: ServiceRequestRetrieveSpec - subject: PatientRetrieveSpec - collected_by: UserSpec | None - dispatched_by: UserSpec | None - received_by: UserSpec | None - parent: SpecimenRetrieveSpec | None - - created_by: UserSpec | None - updated_by: UserSpec | None +class SpecimenRetrieveSpec(SpecimenListSpec): @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id From 86f215e39ed686f8a5da8fd26a75b4bead866c9f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Jan 2025 12:26:46 +0530 Subject: [PATCH 15/32] updated facility type filter to support multiple types --- care/facility/api/viewsets/facility.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 979963bdd5..2bfb5ebc4f 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -22,11 +22,7 @@ FacilitySerializer, FacilitySpokeSerializer, ) -from care.facility.models import ( - Facility, - FacilityCapacity, - HospitalDoctors, -) +from care.facility.models import Facility, FacilityCapacity, HospitalDoctors from care.facility.models.facility import FacilityHubSpoke from care.users.models import User from care.utils.file_uploads.cover_image import delete_cover_image @@ -43,7 +39,7 @@ def filter(self, qs, value): class FacilityFilter(filters.FilterSet): name = filters.CharFilter(field_name="name", lookup_expr="icontains") - facility_type = filters.NumberFilter(field_name="facility_type") + facility_type = filters.BaseInFilter(field_name="facility_type", lookup_expr="in") geo_organization = GeoOrganizationFilter() From 79db03410c7682ef8f5bb3034464f840426b6e63 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Jan 2025 12:27:32 +0530 Subject: [PATCH 16/32] serialize subject, encounter and requester in service request list spec --- care/emr/resources/service_request/spec.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index 5e8d85ca75..627a8dbbce 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -185,6 +185,15 @@ class ServiceRequestListSpec(ServiceRequestSpec): def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() + if obj.encounter: + mapping["encounter"] = EncounterRetrieveSpec.serialize( + obj.encounter + ).to_json() + if obj.requester: + mapping["requester"] = UserSpec.serialize(obj.requester).to_json() + class ServiceRequestRetrieveSpec(ServiceRequestListSpec): @classmethod From 703d651bc6c518b5bfdbc6009be0e01c26d4926e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Jan 2025 13:17:40 +0530 Subject: [PATCH 17/32] cleaned up migrations --- ...ervicerequest_specimen_diagnosticreport.py | 118 +++++++++++++++++ care/emr/migrations/0005_servicerequest.py | 50 ------- care/emr/migrations/0006_specimen.py | 42 ------ ..._specimen_0017_alter_questionnaire_slug.py | 14 -- care/emr/migrations/0019_diagnosticreport.py | 48 ------- ...st_location_specimen_condition_and_more.py | 122 ------------------ .../migrations/0062_merge_20250104_1141.py | 14 -- ...63_diagnosticreport_created_by_and_more.py | 46 ------- ..._alter_servicerequest_category_and_more.py | 40 ------ ...ter_diagnosticreport_encounter_and_more.py | 39 ------ 10 files changed, 118 insertions(+), 415 deletions(-) create mode 100644 care/emr/migrations/0002_servicerequest_specimen_diagnosticreport.py delete mode 100644 care/emr/migrations/0005_servicerequest.py delete mode 100644 care/emr/migrations/0006_specimen.py delete mode 100644 care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py delete mode 100644 care/emr/migrations/0019_diagnosticreport.py delete mode 100644 care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py delete mode 100644 care/emr/migrations/0062_merge_20250104_1141.py delete mode 100644 care/emr/migrations/0063_diagnosticreport_created_by_and_more.py delete mode 100644 care/emr/migrations/0064_alter_servicerequest_category_and_more.py delete mode 100644 care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py diff --git a/care/emr/migrations/0002_servicerequest_specimen_diagnosticreport.py b/care/emr/migrations/0002_servicerequest_specimen_diagnosticreport.py new file mode 100644 index 0000000000..ed184df195 --- /dev/null +++ b/care/emr/migrations/0002_servicerequest_specimen_diagnosticreport.py @@ -0,0 +1,118 @@ +# Generated by Django 5.1.3 on 2025-01-06 07:47 + +import care.emr.models.organization +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ServiceRequest', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('intent', models.CharField(blank=True, max_length=100, null=True)), + ('priority', models.CharField(blank=True, max_length=100, null=True)), + ('category', models.CharField(blank=True, max_length=100, null=True)), + ('code', models.JSONField(default=dict)), + ('do_not_perform', models.BooleanField(default=False)), + ('occurrence_datetime', models.DateTimeField(blank=True, null=True)), + ('occurrence_timing', models.JSONField(blank=True, null=True)), + ('as_needed', models.BooleanField(default=False)), + ('as_needed_for', models.JSONField(blank=True, null=True)), + ('authored_on', models.DateTimeField(blank=True, null=True)), + ('location', models.UUIDField(blank=True, null=True, verbose_name=care.emr.models.organization.Organization)), + ('note', models.TextField(blank=True, null=True)), + ('patient_instruction', models.TextField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.encounter')), + ('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), + ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_service_request', to=settings.AUTH_USER_MODEL)), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.patient')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Specimen', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('identifier', models.CharField(blank=True, max_length=100, null=True)), + ('accession_identifier', models.CharField(blank=True, max_length=100, null=True)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('type', models.JSONField(default=dict)), + ('collected_at', models.DateTimeField(blank=True, null=True)), + ('dispatched_at', models.DateTimeField(blank=True, null=True)), + ('received_at', models.DateTimeField(blank=True, null=True)), + ('condition', models.JSONField(blank=True, null=True)), + ('processing', models.JSONField(blank=True, default=list, null=True)), + ('note', models.TextField(blank=True, null=True)), + ('collected_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collected_specimen', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('dispatched_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dispatched_specimen', to=settings.AUTH_USER_MODEL)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.specimen')), + ('received_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_specimen', to=settings.AUTH_USER_MODEL)), + ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.servicerequest')), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.patient')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='DiagnosticReport', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('status', models.CharField(blank=True, max_length=100, null=True)), + ('category', models.JSONField(blank=True, null=True)), + ('code', models.JSONField(default=dict)), + ('issued', models.DateTimeField(blank=True, null=True)), + ('effective_period', models.JSONField(blank=True, null=True)), + ('media', models.JSONField(blank=True, default=list, null=True)), + ('note', models.TextField(blank=True, null=True)), + ('conclusion', models.TextField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.encounter')), + ('performer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='performed_diagnostic_report', to=settings.AUTH_USER_MODEL)), + ('result', models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.observation')), + ('results_interpreter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interpreted_diagnostic_report', to=settings.AUTH_USER_MODEL)), + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.patient')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ('based_on', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.servicerequest')), + ('specimen', models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.specimen')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/migrations/0005_servicerequest.py b/care/emr/migrations/0005_servicerequest.py deleted file mode 100644 index cbccb63c06..0000000000 --- a/care/emr/migrations/0005_servicerequest.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-28 19:52 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0004_questionnaire'), - ('facility', '0466_camera_presets'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ServiceRequest', - 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)), - ('history', models.JSONField(default=dict)), - ('meta', models.JSONField(default=dict)), - ('status', models.CharField(blank=True, max_length=100, null=True)), - ('intent', models.CharField(blank=True, max_length=100, null=True)), - ('priority', models.CharField(blank=True, max_length=100, null=True)), - ('category', models.JSONField(blank=True, null=True)), - ('code', models.JSONField(default=dict)), - ('do_not_perform', models.BooleanField(default=False)), - ('occurrence_datetime', models.DateTimeField(blank=True, null=True)), - ('occurrence_timing', models.JSONField(blank=True, default=dict, null=True)), - ('as_needed', models.BooleanField(default=False)), - ('as_needed_for', models.JSONField(blank=True, null=True)), - ('authored_on', models.DateTimeField(blank=True, null=True)), - ('note', models.JSONField(blank=True, default=list, null=True)), - ('patient_instruction', models.TextField(blank=True, null=True)), - ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientconsultation')), - ('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), - ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/care/emr/migrations/0006_specimen.py b/care/emr/migrations/0006_specimen.py deleted file mode 100644 index c29b76032a..0000000000 --- a/care/emr/migrations/0006_specimen.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-30 18:19 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0005_servicerequest'), - ('facility', '0466_camera_presets'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Specimen', - 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)), - ('history', models.JSONField(default=dict)), - ('meta', models.JSONField(default=dict)), - ('accession_identifier', models.CharField(blank=True, max_length=100, null=True)), - ('status', models.CharField(blank=True, max_length=100, null=True)), - ('type', models.JSONField(default=dict)), - ('collected_at', models.DateTimeField(blank=True, null=True)), - ('processing', models.JSONField(blank=True, default=list, null=True)), - ('note', models.JSONField(blank=True, default=list, null=True)), - ('collected_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), - ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py b/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py deleted file mode 100644 index 96fc5eb5e6..0000000000 --- a/care/emr/migrations/0018_merge_0006_specimen_0017_alter_questionnaire_slug.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.1 on 2024-12-08 10:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0006_specimen'), - ('emr', '0017_alter_questionnaire_slug'), - ] - - operations = [ - ] diff --git a/care/emr/migrations/0019_diagnosticreport.py b/care/emr/migrations/0019_diagnosticreport.py deleted file mode 100644 index fe04239e35..0000000000 --- a/care/emr/migrations/0019_diagnosticreport.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.1 on 2024-12-08 12:41 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0018_merge_0006_specimen_0017_alter_questionnaire_slug'), - ('facility', '0466_camera_presets'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='DiagnosticReport', - 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)), - ('history', models.JSONField(default=dict)), - ('meta', models.JSONField(default=dict)), - ('status', models.CharField(blank=True, max_length=100, null=True)), - ('category', models.JSONField(blank=True, null=True)), - ('code', models.JSONField(default=dict)), - ('issued', models.DateTimeField(blank=True, null=True)), - ('effective_period', models.JSONField(blank=True, null=True)), - ('media', models.JSONField(blank=True, default=list, null=True)), - ('conclusion', models.TextField(blank=True, null=True)), - ('note', models.JSONField(blank=True, default=list, null=True)), - ('based_on', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.servicerequest')), - ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientconsultation')), - ('performer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('result', models.ManyToManyField(blank=True, to='emr.observation')), - ('results_interpreter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('specimen', models.ManyToManyField(blank=True, to='emr.specimen')), - ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='facility.patientregistration')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py b/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py deleted file mode 100644 index e301687b2e..0000000000 --- a/care/emr/migrations/0020_servicerequest_location_specimen_condition_and_more.py +++ /dev/null @@ -1,122 +0,0 @@ -# Generated by Django 5.1.1 on 2024-12-08 21:18 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0019_diagnosticreport'), - ('facility', '0466_camera_presets'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='servicerequest', - name='location', - field=models.UUIDField(blank=True, null=True), - ), - migrations.AddField( - model_name='specimen', - name='condition', - field=models.JSONField(blank=True, null=True), - ), - migrations.AddField( - model_name='specimen', - name='dispatched_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='specimen', - name='dispatched_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dispatched_specimen', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='specimen', - name='identifier', - field=models.CharField(blank=True, max_length=100, null=True), - ), - migrations.AddField( - model_name='specimen', - name='parent', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.specimen'), - ), - migrations.AddField( - model_name='specimen', - name='received_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='specimen', - name='received_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_specimen', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='based_on', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.servicerequest'), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='encounter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='facility.patientconsultation'), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='performer', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='performed_diagnostic_report', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='result', - field=models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.observation'), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='results_interpreter', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interpreted_diagnostic_report', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='specimen', - field=models.ManyToManyField(blank=True, related_name='diagnostic_report', to='emr.specimen'), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='facility.patientregistration'), - ), - migrations.AlterField( - model_name='servicerequest', - name='encounter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='facility.patientconsultation'), - ), - migrations.AlterField( - model_name='servicerequest', - name='requester', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_service_request', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='servicerequest', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='facility.patientregistration'), - ), - migrations.AlterField( - model_name='specimen', - name='collected_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collected_specimen', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='specimen', - name='request', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.servicerequest'), - ), - migrations.AlterField( - model_name='specimen', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='facility.patientregistration'), - ), - ] diff --git a/care/emr/migrations/0062_merge_20250104_1141.py b/care/emr/migrations/0062_merge_20250104_1141.py deleted file mode 100644 index 8e4e3aea49..0000000000 --- a/care/emr/migrations/0062_merge_20250104_1141.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 5.1.1 on 2025-01-04 06:11 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0020_servicerequest_location_specimen_condition_and_more'), - ('emr', '0061_rename_resource_schedulableuserresource_user_and_more'), - ] - - operations = [ - ] diff --git a/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py b/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py deleted file mode 100644 index 5949f0e0c1..0000000000 --- a/care/emr/migrations/0063_diagnosticreport_created_by_and_more.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.1.1 on 2025-01-04 06:12 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0062_merge_20250104_1141'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='diagnosticreport', - name='created_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='diagnosticreport', - name='updated_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='servicerequest', - name='created_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='servicerequest', - name='updated_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='specimen', - name='created_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='specimen', - name='updated_by', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/care/emr/migrations/0064_alter_servicerequest_category_and_more.py b/care/emr/migrations/0064_alter_servicerequest_category_and_more.py deleted file mode 100644 index 56122c3fa5..0000000000 --- a/care/emr/migrations/0064_alter_servicerequest_category_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.1.1 on 2025-01-05 13:22 - -import care.emr.models.organization -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0063_diagnosticreport_created_by_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='servicerequest', - name='category', - field=models.CharField(blank=True, max_length=100, null=True), - ), - migrations.AlterField( - model_name='servicerequest', - name='encounter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.encounter'), - ), - migrations.AlterField( - model_name='servicerequest', - name='location', - field=models.UUIDField(blank=True, null=True, verbose_name=care.emr.models.organization.Organization), - ), - migrations.AlterField( - model_name='servicerequest', - name='note', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='servicerequest', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_request', to='emr.patient'), - ), - ] diff --git a/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py b/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py deleted file mode 100644 index d9355adfd9..0000000000 --- a/care/emr/migrations/0065_alter_diagnosticreport_encounter_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 5.1.1 on 2025-01-05 16:32 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('emr', '0064_alter_servicerequest_category_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='diagnosticreport', - name='encounter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.encounter'), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='note', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='diagnosticreport', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='diagnostic_report', to='emr.patient'), - ), - migrations.AlterField( - model_name='specimen', - name='note', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='specimen', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specimen', to='emr.patient'), - ), - ] From 5f1e76e81de3a9f99ee86c9d6477a8e9a14624cd Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 18:03:43 +0530 Subject: [PATCH 18/32] fixed migration conflicts --- care/emr/migrations/0006_merge_20250110_1637.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 care/emr/migrations/0006_merge_20250110_1637.py diff --git a/care/emr/migrations/0006_merge_20250110_1637.py b/care/emr/migrations/0006_merge_20250110_1637.py new file mode 100644 index 0000000000..826b652579 --- /dev/null +++ b/care/emr/migrations/0006_merge_20250110_1637.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.3 on 2025-01-10 11:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0002_servicerequest_specimen_diagnosticreport'), + ('emr', '0005_alter_availability_slot_size_in_minutes_and_more'), + ] + + operations = [ + ] From fcbcf2e56ff56b2a364f538738bc1a74f62857bb Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 18:04:10 +0530 Subject: [PATCH 19/32] fixed receive at lab endpoint --- care/emr/api/viewsets/specimen.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index c41c940e0a..3616d736ee 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -10,7 +10,6 @@ from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet -from care.emr.fhir.schema.base import DateTime from care.emr.models.specimen import Specimen from care.emr.resources.specimen.spec import ( SpecimenCollectRequest, @@ -159,16 +158,12 @@ def send_to_lab(self, request, *args, **kwargs): def receive_at_lab(self, request, *args, **kwargs): data = SpecimenReceiveAtLabRequest(**request.data) specimen = self.get_object() - note = data.note specimen.accession_identifier = data.accession_identifier specimen.condition = data.condition specimen.received_at = datetime.now(UTC) specimen.received_by = request.user - if note: - note.authorReference = {"id": request.user.external_id} - note.time = DateTime(datetime.now(UTC).isoformat()) - specimen.note.append(note.model_dump(mode="json")) + specimen.note = data.note specimen.save() return Response( From 223f0c7064ee26f659b0386e97cce611cec5a212 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 18:07:46 +0530 Subject: [PATCH 20/32] fixed len issue in diagnostic report retrieve serializer --- care/emr/resources/diagnostic_report/spec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 26abaea041..12998b53b0 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -215,12 +215,12 @@ def perform_extra_serialization(cls, mapping, obj): mapping["results_interpreter"] = UserSpec.serialize( obj.results_interpreter ).to_json() - if len(obj.specimen): + if obj.specimen.exists(): mapping["specimen"] = [ SpecimenRetrieveSpec.serialize(specimen).to_json() for specimen in obj.specimen.all() ] - if len(obj.result): + if obj.result.exists(): mapping["result"] = [ ObservationSpec.serialize(observation).to_json() for observation in obj.result.all() From c4e043bd923c8a279afc191edbbd98433bb66703 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 19:48:01 +0530 Subject: [PATCH 21/32] added phase field to service request --- .../migrations/0007_servicerequest_phase.py | 18 ++++++ care/emr/models/diagnostic_report.py | 5 ++ care/emr/models/service_request.py | 58 +++++++++++++++++++ care/emr/models/specimen.py | 6 ++ care/emr/resources/service_request/spec.py | 8 +-- 5 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 care/emr/migrations/0007_servicerequest_phase.py diff --git a/care/emr/migrations/0007_servicerequest_phase.py b/care/emr/migrations/0007_servicerequest_phase.py new file mode 100644 index 0000000000..1fd6e218d1 --- /dev/null +++ b/care/emr/migrations/0007_servicerequest_phase.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2025-01-10 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0006_merge_20250110_1637'), + ] + + operations = [ + migrations.AddField( + model_name='servicerequest', + name='phase', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/care/emr/models/diagnostic_report.py b/care/emr/models/diagnostic_report.py index dfe6e78032..1a7f2990af 100644 --- a/care/emr/models/diagnostic_report.py +++ b/care/emr/models/diagnostic_report.py @@ -58,3 +58,8 @@ class DiagnosticReport(EMRBaseModel): note = models.TextField(null=True, blank=True) conclusion = models.TextField(null=True, blank=True) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.based_on: + self.based_on.save() diff --git a/care/emr/models/service_request.py b/care/emr/models/service_request.py index 1505634f55..4432dd7c5e 100644 --- a/care/emr/models/service_request.py +++ b/care/emr/models/service_request.py @@ -1,3 +1,4 @@ +from django.apps import apps from django.db import models from care.emr.models.base import EMRBaseModel @@ -46,3 +47,60 @@ class ServiceRequest(EMRBaseModel): replaces = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True ) + + phase = models.CharField(max_length=100, null=True, blank=True) + + def calculate_phase(self): # noqa: PLR0911 PLR0912 + DiagnosticReport = apps.get_model("emr", "DiagnosticReport") + Specimen = apps.get_model("emr", "Specimen") + + if not (self.category == "laboratory_procedure" and self.intent == "order"): + return None + + if self.status == "revoked": + return "order_cancelled" + + if DiagnosticReport.objects.filter(based_on=self.pk, status="final").exists(): + return "order_completed" + + if DiagnosticReport.objects.filter(based_on=self.pk, status="invalid").exists(): + return "result_invalid" + + if DiagnosticReport.objects.filter( + based_on=self.pk, status="preliminary", issued__isnull=False + ).exists(): + return "result_under_review" + + if DiagnosticReport.objects.filter(based_on=self.pk, status="partial").exists(): + return "result_under_verification" + + if Specimen.objects.filter(request=self.pk, processing__gt=[]).exists(): + return "sample_in_process" + + if Specimen.objects.filter(request=self.pk, status="unsatisfactory").exists(): + return "sample_rejected" + + if Specimen.objects.filter(request=self.pk, received_at__isnull=False).exists(): + return "sample_received_at_lab" + + if Specimen.objects.filter( + request=self.pk, dispatched_at__isnull=False + ).exists(): + return "sample_sent_to_lab" + + if Specimen.objects.filter( + request=self.pk, collected_at__isnull=False + ).exists(): + return "sample_collected" + + if self.status == "active": + return "order_in_progress" + + if self.status == "draft": + return "order_placed" + + return None + + def save(self, *args, **kwargs): + self.phase = self.calculate_phase() + super().save(*args, **kwargs) diff --git a/care/emr/models/specimen.py b/care/emr/models/specimen.py index 7c43bbbf13..f877a8c44c 100644 --- a/care/emr/models/specimen.py +++ b/care/emr/models/specimen.py @@ -1,3 +1,4 @@ + from django.db import models from care.emr.models.base import EMRBaseModel @@ -57,3 +58,8 @@ class Specimen(EMRBaseModel): note = models.TextField(null=True, blank=True) parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.request: + self.request.save() diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index 627a8dbbce..e76ebae04c 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -208,10 +208,10 @@ def perform_extra_serialization(cls, mapping, obj): ).to_json() if obj.requester: mapping["requester"] = UserSpec.serialize(obj.requester).to_json() - if obj.location: - mapping["location"] = OrganizationRetrieveSpec.serialize( - obj.location - ).to_json() + # if obj.location: + # mapping["location"] = OrganizationRetrieveSpec.serialize( + # obj.location + # ).to_json() if obj.replaces: mapping["replaces"] = ServiceRequestRetrieveSpec.serialize( obj.replaces From 8094d8c6c06d5c3529ed1decbe80dc9dcbd0749e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 19:49:13 +0530 Subject: [PATCH 22/32] use retrieve spec model in endpoints in specimen and diagnostic report --- care/emr/api/viewsets/diagnostic_report.py | 6 +++--- care/emr/api/viewsets/specimen.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 14f46db401..8b2147b417 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -110,7 +110,7 @@ def observations(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model().serialize(report).to_json(), + self.get_retrieve_pydantic_model().serialize(report).to_json(), ) @extend_schema( @@ -132,7 +132,7 @@ def verify(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model().serialize(report).to_json(), + self.get_retrieve_pydantic_model().serialize(report).to_json(), ) @extend_schema( @@ -164,5 +164,5 @@ def review(self, request, *args, **kwargs): report.save() return Response( - self.get_read_pydantic_model().serialize(report).to_json(), + self.get_retrieve_pydantic_model().serialize(report).to_json(), ) diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 3616d736ee..fdd83a990f 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -125,7 +125,7 @@ def collect(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model().serialize(specimen).to_json(), + self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( @@ -146,7 +146,7 @@ def send_to_lab(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model().serialize(specimen).to_json(), + self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( @@ -167,7 +167,7 @@ def receive_at_lab(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model().serialize(specimen).to_json(), + self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) @extend_schema( @@ -194,5 +194,5 @@ def process(self, request, *args, **kwargs): specimen.save() return Response( - self.get_read_pydantic_model().serialize(specimen).to_json(), + self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) From 10273ea2dd4bc2e9099c3f9c27115c89670ccd55 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 19:50:30 +0530 Subject: [PATCH 23/32] fixed error while retrieving diagnostic report --- care/emr/resources/diagnostic_report/spec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 12998b53b0..209775504a 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -14,7 +14,7 @@ CARE_DIAGNOSTIC_REPORT_CODE_VALUESET, ) from care.emr.resources.encounter.spec import EncounterRetrieveSpec -from care.emr.resources.observation.spec import ObservationSpec +from care.emr.resources.observation.spec import ObservationReadSpec, ObservationSpec from care.emr.resources.patient.spec import PatientRetrieveSpec from care.emr.resources.service_request.spec import ServiceRequestRetrieveSpec from care.emr.resources.specimen.spec import SpecimenRetrieveSpec @@ -222,7 +222,7 @@ def perform_extra_serialization(cls, mapping, obj): ] if obj.result.exists(): mapping["result"] = [ - ObservationSpec.serialize(observation).to_json() + ObservationReadSpec.serialize(observation).to_json() for observation in obj.result.all() ] From e9f06c0915e52a93770f33043b117449f46ca818 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 22:46:45 +0530 Subject: [PATCH 24/32] added phase field to service request spec --- care/emr/resources/service_request/spec.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index e76ebae04c..6674dfa53b 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -53,6 +53,21 @@ class ServiceRequestCategoryChoices(str, Enum): surgical_procedure = "surgical_procedure" +class ServiceRequestPhaseChoices(str, Enum): + order_placed = "order_placed" + order_in_progress = "order_in_progress" + sample_collected = "sample_collected" + sample_sent_to_lab = "sample_sent_to_lab" + sample_received_at_lab = "sample_received_at_lab" + sample_rejected = "sample_rejected" + sample_in_process = "sample_in_process" + result_under_verification = "result_under_verification" + result_under_review = "result_under_review" + result_invalid = "result_invalid" + order_completed = "order_completed" + order_cancelled = "order_cancelled" + + class ServiceRequestSpec(EMRResource): __model__ = ServiceRequest __exclude__ = ["subject", "encounter", "requester", "location", "replaces"] @@ -140,6 +155,11 @@ class ServiceRequestSpec(EMRResource): description="The request that is being replaced by this request, used in the case of re-orders", ) + phase: ServiceRequestPhaseChoices | None = Field( + None, + description="Indicates the current phase of the lab orders, used internally to track the lifecycle of the order. It is a read-only field and its value is None if the request is not a lab order", + ) + @field_validator("code") @classmethod def validate_code(cls, value: str): From 1b4a12a8cc9650555a50bc84ed3bbf70a88acf47 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 23:08:57 +0530 Subject: [PATCH 25/32] added phase filter and removed flow status filter --- care/emr/api/viewsets/diagnostic_report.py | 36 ++--------- care/emr/api/viewsets/service_request.py | 10 +-- care/emr/api/viewsets/specimen.py | 65 ++------------------ care/emr/resources/diagnostic_report/spec.py | 2 + care/emr/resources/service_request/spec.py | 2 + care/emr/resources/specimen/spec.py | 2 + 6 files changed, 17 insertions(+), 100 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 8b2147b417..484af75d91 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -1,10 +1,8 @@ from datetime import UTC, datetime -from django.db import models -from django.db.models import Case, CharField, Value, When -from django_filters import ChoiceFilter, FilterSet, OrderingFilter, UUIDFilter +from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema from rest_framework.decorators import action from rest_framework.response import Response @@ -24,18 +22,9 @@ from care.emr.resources.observation.spec import Performer, PerformerType -class PhaseChoices(models.TextChoices): - IN_PROCESS = "in_process", "In Process" - VERIFICATION_REQUIRED = "verification_required", "Verification Required" - REVIEW_REQUIRED = "review_required", "Review Required" - REVIEWED = "reviewed", "Reviewed" - - class DiagnosticReportFilters(FilterSet): - phase = ChoiceFilter(choices=PhaseChoices.choices, method="filter_phase") - status = ChoiceFilter( - choices=[(status.value, status.value) for status in StatusChoices] - ) + phase = CharFilter(field_name="based_on__phase", lookup_expr="iexact") + status = CharFilter(field_name="status", lookup_expr="iexact") specimen = UUIDFilter(field_name="specimen__external_id") based_on = UUIDFilter(field_name="based_on__external_id") @@ -46,24 +35,7 @@ class DiagnosticReportFilters(FilterSet): ) ) - def filter_phase(self, queryset, name, value): - return queryset.annotate( - phase=Case( - When(status=StatusChoices.final, then=Value("reviewed")), - When(status=StatusChoices.preliminary, then=Value("review_required")), - When(status=StatusChoices.partial, then=Value("verification_required")), - default=Value("in_process"), - output_field=CharField(), - ) - ).filter(phase=value) - -@extend_schema_view( - create=extend_schema(request=DiagnosticReportCreateSpec), - update=extend_schema(request=DiagnosticReportUpdateSpec), - list=extend_schema(request=DiagnosticReportListSpec), - retrieve=extend_schema(request=DiagnosticReportRetrieveSpec), -) class DiagnosticReportViewSet(EMRModelViewSet): database_model = DiagnosticReport pydantic_model = DiagnosticReportCreateSpec diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index b99d81c068..574554c80e 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -1,6 +1,5 @@ -from django_filters import FilterSet, OrderingFilter, UUIDFilter +from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend -from drf_spectacular.utils import extend_schema, extend_schema_view from care.emr.api.viewsets.base import EMRModelViewSet from care.emr.models.service_request import ServiceRequest @@ -13,6 +12,7 @@ class ServiceRequestFilters(FilterSet): + phase = CharFilter(field_name="phase", lookup_expr="iexact") subject = UUIDFilter(field_name="subject__external_id") encounter = UUIDFilter(field_name="encounter__external_id") @@ -24,12 +24,6 @@ class ServiceRequestFilters(FilterSet): ) -@extend_schema_view( - create=extend_schema(request=ServiceRequestCreateSpec), - update=extend_schema(request=ServiceRequestUpdateSpec), - list=extend_schema(request=ServiceRequestListSpec), - retrieve=extend_schema(request=ServiceRequestRetrieveSpec), -) class ServiceRequestViewSet(EMRModelViewSet): database_model = ServiceRequest pydantic_model = ServiceRequestCreateSpec diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index fdd83a990f..16b16b3df7 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -1,10 +1,9 @@ from datetime import UTC, datetime -from django.db import models -from django.db.models import Case, CharField, Q, Value, When -from django_filters import ChoiceFilter, FilterSet, OrderingFilter, UUIDFilter +from django.db.models import Q +from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -24,18 +23,10 @@ ) -class PhaseChoices(models.TextChoices): - ORDERED = "ordered", "Ordered" - COLLECTED = "collected", "Collected" - SENT = "sent", "Sent" - RECEIVED = "received", "Received" - IN_PROCESS = "in_process", "In Process" - - class SpecimenFilters(FilterSet): + phase = CharFilter(field_name="request__phase", lookup_expr="iexact") request = UUIDFilter(field_name="request__external_id") encounter = UUIDFilter(field_name="request__encounter__external_id") - phase = ChoiceFilter(choices=PhaseChoices.choices, method="filter_phase") ordering = OrderingFilter( fields=( @@ -44,53 +35,7 @@ class SpecimenFilters(FilterSet): ) ) - def filter_phase(self, queryset, name, value): - return queryset.annotate( - phase=Case( - When( - Q(processing__gt=[]) - & ( - Q(diagnostic_report__isnull=True) - | Q(diagnostic_report__status__in=["registered", "partial"]) - ), - then=Value("in_process"), - ), - When( - diagnostic_report__isnull=True, - received_at__isnull=False, - then=Value("received"), - ), - When( - diagnostic_report__isnull=True, - received_at__isnull=True, - dispatched_at__isnull=False, - then=Value("sent"), - ), - When( - diagnostic_report__isnull=True, - received_at__isnull=True, - dispatched_at__isnull=True, - collected_at__isnull=False, - then=Value("collected"), - ), - When( - diagnostic_report__isnull=True, - received_at__isnull=True, - dispatched_at__isnull=True, - collected_at__isnull=True, - then=Value("ordered"), - ), - output_field=CharField(), - ) - ).filter(phase=value) - - -@extend_schema_view( - create=extend_schema(request=SpecimenCreateSpec), - update=extend_schema(request=SpecimenUpdateSpec), - list=extend_schema(request=SpecimenListSpec), - retrieve=extend_schema(request=SpecimenRetrieveSpec), -) + class SpecimenViewSet(EMRModelViewSet): database_model = Specimen pydantic_model = SpecimenCreateSpec diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 209775504a..ec2c1c8e74 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -188,6 +188,8 @@ class DiagnosticReportListSpec(DiagnosticReportSpec): created_by: UserSpec | None = None updated_by: UserSpec | None = None + created_date: datetime | None = None + modified_date: datetime | None = None @classmethod def perform_extra_serialization(cls, mapping, obj): diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index 6674dfa53b..b47107c9d8 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -200,6 +200,8 @@ class ServiceRequestListSpec(ServiceRequestSpec): created_by: UserSpec | None = None updated_by: UserSpec | None = None + created_date: datetime | None = None + modified_date: datetime | None = None @classmethod def perform_extra_serialization(cls, mapping, obj): diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index ced05b71e0..28217b61f9 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -190,6 +190,8 @@ class SpecimenListSpec(SpecimenSpec): created_by: UserSpec | None = None updated_by: UserSpec | None = None + created_date: datetime | None = None + modified_date: datetime | None = None @classmethod def perform_extra_serialization(cls, mapping, obj): From 7549d74854cd97b57129431208e616ba06329add Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 23:15:38 +0530 Subject: [PATCH 26/32] fixed typo caused while resolving merge conflict --- care/facility/api/viewsets/facility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index f83794033d..4d9bff61ed 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -40,7 +40,7 @@ def filter(self, qs, value): class FacilityFilter(filters.FilterSet): name = filters.CharFilter(field_name="name", lookup_expr="icontains") facility_type = filters.BaseInFilter(field_name="facility_type", lookup_expr="in") - geo_organization = GeoOrganizationFilter() + organization = GeoOrganizationFilter() class FacilityViewSet( From 5e23ee089775ca070257a0b6cc410ecb389f4af2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 10 Jan 2025 23:46:51 +0530 Subject: [PATCH 27/32] added code rabbit suggestions --- care/emr/fhir/resources/base.py | 2 +- care/emr/fhir/resources/code_concept.py | 4 ++-- care/emr/fhir/resources/code_system.py | 4 ++-- care/emr/fhir/resources/concept_map.py | 4 ++-- care/emr/fhir/resources/valueset.py | 4 ++-- care/emr/resources/diagnostic_report/spec.py | 7 +++++-- care/emr/resources/service_request/spec.py | 7 +++++-- care/emr/resources/specimen/spec.py | 7 +++++-- care/emr/signals/auto_create_specimen.py | 5 ++--- 9 files changed, 26 insertions(+), 18 deletions(-) diff --git a/care/emr/fhir/resources/base.py b/care/emr/fhir/resources/base.py index 8a18da3f69..3d0c7c8ab2 100644 --- a/care/emr/fhir/resources/base.py +++ b/care/emr/fhir/resources/base.py @@ -12,7 +12,7 @@ default_fhir_client = FHIRClient(server_url=settings.SNOWSTORM_DEPLOYMENT_URL) -class ResourceManger: +class ResourceManager: _fhir_client = default_fhir_client resource = "" allowed_properties = [] diff --git a/care/emr/fhir/resources/code_concept.py b/care/emr/fhir/resources/code_concept.py index 0399d1ac7a..d9c56b0835 100644 --- a/care/emr/fhir/resources/code_concept.py +++ b/care/emr/fhir/resources/code_concept.py @@ -1,10 +1,10 @@ from pydantic.main import BaseModel -from care.emr.fhir.resources.base import ResourceManger +from care.emr.fhir.resources.base import ResourceManager from care.emr.fhir.utils import parse_fhir_parameter_output -class CodeConceptResource(ResourceManger): +class CodeConceptResource(ResourceManager): allowed_properties = ["system", "code", "property"] resource = "CodeConcept" diff --git a/care/emr/fhir/resources/code_system.py b/care/emr/fhir/resources/code_system.py index 590560c976..ff31b4b953 100644 --- a/care/emr/fhir/resources/code_system.py +++ b/care/emr/fhir/resources/code_system.py @@ -3,13 +3,13 @@ from pydantic.main import BaseModel from care.emr.fhir.exceptions import MoreThanOneFHIRResourceFoundError -from care.emr.fhir.resources.base import ResourceManger +from care.emr.fhir.resources.base import ResourceManager from care.facility.models import User User.objects.filter() -class CodeSystemResource(ResourceManger): +class CodeSystemResource(ResourceManager): allowed_properties = ["name", "url"] resource = "CodeSystem" diff --git a/care/emr/fhir/resources/concept_map.py b/care/emr/fhir/resources/concept_map.py index 41192e2a72..ada58a50a6 100644 --- a/care/emr/fhir/resources/concept_map.py +++ b/care/emr/fhir/resources/concept_map.py @@ -2,11 +2,11 @@ from pydantic.main import BaseModel -from care.emr.fhir.resources.base import ResourceManger +from care.emr.fhir.resources.base import ResourceManager from care.emr.fhir.utils import parse_fhir_parameter_output -class ConceptMapResource(ResourceManger): +class ConceptMapResource(ResourceManager): allowed_properties = ["system", "code"] resource = "ConceptMap" diff --git a/care/emr/fhir/resources/valueset.py b/care/emr/fhir/resources/valueset.py index 5bba40f906..d5ee6d1959 100644 --- a/care/emr/fhir/resources/valueset.py +++ b/care/emr/fhir/resources/valueset.py @@ -1,6 +1,6 @@ from pydantic.main import BaseModel -from care.emr.fhir.resources.base import ResourceManger +from care.emr.fhir.resources.base import ResourceManager from care.emr.fhir.resources.code_concept import MinimalCodeConcept from care.emr.fhir.schema.base import Coding from care.emr.fhir.schema.valueset.valueset import ValueSetInclude @@ -13,7 +13,7 @@ class ValueSetFilterValidation(BaseModel): count: int = None -class ValueSetResource(ResourceManger): +class ValueSetResource(ResourceManager): allowed_properties = ["include", "exclude", "search", "count", "display_language"] def serialize(self, result): diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index ec2c1c8e74..fb8c3c18a2 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -131,14 +131,17 @@ class DiagnosticReportSpec(EMRResource): @field_validator("category") @classmethod - def validate_category(cls, value: str): + def validate_category(cls, value: Coding | None): + if value is None: + return None + return validate_valueset( "category", cls.model_fields["category"].json_schema_extra["slug"], value ) @field_validator("code") @classmethod - def validate_code(cls, value: str): + def validate_code(cls, value: Coding): return validate_valueset( "code", cls.model_fields["code"].json_schema_extra["slug"], value ) diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index b47107c9d8..d82e9d2344 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -162,14 +162,17 @@ class ServiceRequestSpec(EMRResource): @field_validator("code") @classmethod - def validate_code(cls, value: str): + def validate_code(cls, value: Coding): return validate_valueset( "code", cls.model_fields["code"].json_schema_extra["slug"], value ) @field_validator("as_needed_for") @classmethod - def validate_as_needed_for(cls, value: str): + def validate_as_needed_for(cls, value: Coding | None): + if value is None: + return None + return validate_valueset( "as_needed_for", cls.model_fields["as_needed_for"].json_schema_extra["slug"], diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 28217b61f9..22b1fba0e4 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -44,7 +44,10 @@ class SpecimenProcessingSpec(EMRResource): @field_validator("method") @classmethod - def validate_method(cls, value: str): + def validate_method(cls, value: Coding | None): + if value is None: + return None + return validate_valueset( "method", cls.model_fields["method"].json_schema_extra["slug"], @@ -141,7 +144,7 @@ class SpecimenSpec(EMRResource): @field_validator("type") @classmethod - def validate_type(cls, value: str): + def validate_type(cls, value: Coding): return validate_valueset( "type", cls.model_fields["type"].json_schema_extra["slug"], value ) diff --git a/care/emr/signals/auto_create_specimen.py b/care/emr/signals/auto_create_specimen.py index 9628128044..9d33817b69 100644 --- a/care/emr/signals/auto_create_specimen.py +++ b/care/emr/signals/auto_create_specimen.py @@ -43,9 +43,8 @@ def create_specimen(sender, instance: ServiceRequest, created: bool, **kwargs): specimen_matches.sort(key=lambda x: x.equivalence.priority) if len(specimen_matches) == 0: - return ValidationError( - f"No Specimen found for the given Service Request code {instance.code}" - ) + err = f"No Specimen found for the given Service Request code {instance.code}" + raise ValidationError(err) specimen_coding = specimen_matches[0].concept From bafcb937f6eaf6a8f019b08e569e4570d6a80164 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 11 Jan 2025 00:25:27 +0530 Subject: [PATCH 28/32] use perform create instead of clean create data --- care/emr/api/viewsets/diagnostic_report.py | 8 +++----- care/emr/api/viewsets/service_request.py | 8 +++----- care/emr/resources/diagnostic_report/spec.py | 9 --------- care/emr/resources/service_request/spec.py | 7 +++---- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 484af75d91..4cb9a5c559 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -45,11 +45,9 @@ class DiagnosticReportViewSet(EMRModelViewSet): filter_backends = [DjangoFilterBackend] filterset_class = DiagnosticReportFilters - def clean_create_data(self, request, *args, **kwargs): - clean_data = super().clean_create_data(request, *args, **kwargs) - - clean_data["performer"] = self.request.user.external_id - return clean_data + def perform_create(self, instance): + instance.performer = self.request.user + super().perform_create(instance) @extend_schema( request=DiagnosticReportObservationRequest, diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index 574554c80e..9b727b057c 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -33,8 +33,6 @@ class ServiceRequestViewSet(EMRModelViewSet): filter_backends = [DjangoFilterBackend] filterset_class = ServiceRequestFilters - def clean_create_data(self, request, *args, **kwargs): - clean_data = super().clean_create_data(request, *args, **kwargs) - - clean_data["requester"] = self.request.user.external_id - return clean_data + def perform_create(self, instance): + instance.requester = self.request.user + return super().perform_create(instance) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index fb8c3c18a2..1c50b06a4d 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -19,7 +19,6 @@ from care.emr.resources.service_request.spec import ServiceRequestRetrieveSpec from care.emr.resources.specimen.spec import SpecimenRetrieveSpec from care.emr.resources.user.spec import UserSpec -from care.users.models import User class DiagnosticReportMedia(EMRResource): @@ -157,14 +156,6 @@ def perform_extra_deserialization(self, is_update, obj): if not obj.code: obj.code = obj.based_on.code - if self.performer: - obj.performer = User.objects.get(external_id=self.performer) - - if self.results_interpreter: - obj.results_interpreter = User.objects.get( - external_id=self.results_interpreter - ) - obj.save() if self.specimen: diff --git a/care/emr/resources/service_request/spec.py b/care/emr/resources/service_request/spec.py index d82e9d2344..ca4c147cd5 100644 --- a/care/emr/resources/service_request/spec.py +++ b/care/emr/resources/service_request/spec.py @@ -18,7 +18,6 @@ CARE_MEDICATION_AS_NEEDED_REASON_VALUESET, ) from care.emr.resources.user.spec import UserSpec -from care.users.models import User class ServiceRequestStatusChoices(str, Enum): @@ -132,8 +131,9 @@ class ServiceRequestSpec(EMRResource): default=datetime.now(UTC), description="The date when the request was made", ) - requester: UUID4 = Field( - description="The individual who initiated the request and has responsibility for its activation", + requester: UUID4 | None = Field( + default=None, + description="The individual who initiated the request and has responsibility for its activation. If None, the current user is assumed to be the requester", ) location: UUID4 | None = Field( @@ -185,7 +185,6 @@ def perform_extra_deserialization(self, is_update, obj): if not is_update: obj.encounter = Encounter.objects.get(external_id=self.encounter) obj.subject = obj.encounter.patient - obj.requester = User.objects.get(external_id=self.requester) class ServiceRequestUpdateSpec(ServiceRequestCreateSpec): From 48a43d9d5be7de4ce4c5b55baf54dd7e7f734ae8 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 11 Jan 2025 00:32:56 +0530 Subject: [PATCH 29/32] moved all the request specs into the viewset --- care/emr/api/viewsets/diagnostic_report.py | 36 ++++++++++--- care/emr/api/viewsets/specimen.py | 53 +++++++++++++++++--- care/emr/resources/diagnostic_report/spec.py | 25 +-------- care/emr/resources/specimen/spec.py | 43 +--------------- 4 files changed, 76 insertions(+), 81 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index 4cb9a5c559..c412ec7c0c 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -3,6 +3,7 @@ from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema +from pydantic import BaseModel, Field from rest_framework.decorators import action from rest_framework.response import Response @@ -12,14 +13,15 @@ from care.emr.resources.diagnostic_report.spec import ( DiagnosticReportCreateSpec, DiagnosticReportListSpec, - DiagnosticReportObservationRequest, DiagnosticReportRetrieveSpec, - DiagnosticReportReviewRequest, DiagnosticReportUpdateSpec, - DiagnosticReportVerifyRequest, StatusChoices, ) -from care.emr.resources.observation.spec import Performer, PerformerType +from care.emr.resources.observation.spec import ( + ObservationSpec, + Performer, + PerformerType, +) class DiagnosticReportFilters(FilterSet): @@ -49,6 +51,12 @@ def perform_create(self, instance): instance.performer = self.request.user super().perform_create(instance) + class DiagnosticReportObservationRequest(BaseModel): + observations: list[ObservationSpec] = Field( + default=[], + description="List of observations that are part of the diagnostic report", + ) + @extend_schema( request=DiagnosticReportObservationRequest, responses={200: DiagnosticReportRetrieveSpec}, @@ -56,7 +64,7 @@ def perform_create(self, instance): ) @action(detail=True, methods=["POST"]) def observations(self, request, *args, **kwargs): - data = DiagnosticReportObservationRequest(**request.data) + data = self.DiagnosticReportObservationRequest(**request.data) report: DiagnosticReport = self.get_object() observations = [] @@ -83,6 +91,11 @@ def observations(self, request, *args, **kwargs): self.get_retrieve_pydantic_model().serialize(report).to_json(), ) + class DiagnosticReportVerifyRequest(BaseModel): + is_approved: bool = Field( + description="Indicates whether the diagnostic report is approved or rejected", + ) + @extend_schema( request=DiagnosticReportVerifyRequest, responses={200: DiagnosticReportRetrieveSpec}, @@ -90,7 +103,7 @@ def observations(self, request, *args, **kwargs): ) @action(detail=True, methods=["POST"]) def verify(self, request, *args, **kwargs): - data = DiagnosticReportVerifyRequest(**request.data) + data = self.DiagnosticReportVerifyRequest(**request.data) report: DiagnosticReport = self.get_object() if data.is_approved: @@ -105,6 +118,15 @@ def verify(self, request, *args, **kwargs): self.get_retrieve_pydantic_model().serialize(report).to_json(), ) + class DiagnosticReportReviewRequest(BaseModel): + is_approved: bool = Field( + description="Indicates whether the diagnostic report is approved or rejected", + ) + conclusion: str | None = Field( + default=None, + description="Additional notes about the review", + ) + @extend_schema( request=DiagnosticReportReviewRequest, responses={200: DiagnosticReportRetrieveSpec}, @@ -112,7 +134,7 @@ def verify(self, request, *args, **kwargs): ) @action(detail=True, methods=["POST"]) def review(self, request, *args, **kwargs): - data = DiagnosticReportReviewRequest(**request.data) + data = self.DiagnosticReportReviewRequest(**request.data) report: DiagnosticReport = self.get_object() if ( diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 16b16b3df7..92e744a9a4 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -4,20 +4,20 @@ from django_filters import CharFilter, FilterSet, OrderingFilter, UUIDFilter from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema +from pydantic import UUID4, BaseModel, Field, field_validator from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.fhir.schema.base import CodeableConcept from care.emr.models.specimen import Specimen from care.emr.resources.specimen.spec import ( - SpecimenCollectRequest, SpecimenCreateSpec, SpecimenListSpec, - SpecimenProcessRequest, - SpecimenReceiveAtLabRequest, + SpecimenProcessingSpec, SpecimenRetrieveSpec, - SpecimenSendToLabRequest, + SpecimenSpec, SpecimenUpdateSpec, StatusChoices, ) @@ -53,6 +53,12 @@ def get_object(self) -> Specimen: | Q(accession_identifier=self.kwargs[self.lookup_field]), ) + class SpecimenCollectRequest(BaseModel): + identifier: str | None = Field( + default=None, + description="The identifier assigned to the specimen while collecting, this can be barcode or any other identifier", + ) + @extend_schema( request=SpecimenCollectRequest, responses={200: SpecimenRetrieveSpec}, @@ -60,7 +66,7 @@ def get_object(self) -> Specimen: ) @action(detail=True, methods=["POST"]) def collect(self, request, *args, **kwargs): - data = SpecimenCollectRequest(**request.data) + data = self.SpecimenCollectRequest(**request.data) specimen = self.get_object() specimen.identifier = data.identifier @@ -73,6 +79,11 @@ def collect(self, request, *args, **kwargs): self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) + class SpecimenSendToLabRequest(BaseModel): + lab: UUID4 = Field( + description="The laboratory to which the specimen is being sent", + ) + @extend_schema( request=SpecimenSendToLabRequest, responses={200: SpecimenRetrieveSpec}, @@ -80,7 +91,7 @@ def collect(self, request, *args, **kwargs): ) @action(detail=True, methods=["POST"]) def send_to_lab(self, request, *args, **kwargs): - data = SpecimenSendToLabRequest(**request.data) + data = self.SpecimenSendToLabRequest(**request.data) specimen = self.get_object() service_request = specimen.request @@ -94,6 +105,27 @@ def send_to_lab(self, request, *args, **kwargs): self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) + class SpecimenReceiveAtLabRequest(BaseModel): + accession_identifier: str | None = Field( + default=None, + description="The identifier assigned to the specimen by the laboratory", + ) + + condition: list[CodeableConcept] | None = Field( + default=None, + description="The condition of the specimen while received at the laboratory", + ) + + note: str | None = Field( + default=None, + description="Comments made about the specimen while received at the laboratory", + ) + + @field_validator("condition") + @classmethod + def validate_condition(cls, value: CodeableConcept): + return SpecimenSpec.validate_condition(value) + @extend_schema( request=SpecimenReceiveAtLabRequest, responses={200: SpecimenRetrieveSpec}, @@ -101,7 +133,7 @@ def send_to_lab(self, request, *args, **kwargs): ) @action(detail=True, methods=["POST"]) def receive_at_lab(self, request, *args, **kwargs): - data = SpecimenReceiveAtLabRequest(**request.data) + data = self.SpecimenReceiveAtLabRequest(**request.data) specimen = self.get_object() specimen.accession_identifier = data.accession_identifier @@ -115,6 +147,11 @@ def receive_at_lab(self, request, *args, **kwargs): self.get_retrieve_pydantic_model().serialize(specimen).to_json(), ) + class SpecimenProcessRequest(BaseModel): + process: list[SpecimenProcessingSpec] = Field( + description="The processing steps that have been performed on the specimen", + ) + @extend_schema( request=SpecimenProcessRequest, responses={200: SpecimenRetrieveSpec}, @@ -122,7 +159,7 @@ def receive_at_lab(self, request, *args, **kwargs): ) @action(detail=True, methods=["POST"]) def process(self, request, *args, **kwargs): - data = SpecimenProcessRequest(**request.data) + data = self.SpecimenProcessRequest(**request.data) specimen = self.get_object() processes = [] diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index 1c50b06a4d..a198eb9b25 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -1,7 +1,7 @@ from datetime import datetime from enum import Enum -from pydantic import UUID4, BaseModel, Field, field_validator +from pydantic import UUID4, Field, field_validator from care.emr.fhir.schema.base import Coding, Period from care.emr.models.diagnostic_report import DiagnosticReport @@ -226,26 +226,3 @@ def perform_extra_serialization(cls, mapping, obj): mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() if obj.updated_by: mapping["updated_by"] = UserSpec.serialize(obj.updated_by).to_json() - - -class DiagnosticReportObservationRequest(BaseModel): - observations: list[ObservationSpec] = Field( - default=[], - description="List of observations that are part of the diagnostic report", - ) - - -class DiagnosticReportVerifyRequest(BaseModel): - is_approved: bool = Field( - description="Indicates whether the diagnostic report is approved or rejected", - ) - - -class DiagnosticReportReviewRequest(BaseModel): - is_approved: bool = Field( - description="Indicates whether the diagnostic report is approved or rejected", - ) - conclusion: str | None = Field( - default=None, - description="Additional notes about the review", - ) diff --git a/care/emr/resources/specimen/spec.py b/care/emr/resources/specimen/spec.py index 22b1fba0e4..7c594f7e82 100644 --- a/care/emr/resources/specimen/spec.py +++ b/care/emr/resources/specimen/spec.py @@ -3,7 +3,7 @@ from datetime import UTC, datetime from enum import Enum -from pydantic import UUID4, BaseModel, Field, field_validator +from pydantic import UUID4, Field, field_validator from care.emr.fhir.schema.base import CodeableConcept, Coding from care.emr.models.service_request import ServiceRequest @@ -232,44 +232,3 @@ def perform_extra_serialization(cls, mapping, obj): mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() if obj.updated_by: mapping["updated_by"] = UserSpec.serialize(obj.updated_by).to_json() - - -class SpecimenCollectRequest(BaseModel): - identifier: str | None = Field( - default=None, - description="The identifier assigned to the specimen while collecting, this can be barcode or any other identifier", - ) - - -class SpecimenSendToLabRequest(BaseModel): - lab: UUID4 = Field( - description="The laboratory to which the specimen is being sent", - ) - - -class SpecimenReceiveAtLabRequest(BaseModel): - accession_identifier: str | None = Field( - default=None, - description="The identifier assigned to the specimen by the laboratory", - ) - - condition: list[CodeableConcept] | None = Field( - default=None, - description="The condition of the specimen while received at the laboratory", - ) - - note: str | None = Field( - default=None, - description="Comments made about the specimen while received at the laboratory", - ) - - @field_validator("condition") - @classmethod - def validate_condition(cls, value: CodeableConcept): - return SpecimenSpec.validate_condition(value) - - -class SpecimenProcessRequest(BaseModel): - process: list[SpecimenProcessingSpec] = Field( - description="The processing steps that have been performed on the specimen", - ) From de165f90e1d375bd446de9f59b5948eca7c98185 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 11 Jan 2025 00:46:08 +0530 Subject: [PATCH 30/32] added authz --- care/emr/api/viewsets/diagnostic_report.py | 6 +++++- care/emr/api/viewsets/service_request.py | 3 ++- care/emr/api/viewsets/specimen.py | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/care/emr/api/viewsets/diagnostic_report.py b/care/emr/api/viewsets/diagnostic_report.py index c412ec7c0c..bc86a7e415 100644 --- a/care/emr/api/viewsets/diagnostic_report.py +++ b/care/emr/api/viewsets/diagnostic_report.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase from care.emr.models.diagnostic_report import DiagnosticReport from care.emr.models.observation import Observation from care.emr.resources.diagnostic_report.spec import ( @@ -38,7 +39,7 @@ class DiagnosticReportFilters(FilterSet): ) -class DiagnosticReportViewSet(EMRModelViewSet): +class DiagnosticReportViewSet(EncounterBasedAuthorizationBase, EMRModelViewSet): database_model = DiagnosticReport pydantic_model = DiagnosticReportCreateSpec pydantic_update_model = DiagnosticReportUpdateSpec @@ -66,6 +67,7 @@ class DiagnosticReportObservationRequest(BaseModel): def observations(self, request, *args, **kwargs): data = self.DiagnosticReportObservationRequest(**request.data) report: DiagnosticReport = self.get_object() + self.authorize_update({}, report) observations = [] for observation in data.observations: @@ -105,6 +107,7 @@ class DiagnosticReportVerifyRequest(BaseModel): def verify(self, request, *args, **kwargs): data = self.DiagnosticReportVerifyRequest(**request.data) report: DiagnosticReport = self.get_object() + self.authorize_update({}, report) if data.is_approved: report.status = StatusChoices.preliminary @@ -136,6 +139,7 @@ class DiagnosticReportReviewRequest(BaseModel): def review(self, request, *args, **kwargs): data = self.DiagnosticReportReviewRequest(**request.data) report: DiagnosticReport = self.get_object() + self.authorize_update({}, report) if ( report.results_interpreter diff --git a/care/emr/api/viewsets/service_request.py b/care/emr/api/viewsets/service_request.py index 9b727b057c..3202711502 100644 --- a/care/emr/api/viewsets/service_request.py +++ b/care/emr/api/viewsets/service_request.py @@ -2,6 +2,7 @@ from django_filters.rest_framework import DjangoFilterBackend from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase from care.emr.models.service_request import ServiceRequest from care.emr.resources.service_request.spec import ( ServiceRequestCreateSpec, @@ -24,7 +25,7 @@ class ServiceRequestFilters(FilterSet): ) -class ServiceRequestViewSet(EMRModelViewSet): +class ServiceRequestViewSet(EncounterBasedAuthorizationBase, EMRModelViewSet): database_model = ServiceRequest pydantic_model = ServiceRequestCreateSpec pydantic_update_model = ServiceRequestUpdateSpec diff --git a/care/emr/api/viewsets/specimen.py b/care/emr/api/viewsets/specimen.py index 92e744a9a4..332c18301b 100644 --- a/care/emr/api/viewsets/specimen.py +++ b/care/emr/api/viewsets/specimen.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase from care.emr.fhir.schema.base import CodeableConcept from care.emr.models.specimen import Specimen from care.emr.resources.specimen.spec import ( @@ -36,7 +37,7 @@ class SpecimenFilters(FilterSet): ) -class SpecimenViewSet(EMRModelViewSet): +class SpecimenViewSet(EncounterBasedAuthorizationBase, EMRModelViewSet): database_model = Specimen pydantic_model = SpecimenCreateSpec pydantic_update_model = SpecimenUpdateSpec @@ -46,6 +47,7 @@ class SpecimenViewSet(EMRModelViewSet): filterset_class = SpecimenFilters def get_object(self) -> Specimen: + self.authorize_read_encounter() return get_object_or_404( self.get_queryset(), Q(external_id__iexact=self.kwargs[self.lookup_field]) @@ -68,6 +70,7 @@ class SpecimenCollectRequest(BaseModel): def collect(self, request, *args, **kwargs): data = self.SpecimenCollectRequest(**request.data) specimen = self.get_object() + self.authorize_update({}, specimen) specimen.identifier = data.identifier specimen.status = StatusChoices.available @@ -94,6 +97,7 @@ def send_to_lab(self, request, *args, **kwargs): data = self.SpecimenSendToLabRequest(**request.data) specimen = self.get_object() service_request = specimen.request + self.authorize_update({}, specimen) service_request.location = data.lab specimen.dispatched_at = datetime.now(UTC) @@ -135,6 +139,7 @@ def validate_condition(cls, value: CodeableConcept): def receive_at_lab(self, request, *args, **kwargs): data = self.SpecimenReceiveAtLabRequest(**request.data) specimen = self.get_object() + self.authorize_update({}, specimen) specimen.accession_identifier = data.accession_identifier specimen.condition = data.condition @@ -161,6 +166,7 @@ class SpecimenProcessRequest(BaseModel): def process(self, request, *args, **kwargs): data = self.SpecimenProcessRequest(**request.data) specimen = self.get_object() + self.authorize_update({}, specimen) processes = [] for process in data.process: From 4dbf130a7aaace1588981534140c16e34b64a6d7 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 11 Jan 2025 00:54:35 +0530 Subject: [PATCH 31/32] fixed migration conflict --- care/emr/migrations/0008_merge_20250111_0054.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 care/emr/migrations/0008_merge_20250111_0054.py diff --git a/care/emr/migrations/0008_merge_20250111_0054.py b/care/emr/migrations/0008_merge_20250111_0054.py new file mode 100644 index 0000000000..b05ec23490 --- /dev/null +++ b/care/emr/migrations/0008_merge_20250111_0054.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.3 on 2025-01-10 19:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0006_alter_patient_blood_group'), + ('emr', '0007_servicerequest_phase'), + ] + + operations = [ + ] From 48f88e6dfb9ed8a10415e4e4679c76f9cf3374be Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 14 Jan 2025 11:59:00 +0530 Subject: [PATCH 32/32] added based on and subject in diagnostic report list spec --- care/emr/resources/diagnostic_report/spec.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/care/emr/resources/diagnostic_report/spec.py b/care/emr/resources/diagnostic_report/spec.py index a198eb9b25..665c8e8cfb 100644 --- a/care/emr/resources/diagnostic_report/spec.py +++ b/care/emr/resources/diagnostic_report/spec.py @@ -189,6 +189,13 @@ class DiagnosticReportListSpec(DiagnosticReportSpec): def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id + if obj.based_on: + mapping["based_on"] = ServiceRequestRetrieveSpec.serialize( + obj.based_on + ).to_json() + if obj.subject: + mapping["subject"] = PatientRetrieveSpec.serialize(obj.subject).to_json() + class DiagnosticReportRetrieveSpec(DiagnosticReportListSpec): @classmethod