Skip to main content
Version: 3.1

Invoice

An Invoice groups charge items raised against a single account into one billable document, with snapshotted line items, totals, and a lifecycle status. You touch it whenever you bill a patient, issue a refund, or reconcile payments.

The data splits across two layers. The Django model is storage: columns plus several opaque JSONFields (charge_items_copy, total_price_components, lock_history) whose real structure lives in the Pydantic resource specs. The specs define the API request/response schemas, the InvoiceStatusOptions enum, the shape of those JSON fields, validation, and the server-side totalling and snapshot behaviour.

Source:

Models

ModelPurpose
InvoiceA billable grouping of charge items for one account, with snapshotted line items, totals, and status

Invoice extends EMRBaseModel (shared Care EMR base with external_id, audit fields, and soft-delete semantics).

On issue, the server computes net and gross totals and snapshots each charge item, so the invoice stays stable even when its charge items change later. The retrieve view reports payment reconciliation records — payments and credit notes — against the invoice.

Invoice fields

References

FieldTypeNotes
facilityFK → facility.Facility (PROTECT)Facility where the invoice is created
patientFK → emr.Patient (PROTECT)Entity that incurred the charges; set server-side from account.patient on write
accountFK → emr.Account (PROTECT)Account being balanced by this invoice

Status & metadata

FieldTypeSpec / valuesNotes
statusCharField(100)InvoiceStatusOptions enum — required on writeDrives the charge item lifecycle. See values
titleCharField(1024)str | None, default NoneOptional human-readable label
cancelled_reasonTextFieldstr | NoneSet when the invoice moves to cancelled
payment_termsTextFieldstr | NoneMarkdown
noteTextFieldstr | NoneFree-text comments; markdown
numberCharField(1000)str | None, default NoneInvoice number. The server generates it on every create when the client does not send one, from the facility's invoice_number_expression (see Methods)
issue_dateDateTimeFielddatetime | None (tz-aware), default NoneSet when the invoice is issued
is_refundBooleanFieldbool, default FalseFlags a refund/return invoice; required for negative totals (see validation)

Charge items

FieldTypeSpec shapeNotes
charge_itemsArrayField[int], default []write: list[UUID4]; storage: list of integer PKsLive references to the grouped ChargeItem rows. The client sends external UUIDs on write; the server resolves them to PKs
charge_items_copyJSONField, default []list[dict] — array of serialized ChargeItemReadSpecPoint-in-time snapshot of the line items, written by sync_invoice_items. Served as charge_items on retrieve for any non-draft invoice

Totals (server-maintained)

sync_invoice_items and calculate_charge_items_summary compute these. Never send them from a client.

FieldTypeSpec shapeNotes
total_price_componentsJSONField, default {}retrieve: list[dict] of MonetaryComponentAggregated monetary components across all charge items (one per type+code). See MonetaryComponent shape
total_netDecimalField(20, 6), default 0Decimal(max_digits=20, decimal_places=6)Net total = base + surcharge − discount; tax excluded. Rounded with INVOICE_FINAL_AMOUNT_PRECISION / ..._ROUNDING_METHOD
total_grossDecimalField(20, 6), default 0Decimal(max_digits=20, decimal_places=6)Gross total = net + tax. Same rounding

When locked is True, the read specs serialize both total_net and total_gross as 0.

Locking

FieldTypeSpec shapeNotes
lockedBooleanField, default Falsebool (read-only on read specs)Freezes the invoice against further edits; while set, totals serialize as 0
lock_historyJSONField, default []list[dict] — each entry { user, ... }Audit trail of lock/unlock events. On retrieve, each entry's user id is hydrated into a UserSpec dict

Enums

InvoiceStatusOptions values

Defined in care/emr/resources/invoice/spec.py. The API value is entered_in_error (underscore), not the FHIR-style hyphen.

ValueMeaning
draftIn progress; retrieve serializes live charge items, not the snapshot
issuedIssued to the patient/payer
balancedFully settled
cancelledCancelled
entered_in_errorRecorded in error

INVOICE_CANCELLED_STATUS = ["cancelled", "entered_in_error"] — the two terminal/void states.

MonetaryComponentType values

Defined in care/emr/resources/common/monetary_component.py. Each type contributes to net and gross differently during totalling.

ValueEffect on totals
baseAdded to net (one base component per item; must carry an amount)
surchargeAdded to net
discountSubtracted from net
taxAdded to gross only (excluded from net)
informationalCarried for display; not summed

Resource specs (API schema)

All specs subclass EMRResource (serialize / de_serialize, with perform_extra_serialization / perform_extra_deserialization hooks). BaseInvoiceSpec.__model__ = Invoice and __exclude__ = ["account", "charge_items"]; the hooks handle those two fields rather than the generic mapper.

SpecRoleFields beyond baseServer behaviour
BaseInvoiceSpecsharedid, title, status (required), cancelled_reason, payment_terms, note, issue_date, numberBase for all specs
InvoiceWriteSpecwrite · create + updateaccount: UUID4 (required), charge_items: list[UUID4] (default [])perform_extra_deserialization: resolves account from external_id, sets patient = account.patient, stages charge_items (rewritten in perform_create)
InvoiceReadSpecread · listtotal_net, total_gross, locked, created_date, modified_date, account: dict, is_refundperform_extra_serialization: id = external_id; accountAccountMinimalReadSpec; if locked, zero out total_net/total_gross
InvoiceRetrieveSpecread · detail(all read fields) + charge_items: list[dict], total_price_components: list[dict], created_by, updated_by, payments: list[dict], total_payments, credit_notes: list[dict], total_credit_notes, lock_history: list[dict]See retrieve serialization below

InvoiceWriteSpec handles both create and update — there's no separate Update spec. InvoiceRetrieveSpec extends InvoiceReadSpec.

InvoiceRetrieveSpec.perform_extra_serialization

  • id = external_id; account serialized via AccountReadSpec (full account on detail).
  • charge_items: if status == draft, live ChargeItemReadSpec.serialize(...) over ChargeItem.objects.filter(id__in=charge_items) (select_related paid_invoice, charge_item_definition); otherwise the stored charge_items_copy snapshot.
  • created_by / updated_by: hydrated via serialize_audit_users.
  • Reconciliations against this invoice with outcome = complete and status = active are split into:
    • payments (+ total_payments) where is_credit_note is false,
    • credit_notes (+ total_credit_notes) where is_credit_note is true,
    • each serialized via PaymentReconciliationRetrieveSpec.
  • lock_history: each entry's user id replaced with a cached UserSpec.

MonetaryComponent shape

Stored inside total_price_components and charge_items_copy. Source: care/emr/resources/common/monetary_component.py.

MonetaryComponent {
monetary_component_type: MonetaryComponentType # required (base|surcharge|discount|tax|informational)
code: Coding | None # { system?, version?, code, display? }
factor: Decimal(20,6) | None
amount: Decimal(20,6) | None
tax_included_amount: Decimal(20,6) | None # only valid on base
global_component: bool = False
conditions: list[EvaluatorConditionSpec] = []
}

Per-component validation: base must carry an amount and no conditions; tax_included_amount is allowed only on base; amount and factor are mutually exclusive; at least one of amount/factor is required, unless the component is a global_component with a code. Collection-level rules (MonetaryComponents) forbid duplicate codes, require exactly one base, and reconcile tax_included_amount against the sum of tax components.

  • ChargeItem — line items grouped by the invoice; snapshotted into charge_items_copy via ChargeItemReadSpec.
  • Account — the billing account; resolved on write, rebalanced after return-invoice generation/cancellation.
  • PaymentReconciliation — payments and credit notes reported on the retrieve view (filtered to outcome=complete, status=active).
  • ChargeItemDefinition — applied when building return-invoice charge items.

Methods & save behaviour

  • sync_invoice_items(invoice) (sync_items.py) — recomputes total_net, total_gross, total_price_components, and charge_items_copy from the linked charge items, then rounds totals using INVOICE_FINAL_AMOUNT_PRECISION / INVOICE_FINAL_AMOUNT_ROUNDING_METHOD. Validation: if is_refund is false and either total is negative, raises ValidationError("A Refund Ivoice is required for negative values").
  • calculate_charge_items_summary(charge_items) — net = Σ base + Σ surcharge − Σ discount; gross = net + Σ tax; components aggregated per type + code (key = system + code, else No-Code).
  • evaluate_invoice_identifier_default_expression(facility) (default_expression_evaluator.py) — generates number from the facility's invoice_number_expression with context { invoice_count, current_year_yyyy, current_year_yy }; returns "" if unset. evaluate_invoice_dummy_expression previews an expression with sample context. perform_create calls this function for every new invoice when the client does not send a number.
  • generate_return_invoice(delivery_order) (return_items_invoice.py) — creates a draft refund invoice (is_refund=True), reverses each completed SupplyDelivery's charge item via apply_charge_item_definition(reverse=True), marks items billed, runs sync_invoice_items, sets status issued, then triggers rebalance_account_task. Raises if any delivery is still in_progress.
  • cancel_return_invoice(delivery_order) — sets the invoice cancelled, marks its charge items entered_in_error (clears paid_invoice/paid_on), voids the supply deliveries, rebalances the account, and resyncs inventory.

API endpoints

InvoiceViewSet (care/emr/api/viewsets/invoice.py) is registered on the facility nested router with the prefix invoice. Every route is nested under a facility.

MethodPathPurpose
GET/api/v1/facility/{facility_external_id}/invoice/List the invoices in the facility
POST/api/v1/facility/{facility_external_id}/invoice/Create an invoice
GET/api/v1/facility/{facility_external_id}/invoice/{external_id}/Retrieve one invoice
PUT/api/v1/facility/{facility_external_id}/invoice/{external_id}/Update an invoice
POST.../invoice/{external_id}/attach_items_to_invoice/Add charge items to a draft invoice. Body: charge_items, a list of UUIDs
POST.../invoice/{external_id}/remove_item_from_invoice/Remove one charge item from a draft invoice. Body: charge_item, a UUID
POST.../invoice/{external_id}/attach_account_to_invoice/Attach every billable charge item on the account to the draft invoice. No body
POST.../invoice/{external_id}/cancel_invoice/Cancel the invoice. Body: reason, either cancelled or entered_in_error
POST.../invoice/{external_id}/lock/Lock the invoice
POST.../invoice/{external_id}/unlock/Unlock the invoice

There is no destroy endpoint.

Custom action behaviour

  • attach_items_to_invoice, remove_item_from_invoice, attach_account_to_invoice, and cancel_invoice each take an account-level lock.
  • The three charge-item actions require the status draft. For any other status they raise ValidationError("Invoice is not in draft").
  • attach_items_to_invoice accepts only the charge items on the same account with the status billable. Matched items move to billed and link to the invoice. The action ignores unmatched ids without a message.
  • remove_item_from_invoice returns the item to billable and clears paid_invoice and paid_on. An item that is not on the invoice raises ValidationError("Charge item not found in invoice").
  • cancel_invoice rejects a reason outside the two allowed values with ValidationError("Invalid reason"). It rejects an invoice that is already void with ValidationError("Invoice is already cancelled"). It returns every charge item on the invoice to billable.
  • lock and unlock append an entry to lock_history with the user, a timestamp, and the action. They raise ValidationError with "Invoice is already locked" and "Invoice is not locked".
  • The three charge-item actions and cancel_invoice return the full InvoiceRetrieveSpec payload.
  • Every action that changes the items or the status triggers rebalance_account_task for the account. lock and unlock do not.

Filtering and ordering

The list is scoped to the facility in the path. The filterset is InvoiceFilters.

Query parameterMatch
statusCase-insensitive exact
titleCase-insensitive contains
accountAccount external id (UUID)
patientPatient external id (UUID)
numberCase-insensitive contains
lockedBoolean
is_refundBoolean
created_byUser external id (UUID)
created_dateDatetime from/to range
payment_reconciliation_presentBoolean. Keeps or excludes the invoices that have a reconciliation on the given account

payment_reconciliation_present also needs account. Without account, the filterset raises ValidationError("Account is required when payment reconciliation filter is present").

Ordering fields: created_date and modified_date.

Permissions

The four invoice permissions are facility-scoped. The authorization controller checks each permission against the facility organization memberships of the user.

PermissionHuman-readable nameChecked bySystem roles
can_read_invoiceCan Read Invoicecan_read_invoice_in_facility — list and retrieveFacility Admin, Administrator, Admin, Staff, Doctor, Nurse, Volunteer, Pharmacist
can_write_invoiceCan Write Invoicecan_write_invoice_in_facility — create, update, the three charge-item actions, and cancel inside the free-cancel periodFacility Admin, Admin, Staff, Doctor, Nurse, Pharmacist
can_destroy_invoiceCan Destroy Invoicecan_destroy_invoice_in_facility — cancel after the free-cancel periodFacility Admin, Admin
can_manage_locked_invoiceCan Manage Locked Invoicecan_manage_locked_invoice_in_facility — lock, unlock, and retrieve a locked invoiceFacility Admin, Admin

The failure messages are "Cannot read invoice", "Cannot write invoice", "Cannot cancel invoice", and "Locked invoice permission denied."

Create also validates that the account belongs to the facility in the path. For an account of another facility, the viewset raises ValidationError("Account is not associated with the facility").

Free-cancel period

The setting INVOICE_FREE_CANCEL_PERIOD_MINUTES comes from the environment, and its default value is 0. Inside that many minutes of created_date, a cancel needs only can_write_invoice. After that period, a cancel needs can_destroy_invoice. With the default value of 0, every cancel needs can_destroy_invoice.

Status transitions

Create

perform_create always forces status to draft. It sets the facility from the path, and resolves only the billable charge items on the account. It generates number when the client did not send one. It moves the resolved items to billed with paid_invoice set, runs sync_invoice_items, then rebalances the account. perform_create takes an account lock and a create lock. A lock conflict raises ValidationError("Invoice creation failed").

Update

perform_update holds an invoice lock and enforces the transitions below. It calls rebalance_account_task afterwards.

FromToResult
draftissuedAllowed when the invoice has at least one charge item. For an empty invoice, ValidationError("Invoice must have at least one charge item")
draftbalancedValidationError("Invoice needs to be issued before balancing")
issuedbalancedAllowed. Every charge item on the invoice with the status billed moves to paid, with paid_invoice set and paid_on stamped
issueddraftValidationError("Invoice is already issued")
balancedanyValidationError("Invoice is already balanced")
anycancelled or entered_in_errorValidationError("Call the cancel invoice API to cancel the invoice"). Use the cancel_invoice action
cancelled or entered_in_erroranyValidationError("Invoice is already cancelled")

API integration notes

  • status is the control surface for the billing lifecycle. Drive transitions through the API rather than editing rows directly; the values are draft, issued, balanced, cancelled, entered_in_error.
  • On write (InvoiceWriteSpec): send account and charge_items as external UUIDs. The server derives patient from the account, so don't send it. account and charge_items bypass the generic field mapper and are handled in perform_extra_deserialization.
  • charge_items_copy, total_price_components, total_net, and total_gross are platform-maintained snapshots derived from the linked charge items — don't set them directly.
  • The list view (InvoiceReadSpec) returns a minimal account; the detail view (InvoiceRetrieveSpec) returns the full account plus charge items, aggregated price components, payments, credit notes, and lock history.
  • When locked is True, the read specs serialize total_net and total_gross as 0.