Account
An Account is the financial bucket that aggregates every transaction made against a patient within a facility. You touch it indirectly: charge items land on a patient's default account, and invoices and reconciliations settle against it.
Several model fields are opaque JSONFields whose real structure lives in the Pydantic resource specs under care/emr/resources/account/. Each field below is paired with the spec-defined enums, JSON shapes, validation, and read/write schemas that govern it.
Source:
- Model:
care/emr/models/account.py - Resource spec:
care/emr/resources/account/spec.py - Default account helper:
account/default_account.py - Balance sync:
account/sync_items.py - Base resource:
care/emr/resources/base.py
Models
| Model | Purpose |
|---|---|
Account | Aggregates charges and balances for one patient in one facility across encounters |
Account extends EMRBaseModel, which contributes external_id, the audit fields created_date/modified_date, soft-delete via deleted, created_by/updated_by, and history/meta JSON.
Accounts are perpetual. One active, open account exists per patient per facility (see get_default_account), and charge items land on it. After discharge, the account can be balanced and closed. Invoices are raised against an account or against the items within it.
Account fields
Identity & scope
| Field | Type | Required | Notes |
|---|---|---|---|
facility | FK → facility.Facility (PROTECT) | yes | Server-set from the request scope; absent from every write spec |
patient | FK → emr.Patient (PROTECT) | yes | The entity that incurred the expenses. On create, pass a patient external_id (UUID); the server resolves it |
name | CharField(255) | yes | Human-readable label. The default account auto-names as "{patient.name} {YYYY-MM-DD}" |
description | TextField | no | Nullable free text for purpose or use |
primary_encounter | FK → emr.Encounter (SET_NULL, nullable) | no | Pins the account to a single encounter for reports, summaries, and insurance paperwork. Settable only through the update spec, as an encounter external_id |
Status
| Field | Type | Required | Notes |
|---|---|---|---|
status | CharField(255) | yes | Lifecycle status from AccountStatusOptions (see values); the enum rejects out-of-set strings |
billing_status | CharField(255) | yes | Billing lifecycle status from AccountBillingStatusOptions (see values) |
service_period | JSONField (default {}) | yes (on spec) | Transaction window holding a PeriodSpec { start, end } — see service_period shape |
Balances & totals
These decimal totals (max_digits=20, decimal_places=6, default 0) are platform-maintained aggregates that sync_account_items recomputes. None are client-writable, and none appear on a write spec.
| Field | Type | Notes |
|---|---|---|
total_net | DecimalField | Net total before adjustments. Not populated — the sync_account_items logic is commented out, so it holds the default 0 |
total_gross | DecimalField | Sum of total_price over charge items in paid or billed status |
total_paid | DecimalField | Sum of active+complete payment reconciliations, minus credit-note reconciliations |
total_balance | DecimalField | total_gross − total_paid |
total_billable_charge_items | DecimalField | Sum of total_price over charge items in billable status |
total_price_components | JSONField (default {}) | Intended price-component breakdown (taxes, discounts, etc.) as a list of MonetaryComponent (see below). Not populated — the sync logic is commented out |
cached_items | JSONField (default {}) | Denormalized snapshot of the account's charge items, serialized as a list on retrieve. Not populated — the sync logic is commented out |
calculated_at | DateTimeField (nullable) | When the balances were last computed; set to care_now() on each sync |
Tags & extensions
| Field | Type | Notes |
|---|---|---|
tags | ArrayField[int] (default []) | Tag IDs applied to the account; rendered through SingleFacilityTagManager on read |
extensions | JSONField (default {}) | Open bag for deployment-specific metadata. ExtensionValidator checks it against ExtensionResource.account; surfaced only on the retrieve spec |
Enum values
AccountStatusOptions values
Stored as the raw enum value string in status.
| Value | Meaning |
|---|---|
active | Open and in use (the default-account state) |
inactive | No longer actively used |
entered_in_error | Created in error |
on_hold | Temporarily paused |
Values use underscores (entered_in_error, on_hold), not the FHIR-style hyphenated forms.
AccountBillingStatusOptions values
Stored as the raw enum value string in billing_status.
| Value | Meaning |
|---|---|
open | Billing open and accruing (the default-account state) |
carecomplete_notbilled | Care complete, not yet billed |
billing | Billing in progress |
closed_baddebt | Closed as bad debt |
closed_voided | Closed and voided |
closed_completed | Closed, billing completed |
closed_combined | Closed, combined into another account |
JSON field shapes
service_period shape
service_period stores a PeriodSpec (from resources/base.py):
PeriodSpec {
start: datetime | null # ISO 8601, timezone-aware
end: datetime | null # ISO 8601, timezone-aware
}
PeriodSpec.validate_period enforces three rules:
start, if present, must be timezone-aware — a naive datetime raises"Start Date must be timezone aware".end, if present, must be timezone-aware —"End Date must be timezone aware".- With both present,
start <= end, or"Start Date cannot be greater than End Date".
The default account sets only start, formatted as "%Y-%m-%dT%H:%M:%S.%fZ" from care_now().
The FHIR-style Period common type ({ id?, start?, end? }, extra="forbid") is a related but distinct shape — Account binds service_period to PeriodSpec, not Period.
MonetaryComponent shape
total_price_components is meant to hold a list of MonetaryComponent entries, the same shape used across billing:
MonetaryComponent {
monetary_component_type: base | surcharge | discount | tax | informational
code: Coding | null
factor: Decimal(max_digits=20, decimal_places=6) | null
amount: Decimal(max_digits=20, decimal_places=6) | null
tax_included_amount: Decimal(max_digits=20, decimal_places=6) | null
global_component: bool = false
conditions: EvaluatorConditionSpec[] = []
}
MonetaryComponent validation: tax_included_amount is allowed only on a base component; a base component must carry an amount, no factor, and no conditions; amount and factor are mutually exclusive; exactly one of amount/factor is required unless the entry is a global_component with a code.
Resource specs (API schema)
The API layer lives in resources/account/spec.py. Every spec builds on EMRResource (serialize / de_serialize, with the perform_extra_serialization / perform_extra_deserialization hooks). AccountSpec sets __exclude__ = ["patient"] and binds ___extension_resource_type__ = ExtensionResource.account.
| Spec class | Role | Fields exposed (beyond base) |
|---|---|---|
AccountSpec | shared base | id, status, billing_status, name, service_period (PeriodSpec), description |
AccountCreateSpec | write · create | base + patient (UUID, required) |
AccountUpdateSpec | write · update | base + primary_encounter (UUID, optional) |
AccountMinimalReadSpec | read · minimal | base + total_gross, total_paid, total_balance, total_billable_charge_items (all Decimal 20/6), calculated_at, created_date, modified_date |
AccountReadSpec | read · list | minimal + patient (serialized PatientListSpec), tags (rendered tag dicts) |
AccountRetrieveSpec | read · detail | read + patient (serialized PatientRetrieveSpec), primary_encounter (serialized EncounterRetrieveSpec when set), cached_items (list), total_price_components (dict), extensions (dict) |
Validation & server-side behaviour
- Create (
AccountCreateSpec): mixes inExtensionValidatorto validateextensions, and requirespatientas a UUID.perform_extra_deserializationresolves it viaget_object_or_404(Patient, external_id=...)and assignsobj.patient;patientis otherwise excluded from the base serialize loop. - Update (
AccountUpdateSpec): mixes inExtensionValidator;primary_encounteris optional. When supplied,perform_extra_deserializationresolves it viaget_object_or_404(Encounter, external_id=...)and assignsobj.primary_encounter. - Status / billing_status: the Pydantic enums
AccountStatusOptions/AccountBillingStatusOptionsreject any value outside their members. - Read serialization:
perform_extra_serializationsetsmapping["id"] = obj.external_id.AccountReadSpecserializes the patient (PatientListSpec) and renders tags viaSingleFacilityTagManager;AccountRetrieveSpecserializes the full patient (PatientRetrieveSpec, scoped toobj.facility) and, when present, the primary encounter (EncounterRetrieveSpec). - Totals are read-only: balance and aggregate fields appear only on read specs.
sync_account_itemsrecomputes them; client payloads never set them.
Default account
get_default_account(patient, facility) (default_account.py) returns the first account for that patient + facility with status = active and billing_status = open. If none exists, it creates one with:
status = active,billing_status = openservice_period = { "start": care_now() }(start only)name = "{patient.name} {YYYY-MM-DD}"
This runs when the first charge item is added, so the default account materializes on demand. Clients normally never create accounts directly.
Balance sync
sync_account_items(account) (sync_items.py) recomputes the totals under an AccountLock:
total_billable_charge_items=Σ total_priceof charge items with statusbillabletotal_gross=Σ total_priceof charge items with statuspaidorbilledtotal_paid=Σ amountofactive+completenon-credit-note payment reconciliations − the same for credit notestotal_balance=total_gross − total_paidcalculated_at=care_now()
Every sum runs through care_round. total_net, cached_items, and total_price_components are not updated — their logic is commented out — so they hold their defaults. rebalance_account_task(account_id) is the Celery wrapper that runs the sync and saves.
Related models
Account is the only class in account.py. Its relationships:
facility → FK facility.Facility (PROTECT)
patient → FK emr.Patient (PROTECT)
primary_encounter → FK emr.Encounter (SET_NULL, nullable)
Charge items reference the account they belong to and drive its totals; invoices and payment reconciliations settle against the account or its items.
API integration notes
- The REST API aligns to the FHIR
Accountresource, but spec field names and enum values differ (enums use underscores, not hyphens). get_default_accountcreates the default account when the first charge item is added for a patient in a facility; clients normally do not create accounts directly.- On create, supply
patientas the patient'sexternal_id(UUID);facilityis server-scoped. - On update,
primary_encounteraccepts an encounterexternal_id(UUID); only the base fields plusprimary_encounterare mutable. - Balance and aggregate fields (
total_net,total_gross,total_paid,total_balance,total_billable_charge_items,total_price_components,cached_items,calculated_at) are platform-maintained and appear only on read specs — never set them from clients. service_periodrequires timezone-aware datetimes withstart <= end.extensionsis the supported place for custom key-value data without schema migrations;ExtensionValidatorchecks it, and it surfaces only on the retrieve spec.- Gate account data behind permissions that restrict access to users with rights to financial information.
Related
- Reference: Charge item
- Reference: Charge item definition
- Reference: Invoice
- Reference: Payment reconciliation
- Reference: Patient
- Reference: Encounter
- Reference: Base model
- Patient concept: Patient
- Source: account.py on GitHub
- Spec: account/spec.py on GitHub