diff --git a/care/emr/api/viewsets/charge_item.py b/care/emr/api/viewsets/charge_item.py index ffcb00575f..b41a9ccc26 100644 --- a/care/emr/api/viewsets/charge_item.py +++ b/care/emr/api/viewsets/charge_item.py @@ -324,6 +324,13 @@ def apply_charge_item_defs(self, request, *args, **kwargs): raise ValidationError( "Charge item definition is not associated with the facility" ) + if ( + not charge_item_definition.separately_billable + and not charge_item_request.service_resource + ): + raise ValidationError( + "Charge item definition cannot be applied manually. It must be linked to a resource." + ) patient = None encounter = None if charge_item_request.encounter: diff --git a/care/emr/migrations/0066_chargeitemdefinition_separately_billable.py b/care/emr/migrations/0066_chargeitemdefinition_separately_billable.py new file mode 100644 index 0000000000..1580c09e9e --- /dev/null +++ b/care/emr/migrations/0066_chargeitemdefinition_separately_billable.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2026-01-27 07:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0065_account_primary_encounter'), + ] + + operations = [ + migrations.AddField( + model_name='chargeitemdefinition', + name='separately_billable', + field=models.BooleanField(default=True), + ), + ] diff --git a/care/emr/migrations/0067_sync_separately_billable_values.py b/care/emr/migrations/0067_sync_separately_billable_values.py new file mode 100644 index 0000000000..f54b2e24ff --- /dev/null +++ b/care/emr/migrations/0067_sync_separately_billable_values.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2026-01-27 07:48 + +from django.db import migrations + + +def sync_separately_billable(apps, schema_editor): + ChargeItemDefinition = apps.get_model("emr", "ChargeItemDefinition") + Product = apps.get_model("emr", "Product") + Schedule = apps.get_model("emr", "Schedule") + ActivityDefinition = apps.get_model("emr", "ActivityDefinition") + + for charge_item_def in ChargeItemDefinition.objects.all(): + is_linked = ( + Product.objects.filter(charge_item_definition_id=charge_item_def.id).exists() + or Schedule.objects.filter(charge_item_definition_id=charge_item_def.id).exists() + or Schedule.objects.filter(revisit_charge_item_definition_id=charge_item_def.id).exists() + or ActivityDefinition.objects.filter(charge_item_definitions__contains=[charge_item_def.id]).exists() + ) + + if is_linked and charge_item_def.separately_billable: + charge_item_def.separately_billable = False + charge_item_def.save(update_fields=["separately_billable"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0066_chargeitemdefinition_separately_billable'), + ] + + operations = [ + migrations.RunPython(sync_separately_billable), + ] diff --git a/care/emr/models/charge_item_definition.py b/care/emr/models/charge_item_definition.py index 9ce3dcd02d..b0c134e474 100644 --- a/care/emr/models/charge_item_definition.py +++ b/care/emr/models/charge_item_definition.py @@ -24,3 +24,4 @@ class ChargeItemDefinition(SlugBaseModel): null=True, blank=True, ) + separately_billable = models.BooleanField(default=True) diff --git a/care/emr/resources/charge_item_definition/spec.py b/care/emr/resources/charge_item_definition/spec.py index 75e0c6bf98..edb19b472f 100644 --- a/care/emr/resources/charge_item_definition/spec.py +++ b/care/emr/resources/charge_item_definition/spec.py @@ -31,6 +31,7 @@ class ChargeItemDefinitionSpec(EMRResource): description: str | None = None purpose: str | None = None price_components: list[MonetaryComponent] + separately_billable: bool = True class ChargeItemDefinitionWriteSpec(ChargeItemDefinitionSpec): diff --git a/care/emr/signals/__init__.py b/care/emr/signals/__init__.py index 52d8f470f6..545e8bd1b9 100644 --- a/care/emr/signals/__init__.py +++ b/care/emr/signals/__init__.py @@ -1 +1,2 @@ +from .charge_item_def import * # noqa F403 from .patient import * # noqa F403 diff --git a/care/emr/signals/charge_item_def/__init__.py b/care/emr/signals/charge_item_def/__init__.py new file mode 100644 index 0000000000..4ccc39516a --- /dev/null +++ b/care/emr/signals/charge_item_def/__init__.py @@ -0,0 +1,3 @@ +from .activity_definition import * # noqa F403 +from .product import * # noqa F403 +from .schedule import * # noqa F403 diff --git a/care/emr/signals/charge_item_def/activity_definition.py b/care/emr/signals/charge_item_def/activity_definition.py new file mode 100644 index 0000000000..8ed7c4bc66 --- /dev/null +++ b/care/emr/signals/charge_item_def/activity_definition.py @@ -0,0 +1,49 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from care.emr.models.activity_definition import ActivityDefinition +from care.emr.utils.charge_item_definition import ( + recalculate_separately_billable, + set_separately_billable_false, +) + +_OLD_CHARGE_ITEM_DEFS_IDS = "_old_charge_item_definitions" +_OLD_DELETED = "_old_deleted" + + +@receiver(pre_save, sender=ActivityDefinition) +def capture_old_charge_item_defs_activity(sender, instance, **kwargs): + if instance.id: + old_instance = ActivityDefinition.objects.get(id=instance.id) + setattr( + instance, + _OLD_CHARGE_ITEM_DEFS_IDS, + list(old_instance.charge_item_definitions or []), + ) + setattr(instance, _OLD_DELETED, old_instance.deleted) + else: + setattr(instance, _OLD_CHARGE_ITEM_DEFS_IDS, []) + setattr(instance, _OLD_DELETED, False) + + +@receiver(post_save, sender=ActivityDefinition) +def update_separately_billable_activity(sender, instance, created, **kwargs): + old_ids = set(getattr(instance, _OLD_CHARGE_ITEM_DEFS_IDS, []) or []) + new_ids = set(instance.charge_item_definitions or []) + old_deleted = getattr(instance, _OLD_DELETED, False) + + is_soft_deleted = not old_deleted and instance.deleted + + if is_soft_deleted: + for charge_item_def_id in new_ids: + recalculate_separately_billable(charge_item_def_id) + return + + added_ids = new_ids - old_ids + for charge_item_def_id in added_ids: + if not instance.deleted: + set_separately_billable_false(charge_item_def_id) + + removed_ids = old_ids - new_ids + for charge_item_def_id in removed_ids: + recalculate_separately_billable(charge_item_def_id) diff --git a/care/emr/signals/charge_item_def/product.py b/care/emr/signals/charge_item_def/product.py new file mode 100644 index 0000000000..ff41a679c0 --- /dev/null +++ b/care/emr/signals/charge_item_def/product.py @@ -0,0 +1,43 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from care.emr.models.product import Product +from care.emr.utils.charge_item_definition import ( + recalculate_separately_billable, + set_separately_billable_false, +) + +_OLD_CHARGE_ITEM_DEF_ID = "_old_charge_item_definition_id" +_OLD_DELETED = "_old_deleted" + + +@receiver(pre_save, sender=Product) +def capture_old_charge_item_def_product(sender, instance, **kwargs): + if instance.id: + old_instance = Product.objects.get(id=instance.id) + setattr( + instance, _OLD_CHARGE_ITEM_DEF_ID, old_instance.charge_item_definition_id + ) + setattr(instance, _OLD_DELETED, old_instance.deleted) + else: + setattr(instance, _OLD_CHARGE_ITEM_DEF_ID, None) + setattr(instance, _OLD_DELETED, False) + + +@receiver(post_save, sender=Product) +def update_separately_billable_product(sender, instance, created, **kwargs): + old_charge_item_def_id = getattr(instance, _OLD_CHARGE_ITEM_DEF_ID, None) + new_charge_item_def_id = instance.charge_item_definition_id + old_deleted = getattr(instance, _OLD_DELETED, False) + + is_soft_deleted = not old_deleted and instance.deleted + + if is_soft_deleted and new_charge_item_def_id: + recalculate_separately_billable(new_charge_item_def_id) + return + + if old_charge_item_def_id and old_charge_item_def_id != new_charge_item_def_id: + recalculate_separately_billable(old_charge_item_def_id) + + if new_charge_item_def_id and not instance.deleted: + set_separately_billable_false(new_charge_item_def_id) diff --git a/care/emr/signals/charge_item_def/schedule.py b/care/emr/signals/charge_item_def/schedule.py new file mode 100644 index 0000000000..f6e2c7455c --- /dev/null +++ b/care/emr/signals/charge_item_def/schedule.py @@ -0,0 +1,61 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from care.emr.models.scheduling.schedule import Schedule +from care.emr.utils.charge_item_definition import ( + recalculate_separately_billable, + set_separately_billable_false, +) + +_OLD_CHARGE_ITEM_DEF_ID = "_old_charge_item_definition_id" +_OLD_REVISIT_CHARGE_ITEM_DEF_ID = "_old_revisit_charge_item_definition_id" +_OLD_DELETED = "_old_deleted" + + +@receiver(pre_save, sender=Schedule) +def capture_old_charge_item_def_schedule(sender, instance, **kwargs): + if instance.id: + old_instance = Schedule.objects.get(id=instance.id) + setattr( + instance, _OLD_CHARGE_ITEM_DEF_ID, old_instance.charge_item_definition_id + ) + setattr( + instance, + _OLD_REVISIT_CHARGE_ITEM_DEF_ID, + old_instance.revisit_charge_item_definition_id, + ) + setattr(instance, _OLD_DELETED, old_instance.deleted) + else: + setattr(instance, _OLD_CHARGE_ITEM_DEF_ID, None) + setattr(instance, _OLD_REVISIT_CHARGE_ITEM_DEF_ID, None) + setattr(instance, _OLD_DELETED, False) + + +@receiver(post_save, sender=Schedule) +def update_separately_billable_schedule(sender, instance, created, **kwargs): + old_charge_item_def_id = getattr(instance, _OLD_CHARGE_ITEM_DEF_ID, None) + new_charge_item_def_id = instance.charge_item_definition_id + old_revisit_id = getattr(instance, _OLD_REVISIT_CHARGE_ITEM_DEF_ID, None) + new_revisit_id = instance.revisit_charge_item_definition_id + old_deleted = getattr(instance, _OLD_DELETED, False) + + is_soft_deleted = not old_deleted and instance.deleted + + if is_soft_deleted: + if new_charge_item_def_id: + recalculate_separately_billable(new_charge_item_def_id) + if new_revisit_id: + recalculate_separately_billable(new_revisit_id) + return + + if old_charge_item_def_id and old_charge_item_def_id != new_charge_item_def_id: + recalculate_separately_billable(old_charge_item_def_id) + + if new_charge_item_def_id and not instance.deleted: + set_separately_billable_false(new_charge_item_def_id) + + if old_revisit_id and old_revisit_id != new_revisit_id: + recalculate_separately_billable(old_revisit_id) + + if new_revisit_id and not instance.deleted: + set_separately_billable_false(new_revisit_id) diff --git a/care/emr/tests/test_charge_item_api.py b/care/emr/tests/test_charge_item_api.py index 88cb708dfd..e4d454b6c9 100644 --- a/care/emr/tests/test_charge_item_api.py +++ b/care/emr/tests/test_charge_item_api.py @@ -1896,3 +1896,178 @@ def test_apply_charge_item_definition_request_defaults(self): self.assertIsNone(request.patient) self.assertIsNone(request.service_resource) self.assertIsNone(request.service_resource_id) + + +class TestSeparatelyBillableChargeItemDefinition(CareAPITestBase): + """Tests for the separately_billable flag on ChargeItemDefinition.""" + + def setUp(self): + super().setUp() + self.user = self.create_user() + self.facility = self.create_facility(user=self.user) + self.organization = self.create_facility_organization(facility=self.facility) + self.patient = self.create_patient() + self.encounter = self.create_encounter( + patient=self.patient, facility=self.facility, organization=self.organization + ) + + self.account = Account.objects.create( + facility=self.facility, + patient=self.patient, + name=f"Account for {self.patient.name}", + status=AccountStatusOptions.active.value, + billing_status=AccountBillingStatusOptions.open.value, + ) + + # Create a separately billable charge item definition (default) + self.separately_billable_definition = ChargeItemDefinition.objects.create( + facility=self.facility, + status=ChargeItemDefinitionStatusOptions.active.value, + title="Separately Billable Definition", + slug=f"f-{self.facility.external_id}-separately-billable-def", + price_components=[ + { + "monetary_component_type": "base", + "currency": "INR", + "amount": "100.00", + } + ], + separately_billable=True, + ) + + # Create a non-separately billable charge item definition + self.non_separately_billable_definition = ChargeItemDefinition.objects.create( + facility=self.facility, + status=ChargeItemDefinitionStatusOptions.active.value, + title="Non-Separately Billable Definition", + slug=f"f-{self.facility.external_id}-non-separately-billable-def", + price_components=[ + { + "monetary_component_type": "base", + "currency": "INR", + "amount": "200.00", + } + ], + separately_billable=False, + ) + + self.base_url = reverse( + "charge_item-list", + kwargs={"facility_external_id": self.facility.external_id}, + ) + + def test_separately_billable_default_is_true(self): + """Test that separately_billable defaults to True.""" + definition = ChargeItemDefinition.objects.create( + facility=self.facility, + status=ChargeItemDefinitionStatusOptions.active.value, + title="Default Definition", + slug=f"f-{self.facility.external_id}-default-def", + price_components=[], + ) + self.assertTrue(definition.separately_billable) + + def test_apply_separately_billable_definition_without_service_resource(self): + """Test that separately billable definition can be applied without service_resource.""" + role = self.create_role_with_permissions( + [ChargeItemPermissions.can_create_charge_item.name] + ) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.client.force_authenticate(user=self.user) + + url = f"{self.base_url}apply_charge_item_defs/" + data = { + "requests": [ + { + "charge_item_definition": self.separately_billable_definition.slug, + "quantity": 1, + "encounter": self.encounter.external_id, + } + ] + } + + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_apply_non_separately_billable_definition_without_service_resource_fails( + self, + ): + """Test that non-separately billable definition cannot be applied without service_resource.""" + role = self.create_role_with_permissions( + [ChargeItemPermissions.can_create_charge_item.name] + ) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.client.force_authenticate(user=self.user) + + url = f"{self.base_url}apply_charge_item_defs/" + data = { + "requests": [ + { + "charge_item_definition": self.non_separately_billable_definition.slug, + "quantity": 1, + "encounter": self.encounter.external_id, + } + ] + } + + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("cannot be applied manually", str(response.data)) + + def test_apply_non_separately_billable_definition_with_service_resource_succeeds( + self, + ): + """Test that non-separately billable definition can be applied with service_resource.""" + role = self.create_role_with_permissions( + [ChargeItemPermissions.can_create_charge_item.name] + ) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.client.force_authenticate(user=self.user) + + service_request = self.create_service_request( + patient=self.patient, facility=self.facility, encounter=self.encounter + ) + + url = f"{self.base_url}apply_charge_item_defs/" + data = { + "requests": [ + { + "charge_item_definition": self.non_separately_billable_definition.slug, + "quantity": 1, + "encounter": self.encounter.external_id, + "service_resource": ChargeItemResourceOptions.service_request.value, + "service_resource_id": str(service_request.external_id), + } + ] + } + + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_apply_multiple_definitions_with_mixed_separately_billable_fails(self): + """Test that batch apply fails when one definition is non-separately billable without service_resource.""" + role = self.create_role_with_permissions( + [ChargeItemPermissions.can_create_charge_item.name] + ) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.client.force_authenticate(user=self.user) + + url = f"{self.base_url}apply_charge_item_defs/" + data = { + "requests": [ + { + "charge_item_definition": self.separately_billable_definition.slug, + "quantity": 1, + "encounter": self.encounter.external_id, + }, + { + "charge_item_definition": self.non_separately_billable_definition.slug, + "quantity": 1, + "encounter": self.encounter.external_id, + }, + ] + } + + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("cannot be applied manually", str(response.data)) diff --git a/care/emr/tests/test_charge_item_def_signals.py b/care/emr/tests/test_charge_item_def_signals.py new file mode 100644 index 0000000000..1500a28d04 --- /dev/null +++ b/care/emr/tests/test_charge_item_def_signals.py @@ -0,0 +1,280 @@ +from datetime import UTC, datetime, timedelta + +from care.emr.models import SchedulableResource, Schedule +from care.emr.models.activity_definition import ActivityDefinition +from care.emr.models.charge_item_definition import ChargeItemDefinition +from care.emr.models.product import Product +from care.emr.resources.scheduling.schedule.spec import SchedulableResourceTypeOptions +from care.utils.tests.base import CareAPITestBase + + +class TestChargeItemDefinitionSignals(CareAPITestBase): + def setUp(self): + super().setUp() + self.user = self.create_user() + self.facility = self.create_facility(user=self.user) + + def create_charge_item_definition(self, **kwargs): + data = { + "facility": self.facility, + "status": "active", + "title": self.fake.sentence(nb_words=4), + "slug": f"f-{self.facility.external_id}-{self.fake.slug()}", + } + data.update(**kwargs) + return ChargeItemDefinition.objects.create(**data) + + def create_product(self, charge_item_definition=None, **kwargs): + from model_bakery import baker + + product_knowledge = baker.make("emr.ProductKnowledge", facility=self.facility) + data = { + "facility": self.facility, + "product_knowledge": product_knowledge, + "charge_item_definition": charge_item_definition, + "status": "active", + "product_type": "medication", + } + data.update(**kwargs) + return Product.objects.create(**data) + + def create_schedule( + self, charge_item_definition=None, revisit_charge_item_definition=None, **kwargs + ): + resource = SchedulableResource.objects.create( + resource_type=SchedulableResourceTypeOptions.practitioner.value, + user=self.user, + facility=self.facility, + ) + data = { + "resource": resource, + "name": "Test Schedule", + "valid_from": datetime.now(UTC), + "valid_to": datetime.now(UTC) + timedelta(days=30), + "charge_item_definition": charge_item_definition, + "revisit_charge_item_definition": revisit_charge_item_definition, + } + data.update(**kwargs) + return Schedule.objects.create(**data) + + def create_activity_definition(self, charge_item_definitions=None, **kwargs): + data = { + "facility": self.facility, + "slug": f"f-{self.facility.external_id}-{self.fake.slug()}", + "title": self.fake.sentence(nb_words=4), + "classification": "test", + "status": "active", + "description": self.fake.text(), + "usage": self.fake.text(), + "kind": "ServiceRequest", + "charge_item_definitions": charge_item_definitions or [], + } + data.update(**kwargs) + return ActivityDefinition.objects.create(**data) + + # Product Signal Tests + + def test_product_link_sets_separately_billable_false(self): + charge_item_def = self.create_charge_item_definition() + self.assertTrue(charge_item_def.separately_billable) + + self.create_product(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + def test_product_unlink_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + product = self.create_product(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + product.charge_item_definition = None + product.save() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + def test_product_delete_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + product = self.create_product(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + product.delete() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + def test_product_change_link_recalculates_old_and_sets_new(self): + charge_item_def_1 = self.create_charge_item_definition() + charge_item_def_2 = self.create_charge_item_definition() + product = self.create_product(charge_item_definition=charge_item_def_1) + + charge_item_def_1.refresh_from_db() + self.assertFalse(charge_item_def_1.separately_billable) + + product.charge_item_definition = charge_item_def_2 + product.save() + + charge_item_def_1.refresh_from_db() + charge_item_def_2.refresh_from_db() + self.assertTrue(charge_item_def_1.separately_billable) + self.assertFalse(charge_item_def_2.separately_billable) + + # Schedule Signal Tests + + def test_schedule_link_sets_separately_billable_false(self): + charge_item_def = self.create_charge_item_definition() + self.assertTrue(charge_item_def.separately_billable) + + self.create_schedule(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + def test_schedule_revisit_link_sets_separately_billable_false(self): + charge_item_def = self.create_charge_item_definition() + self.assertTrue(charge_item_def.separately_billable) + + self.create_schedule(revisit_charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + def test_schedule_unlink_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + schedule = self.create_schedule(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + schedule.charge_item_definition = None + schedule.save() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + def test_schedule_delete_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + schedule = self.create_schedule(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + schedule.delete() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + # ActivityDefinition Signal Tests + + def test_activity_definition_link_sets_separately_billable_false(self): + charge_item_def = self.create_charge_item_definition() + self.assertTrue(charge_item_def.separately_billable) + + self.create_activity_definition(charge_item_definitions=[charge_item_def.id]) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + def test_activity_definition_unlink_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + activity_def = self.create_activity_definition( + charge_item_definitions=[charge_item_def.id] + ) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + activity_def.charge_item_definitions = [] + activity_def.save() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + def test_activity_definition_delete_recalculates_separately_billable(self): + charge_item_def = self.create_charge_item_definition() + activity_def = self.create_activity_definition( + charge_item_definitions=[charge_item_def.id] + ) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + activity_def.delete() + + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) + + def test_activity_definition_add_multiple_charge_items(self): + charge_item_def_1 = self.create_charge_item_definition() + charge_item_def_2 = self.create_charge_item_definition() + + self.create_activity_definition( + charge_item_definitions=[charge_item_def_1.id, charge_item_def_2.id] + ) + + charge_item_def_1.refresh_from_db() + charge_item_def_2.refresh_from_db() + self.assertFalse(charge_item_def_1.separately_billable) + self.assertFalse(charge_item_def_2.separately_billable) + + def test_activity_definition_partial_unlink(self): + charge_item_def_1 = self.create_charge_item_definition() + charge_item_def_2 = self.create_charge_item_definition() + activity_def = self.create_activity_definition( + charge_item_definitions=[charge_item_def_1.id, charge_item_def_2.id] + ) + + activity_def.charge_item_definitions = [charge_item_def_1.id] + activity_def.save() + + charge_item_def_1.refresh_from_db() + charge_item_def_2.refresh_from_db() + self.assertFalse(charge_item_def_1.separately_billable) + self.assertTrue(charge_item_def_2.separately_billable) + + # Cross-model Tests + + def test_unlink_keeps_false_if_linked_elsewhere(self): + charge_item_def = self.create_charge_item_definition() + product = self.create_product(charge_item_definition=charge_item_def) + self.create_schedule(charge_item_definition=charge_item_def) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + product.charge_item_definition = None + product.save() + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + def test_multiple_links_all_removed(self): + charge_item_def = self.create_charge_item_definition() + product = self.create_product(charge_item_definition=charge_item_def) + schedule = self.create_schedule(charge_item_definition=charge_item_def) + activity_def = self.create_activity_definition( + charge_item_definitions=[charge_item_def.id] + ) + + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + product.charge_item_definition = None + product.save() + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + schedule.charge_item_definition = None + schedule.save() + charge_item_def.refresh_from_db() + self.assertFalse(charge_item_def.separately_billable) + + activity_def.charge_item_definitions = [] + activity_def.save() + charge_item_def.refresh_from_db() + self.assertTrue(charge_item_def.separately_billable) diff --git a/care/emr/utils/charge_item_definition.py b/care/emr/utils/charge_item_definition.py new file mode 100644 index 0000000000..b999395937 --- /dev/null +++ b/care/emr/utils/charge_item_definition.py @@ -0,0 +1,41 @@ +from care.emr.models.activity_definition import ActivityDefinition +from care.emr.models.charge_item_definition import ChargeItemDefinition +from care.emr.models.product import Product +from care.emr.models.scheduling.schedule import Schedule + + +def is_linked_to_any_resource(charge_item_def_id: int) -> bool: + if Product.objects.filter(charge_item_definition_id=charge_item_def_id).exists(): + return True + + if Schedule.objects.filter(charge_item_definition_id=charge_item_def_id).exists(): + return True + + if Schedule.objects.filter( + revisit_charge_item_definition_id=charge_item_def_id + ).exists(): + return True + + return ActivityDefinition.objects.filter( + charge_item_definitions__contains=[charge_item_def_id] + ).exists() + + +def recalculate_separately_billable(charge_item_def_id: int) -> None: + try: + charge_item_def = ChargeItemDefinition.objects.get(id=charge_item_def_id) + except ChargeItemDefinition.DoesNotExist: + return + + is_linked = is_linked_to_any_resource(charge_item_def_id) + new_value = not is_linked + + if charge_item_def.separately_billable != new_value: + charge_item_def.separately_billable = new_value + charge_item_def.save(update_fields=["separately_billable"]) + + +def set_separately_billable_false(charge_item_def_id: int) -> None: + ChargeItemDefinition.objects.filter(id=charge_item_def_id).update( + separately_billable=False + )