Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion care/emr/api/viewsets/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class InvoiceFilters(filters.FilterSet):
status = filters.CharFilter(lookup_expr="iexact")
title = filters.CharFilter(lookup_expr="icontains")
account = filters.UUIDFilter(field_name="account__external_id")
encounter = filters.UUIDFilter(field_name="encounter__external_id")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invoice model doesnt has encounter field

patient = filters.UUIDFilter(field_name="patient__external_id")
number = filters.CharFilter(lookup_expr="icontains")
locked = filters.BooleanFilter()
Expand Down
8 changes: 7 additions & 1 deletion care/emr/reports/authorizers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from . import discharge_summary
from .account import AccountReportAuthorizer
from .base import BaseReportAuthorizer
from .discharge_summary import DischargeSummaryReportAuthorizer
from .discharge_summary import (
DischargeSummaryReportAuthorizer,
)
from .encounter import EncounterReportAuthorizer
from .patient import PatientReportAuthorizer
from .utils import report_authorizer

__all__ = [
"AccountReportAuthorizer",
"BaseReportAuthorizer",
"DischargeSummaryReportAuthorizer",
"EncounterReportAuthorizer",
"PatientReportAuthorizer",
"report_authorizer",
]
18 changes: 18 additions & 0 deletions care/emr/reports/authorizers/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from care.emr.models.account import Account
from care.emr.reports.authorizers.base import BaseReportAuthorizer
from care.security.authorization.base import AuthorizationController
from care.utils.shortcuts import get_object_or_404


class AccountReportAuthorizer(BaseReportAuthorizer):
def authorize_read(self, user, associating_id: str) -> bool:
account_obj = get_object_or_404(Account, external_id=associating_id)
return AuthorizationController.call(
"can_read_account_in_facility", user, account_obj
)

def authorize_write(self, user, associating_id: str) -> bool:
account_obj = get_object_or_404(Account, external_id=associating_id)
return AuthorizationController.call(
"can_update_account_in_facility", user, account_obj
)
14 changes: 14 additions & 0 deletions care/emr/reports/authorizers/patient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from care.emr.models.patient import Patient
from care.emr.reports.authorizers.base import BaseReportAuthorizer
from care.security.authorization import AuthorizationController
from care.utils.shortcuts import get_object_or_404


class PatientReportAuthorizer(BaseReportAuthorizer):
def authorize_read(self, user, associating_id: str) -> bool:
patient_obj = get_object_or_404(Patient, external_id=associating_id)
return AuthorizationController.call("can_view_clinical_data", user, patient_obj)

def authorize_write(self, user, associating_id: str) -> bool:
patient_obj = get_object_or_404(Patient, external_id=associating_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be clinical write permission, or encounter write permission (Not encounter create either)

return AuthorizationController.call("can_write_patient_obj", user, patient_obj)
2 changes: 2 additions & 0 deletions care/emr/reports/context_builder/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .data_points.encounter import * # noqa F403
from .data_points.patient import * # noqa F403
from .data_points.account import * # noqa F403
137 changes: 137 additions & 0 deletions care/emr/reports/context_builder/data_points/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from care.emr.models.account import Account
from care.emr.reports.context_builder.data_point_registry import DataPointRegistry
from care.emr.reports.context_builder.data_points.base import (
Field,
SingleObjectContextBuilder,
)
from care.emr.reports.context_builder.data_points.charge_items import (
AccountChargeItemContextBuilder,
)
from care.emr.reports.context_builder.data_points.invoice import (
AccountInvoiceContextBuilder,
)
from care.emr.reports.context_builder.data_points.monetary_component import (
MonetaryComponentContextBuilder,
)
from care.emr.reports.context_builder.data_points.payment_reconciliation import (
PaymentReconciliationContextBuilder,
)

STATUS_DISPLAY = {
"active": "Active",
"inactive": "Inactive",
"entered_in_error": "Entered in Error",
"on_hold": "On Hold",
}
BILLING_STATUS_DISPLAY = {
"open": "Open",
"carecomplete_notbilled": "CareComplete Not Billed",
"billing": "Billing",
"closed_baddebt": "Closed Bad Debt",
"closed_voided": "Closed Voided",
"closed_completed": "Closed Completed",
"closed_combined": "Closed Combined",
}


class BaseAccountContextBuilder(SingleObjectContextBuilder):
name = Field(
display="Account Title",
preview_value="General Checkup Account",
description="Title of the account",
)
status = Field(
display="Account Status",
preview_value="Active",
mapping=lambda a: STATUS_DISPLAY.get(a.status, a.status.title())
if a.status
else "",
description="Current status of the account",
)
billing_status = Field(
display="Account Billing Status",
preview_value="Billed",
mapping=lambda a: BILLING_STATUS_DISPLAY.get(
a.billing_status, a.billing_status.title()
)
if a.billing_status
else "",
description="Billing status of the account",
)
description = Field(
display="Account Description",
preview_value="Account for general health checkup",
description="Detailed description of the account",
)
total_net = Field(
display="Total Net Amount",
preview_value="150.00",
description="Total net amount for the account",
)
total_gross = Field(
display="Total Gross Amount",
preview_value="180.00",
description="Total gross amount for the account",
)
total_paid = Field(
display="Total Paid Amount",
preview_value="100.00",
description="Total amount paid towards the account",
)
total_balance = Field(
display="Total Balance Amount",
preview_value="80.00",
description="Total balance amount remaining for the account",
)
total_price_components = Field(
display="Total Price Components",
preview_value="",
target_context=MonetaryComponentContextBuilder,
description="Breakdown of total price components for the account",
)
invoices = Field(
display="Associated Invoices",
preview_value="",
target_context=AccountInvoiceContextBuilder,
description="Invoices linked to the account",
)
charge_items = Field(
display="Billable Charge Items",
preview_value="",
target_context=AccountChargeItemContextBuilder,
description="Chargeable items associated with the account",
)
payment_reconciliations = Field(
display="Payment Reconciliations",
preview_value="",
target_context=PaymentReconciliationContextBuilder,
description="Payment reconciliations for the account",
)
created_date = Field(
display="Account Created Date",
preview_value="2023-01-15T10:30:00Z",
description="Date when the account was created",
)
calculated_at = Field(
display="Account Calculated At",
preview_value="2023-01-20T15:45:00Z",
description="Date when the account totals were last calculated",
)


class PatientAccountContextBuilder(BaseAccountContextBuilder):
def get_context(self):
accounts = Account.objects.filter(patient=self.parent_context)
return accounts.first()


class AccountContextBuilder(BaseAccountContextBuilder):
standalone_context = True
__slug__ = "account_base"
__associating_model__ = Account
__display_name__ = "Account Report"
__description__ = "Report context for account-based reports"
context_key = "account"


DataPointRegistry.register(AccountContextBuilder)
100 changes: 100 additions & 0 deletions care/emr/reports/context_builder/data_points/charge_items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from django_filters import rest_framework as filters

from care.emr.models.charge_item import ChargeItem
from care.emr.reports.context_builder.data_points.base import (
Field,
QuerysetContextBuilder,
)
from care.emr.reports.context_builder.data_points.monetary_component import (
MonetaryComponentContextBuilder,
UnitPriceMonetaryComponentContextBuilder,
)

CHARGE_ITEM_RESOURCE_DISPLAY = {
"service_request": "Service Request",
"medication_dispense": "Medication Dispense",
"appointment": "Appointment",
"bed_association": "Bed Association",
}
CHARGE_ITEM_STATUS_DISPLAY = {
"planned": "Planned",
"billable": "Billable",
"not_billable": "Not Billable",
"aborted": "Aborted",
"billed": "Billed",
"paid": "Paid",
"entered_in_error": "Entered in Error",
}


class ChargeItemReportFilter(filters.FilterSet):
status = filters.CharFilter(lookup_expr="iexact")
title = filters.CharFilter(lookup_expr="icontains")
service_resource = filters.CharFilter(lookup_expr="icontains")


class ChargeItemContextBuilder(QuerysetContextBuilder):
filterset_class = ChargeItemReportFilter
__filterset_backends__ = [filters.DjangoFilterBackend]

title = Field(
display="Charge Item Title",
preview_value="General Consultation",
description="Title of the charge item",
)
status = Field(
display="Charge Item Status",
preview_value="Active",
mapping=lambda ci: CHARGE_ITEM_STATUS_DISPLAY.get(
ci.status, ci.status.replace("_", " ").title()
)
if ci.status
else "",
description="Current status of the charge item",
)
service_resource = Field(
display="Service Resource",
preview_value="Consultation Service",
mapping=lambda ci: CHARGE_ITEM_RESOURCE_DISPLAY.get(
ci.service_resource, ci.service_resource.replace("_", " ").title()
)
if ci.service_resource
else "",
description="Service resource associated with the charge item",
)
quantity = Field(
display="Quantity",
preview_value="5",
description="Quantity of the charge item",
)
unit_price_components = Field(
display="Unit Price Components",
preview_value="",
target_context=UnitPriceMonetaryComponentContextBuilder,
description="Unit price components of the charge item",
)
total_price = Field(
display="Total Price",
preview_value="100.00",
description="Total price of the charge item",
)
total_price_components = Field(
display="Total Price Components",
preview_value="",
target_context=MonetaryComponentContextBuilder,
description="Breakdown of total price components of the charge item",
)

paid_on = Field(
display="Paid On",
preview_value="2024-01-15T10:30:00Z",
description="Date and time when the charge item was paid",
)

def get_context(self):
return ChargeItem.objects.filter(patient=self.parent_context)


class AccountChargeItemContextBuilder(ChargeItemContextBuilder):
def get_context(self):
return ChargeItem.objects.filter(account=self.parent_context)
72 changes: 72 additions & 0 deletions care/emr/reports/context_builder/data_points/invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from django_filters import rest_framework as filters

from care.emr.models.invoice import Invoice
from care.emr.reports.context_builder.data_points.base import (
Field,
QuerysetContextBuilder,
)
from care.emr.reports.context_builder.data_points.monetary_component import (
MonetaryComponentContextBuilder,
)

STATUS_DISPLAY = {
"draft": "Draft",
"issued": "Issued",
"balanced": "Balanced",
"cancelled": "Cancelled",
"entered_in_error": "Entered in Error",
}


class InvoiceReportFilter(filters.FilterSet):
status = filters.CharFilter(lookup_expr="iexact")
title = filters.CharFilter(lookup_expr="icontains")
number = filters.CharFilter(lookup_expr="icontains")


class InvoiceContextBuilder(QuerysetContextBuilder):
filterset_class = InvoiceReportFilter
__filterset_backends__ = [filters.DjangoFilterBackend]
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Annotate mutable class attribute with ClassVar.

The static analysis tool correctly identified that __filterset_backends__ is a mutable class attribute (list) that should be annotated with typing.ClassVar to indicate it's a class variable rather than an instance variable.

🔎 Proposed fix

Add the import at the top of the file:

+from typing import ClassVar
+
 from django_filters import rest_framework as filters

Then annotate the class attribute:

 class InvoiceContextBuilder(QuerysetContextBuilder):
     filterset_class = InvoiceReportFilter
-    __filterset_backends__ = [filters.DjangoFilterBackend]
+    __filterset_backends__: ClassVar = [filters.DjangoFilterBackend]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filterset_class = InvoiceReportFilter
__filterset_backends__ = [filters.DjangoFilterBackend]
filterset_class = InvoiceReportFilter
__filterset_backends__: ClassVar = [filters.DjangoFilterBackend]
🧰 Tools
🪛 Ruff (0.14.8)

29-29: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🤖 Prompt for AI Agents
In care/emr/reports/context_builder/data_points/invoice.py around lines 28-29,
the mutable class attribute __filterset_backends__ is currently a plain list and
should be annotated as a class variable; add "from typing import ClassVar, List"
to imports and change the attribute annotation to use ClassVar (e.g.,
__filterset_backends__: ClassVar[List] = [filters.DjangoFilterBackend]) so
static analysis knows it’s a class-level mutable constant.


title = Field(
display="Invoice Title",
preview_value="Medical Services Invoice",
description="Title of the invoice",
)
status = Field(
display="Invoice Status",
preview_value="Issued",
mapping=lambda i: STATUS_DISPLAY.get(i.status, i.status.title())
if i.status
else "",
description="Current status of the invoice",
)
number = Field(
display="Invoice Number",
preview_value="INV-1001",
description="Unique number of the invoice",
)
total_net = Field(
display="Total Net Amount",
preview_value="150.00",
description="Total net amount of the invoice",
)
total_gross = Field(
display="Total Gross Amount",
preview_value="180.00",
description="Total gross amount of the invoice",
)
total_price_components = Field(
display="Total Price Components",
preview_value="",
target_context=MonetaryComponentContextBuilder,
description="Breakdown of total price components of the invoice",
)

def get_context(self):
return Invoice.objects.filter(patient=self.parent_context)


class AccountInvoiceContextBuilder(InvoiceContextBuilder):
def get_context(self):
return Invoice.objects.filter(account=self.parent_context)
Loading