Medication Dispense
MedicationDispense records products handed to a patient and draws down the location's inventory as it does so. It closes out the workflow a Medication Request opens: in inpatient settings, items are dispensed into the room and refilled as needed; in outpatient settings, they go out once an encounter completes. A dispense can carry a Charge Item for billing.
Source:
- Model:
care/emr/models/medication_dispense.py - Spec:
care/emr/resources/medication/dispense/spec.py - Spec:
care/emr/resources/medication/dispense/dispense_order.py - Viewset:
care/emr/api/viewsets/medication_dispense.py
The Django model is the storage layer; several of its fields are opaque JSONFields. The Pydantic resource specs under care/emr/resources/medication/dispense/ define the API schema — enums, JSON-field shapes, validation, and the read/write contracts. Where the two disagree, the spec wins: status stores underscored enum values like in_progress, not the FHIR hyphenated forms.
Models
| Model | Purpose |
|---|---|
MedicationDispense | A single dispense event: a quantity of one inventory item handed to a patient |
DispenseOrder | Groups dispense events at a location into a named order with a shared status |
Both extend EMRBaseModel, which supplies external_id, audit fields, and soft-delete semantics.
MedicationDispense fields
Status & classification
| Field | Model type | Spec type | Notes |
|---|---|---|---|
status | CharField(100) | MedicationDispenseStatus | Required. See status values. A cancelling status is terminal and blocks all further updates |
not_performed_reason | CharField(100), null | MedicationDispenseNotPerformedReason | None | Coded reason a dispense was not performed. See values |
category | CharField(100), null | MedicationDispenseCategory | None | Setting the dispense happened in. See values |
Timing & instructions
| Field | Model type | Spec type | Notes |
|---|---|---|---|
when_prepared | DateTimeField, null | datetime | None | When the product was prepared |
when_handed_over | DateTimeField, null | datetime | None | When the product was handed to the patient |
note | TextField, null | str | None | Free-text annotation |
dosage_instruction | JSONField (default list) | list[DosageInstruction] (default []) | Structured dosage directions. See DosageInstruction shape |
substitution | JSONField (default dict) | MedicationDispenseSubstitution | None | Substitution record. See shape |
Quantities
| Field | Model type | Spec type | Notes |
|---|---|---|---|
quantity | DecimalField(20, 6) | Decimal (write: max_digits=20, decimal_places=0) | Required. Amount dispensed; checked against inventory stock on create |
days_supply | DecimalField(20, 6), null | Decimal | None (max_digits=20, decimal_places=0) | Expected number of days the dispensed amount lasts |
Relationships
| Field | Model type | Notes |
|---|---|---|
encounter | FK → Encounter | CASCADE. Resolved from the encounter UUID on write; patient is derived from it rather than sent directly |
patient | FK → Patient | CASCADE. Server-set from encounter.patient |
location | FK → FacilityLocation | CASCADE. Must belong to the encounter's facility |
authorizing_request | FK → MedicationRequest | SET_NULL, null. The prescription this dispense fulfils; must be on the same encounter. Cleared server-side when the dispense is cancelled |
item | FK → InventoryItem | CASCADE. Stock item consumed; must sit in a location of the encounter's facility |
charge_item | FK → ChargeItem | CASCADE, null. Server-created from the product's charge item definition when one exists |
order | FK → DispenseOrder | CASCADE, null. Parent dispense order, when grouped |
patient, encounter, authorizing_request, item, and location sit in __exclude__ on the spec base. The viewset resolves them by hand in perform_extra_deserialization instead of copying them off the Pydantic object.
Enum values
MedicationDispenseStatus values
From MedicationDispenseStatus (spec.py).
| Value |
|---|
preparation |
in_progress |
cancelled |
on_hold |
completed |
entered_in_error |
stopped |
declined |
MEDICATION_DISPENSE_CANCELLED_STATUSES = [cancelled, entered_in_error, stopped, declined] are terminal. Once a dispense lands in one, no further updates are allowed, and the transition into it triggers charge-item cancellation (see Methods & save behaviour).
MedicationDispenseNotPerformedReason values
From MedicationDispenseNotPerformedReason (spec.py). Coded reasons drawn from FHIR medicationdispense-status-reason.
| Value | Meaning |
|---|---|
outofstock | Out of stock |
washout | Washout |
surg | Surgery |
sintol | Sensitivity / intolerance to drug |
sddi | Drug interaction |
sdupther | Duplicate therapy |
saig | Allergy to ingredient of medication |
preg | Patient pregnant |
MedicationDispenseCategory values
From MedicationDispenseCategory (spec.py).
| Value |
|---|
inpatient |
outpatient |
community |
discharge |
SubstitutionType values
From SubstitutionType (spec.py) — used in substitution.substitution_type.
| Value | Meaning |
|---|---|
E | Equivalent |
EC | Equivalent composition |
BC | Brand composition |
G | Generic composition |
TE | Therapeutic alternative |
TB | Therapeutic brand |
TG | Therapeutic generic |
F | Formulary |
N | None |
SubstitutionReason values
From SubstitutionReason (spec.py) — used in substitution.reason.
| Value | Meaning |
|---|---|
CT | Continuing therapy |
FP | Formulary policy |
OS | Out of stock |
RR | Regulatory requirement |
JSON field shapes
MedicationDispenseSubstitution shape
The substitution field (MedicationDispenseSubstitution). When present, all three keys are required:
substitution = {
was_substituted: bool # required
substitution_type: SubstitutionType # required; see SubstitutionType values
reason: SubstitutionReason # required; see SubstitutionReason values
}
DosageInstruction shape
dosage_instruction is a list[DosageInstruction]. DosageInstruction comes straight from the Medication Request spec (care/emr/resources/medication/request/spec.py):
DosageInstruction {
sequence: int | None
text: str | None
additional_instruction: list[Coding]@system-additional-instruction | None
patient_instruction: str | None
timing: Timing | None
as_needed_boolean: bool # required
as_needed_for: Coding@system-as-needed-reason | None
site: Coding@system-body-site | None
route: Coding@system-route | None
method: Coding@system-administration-method | None
dose_and_rate: DoseAndRate | None
max_dose_per_period: DoseRange | None
}
Timing { repeat: TimingRepeat, code: Coding | None }
TimingRepeat{ frequency: int, period: Decimal(20,0), period_unit: TimingUnit, bounds_duration: TimingQuantity }
TimingQuantity { value: Decimal(20,0), unit: TimingUnit }
DoseAndRate { type: DoseType, dose_range: DoseRange | None, dose_quantity: DosageQuantity | None }
DoseRange { low: DosageQuantity, high: DosageQuantity }
DosageQuantity { value: Decimal(20,6), unit: Coding }
Coding@<slug> denotes a Coding bound to a Care value set (ValueSetBoundCoding), where Coding = { system?, version?, code (required), display? }. TimingUnit ∈ {s, min, h, d, wk, mo, a}; DoseType ∈ {ordered, calculated}. The shared PeriodSpec (from base.py) — { start: datetime, end: datetime }, both tz-aware, start ≤ end — is the standard period shape elsewhere, but MedicationDispense exposes no period field of its own.
Resource specs (API schema)
Every spec extends EMRResource (base.py) and rides its serialize / de_serialize plumbing. Read-side data is assembled in perform_extra_serialization; write-side FK resolution and side effects happen in perform_extra_deserialization.
| Spec class | Role | Notes |
|---|---|---|
BaseMedicationDispenseSpec | shared base | __model__ = MedicationDispense. Exposes status, not_performed_reason, category, when_prepared, when_handed_over, note, dosage_instruction, substitution. Excludes the FK fields |
MedicationDispenseWriteSpec | write · create | Adds encounter, location, authorizing_request, item (UUIDs), quantity, days_supply, fully_dispensed, order, create_dispense_order. Resolves FKs and runs create-time logic |
MedicationDispenseUpdateSpec | write · update | Base fields plus fully_dispensed and order. Cannot re-point encounter/item/location |
MedicationDispenseReadSpec | read · list | Serializes nested item, charge_item, location, authorizing_request, order; adds created_date, modified_date |
MedicationDispenseRetrieveSpec | read · detail | Extends ReadSpec, also serializing the full encounter |
MedicationDispenseSubstitution | nested | Shape of the substitution JSON field |
CreateDispenseOrder | nested (write) | Inline order creation payload — see below |
Write-spec fields
| Field | Type | Required | Notes |
|---|---|---|---|
encounter | UUID4 | yes | Resolved to Encounter; patient derived from it |
location | UUID4 | yes | Must be in encounter.facility |
item | UUID4 | yes | InventoryItem in a location of encounter.facility |
authorizing_request | UUID4 | None | no | MedicationRequest on the same encounter |
quantity | Decimal | yes | Write spec constrains to decimal_places=0 (whole units) |
days_supply | Decimal | None | no | decimal_places=0 |
fully_dispensed | bool | None | no | Drives the authorizing request's dispense_status (see below). Stashed on instance._fully_dispensed, not persisted on MedicationDispense |
order | UUID4 | None | no | Existing DispenseOrder to attach to |
create_dispense_order | CreateDispenseOrder | None | no | Inline order creation (get-or-create) |
validate_prescription rejects a payload that sends both order and create_dispense_order.
CreateDispenseOrder
Inline order payload on the write spec. With no matching order (by alternate_identifier + patient + location), one is created. A matching order whose status is not draft/in_progress is rejected with "Prescription is not active".
CreateDispenseOrder {
name: str | None
note: str | None
alternate_identifier: str # required
status: CreateDispenseOrderStatusOptions # default "draft"
}
CreateDispenseOrderStatusOptions ∈ {draft, in_progress}.
Bound value sets
dosage_instruction codings bind to Care value sets, all SNOMED CT and registered as systems:
Field (within DosageInstruction) | Value set slug |
|---|---|
additional_instruction | system-additional-instruction |
as_needed_for | system-as-needed-reason |
site | system-body-site |
route | system-route |
method | system-administration-method |
The dispense-level not_performed_reason lines up with the system-medication-not-given value set (medication_not_given_reason.py), but it is stored and validated as the MedicationDispenseNotPerformedReason enum, not as a bound Coding.
Related models
DispenseOrder
Groups one or more MedicationDispense events at a location into a named order under a single status.
location → FK FacilityLocation (CASCADE)
patient → FK Patient (CASCADE)
facility → FK Facility (CASCADE)
name → CharField(255), nullable
status → CharField(255)
note → TextField, nullable
tags → ArrayField[int], default []
alternate_identifier → CharField(100), nullable
The unique_alternate_identifier_encounter_location constraint enforces uniqueness on (alternate_identifier, patient, location), so each external identifier appears at most once per patient and location.
Source / specs:
- Model:
DispenseOrderinmedication_dispense.py - Specs:
dispense_order.py - Viewset:
api/viewsets/inventory/dispense_order.py
MedicationDispenseOrderStatusOptions values
| Value |
|---|
draft |
in_progress |
completed |
abandoned |
entered_in_error |
MEDICATION_DISPENSE_ORDER_COMPLETED_STATUSES = [abandoned, entered_in_error, completed] are terminal. abandoned and entered_in_error are the cancel transitions.
DispenseOrder specs
| Spec class | Role | Notes |
|---|---|---|
BaseMedicationDispenseOrderSpec | shared base / update | Exposes status, name, note. Doubles as the update spec |
MedicationDispenseOrderWriteSpec | write · create | Adds patient, location (UUIDs), resolved in deserialization. facility is set server-side from the URL |
MedicationDispenseOrderReadSpec | read · list | Serializes nested patient (list spec), location; adds created_date, modified_date |
MedicationDispenseOrderRetrieveSpec | read · detail | Full patient (retrieve spec) plus created_by / updated_by audit users |
Methods & save behaviour
The model has no custom save(). The dispense workflow lives in MedicationDispenseViewSet, inside a transaction.atomic() block, layering side effects over the spec's perform_extra_deserialization.
On create (perform_create):
- Takes an
InventoryItemLockonitemand rejects ifitem.net_content < quantity("Inventory item does not have enough stock"). - When
item.product.charge_item_definitionexists, applies it to create aChargeItem(resourcemedication_dispense, linked to this dispense'sexternal_id;performer_actoris the authorizing request's requester when present) and attaches it viacharge_item. - Calls
sync_inventory_item(item.location, item.product)to draw down stock. - When
fully_dispensedis set and anauthorizing_requestexists, sets that request'sdispense_statustocomplete(onTrue) orpartial(onFalse). - Order resolution:
create_dispense_orderdoes a get-or-create by(alternate_identifier, patient, location); an existing non-draft/in_progressorder is rejected.
On update (perform_update):
validate_datarejects any update while the current status is inMEDICATION_DISPENSE_CANCELLED_STATUSES.- A transition into a cancelling status with a
charge_itemattached cancels that charge item (handle_charge_item_cancel, status →aborted), sets the authorizing request'sdispense_statustoincomplete, and detachesauthorizing_request(SET_NULL). - Re-syncs inventory and re-applies the
fully_dispensed→dispense_status(complete/partial) logic.
Dispense-order cancellation (cancel_dispense_order in the order viewset): moving an order to abandoned or entered_in_error cascades to every member dispense. It cancels their charge items, marks each dispense cancelled / entered_in_error respectively, sets authorizing requests' dispense_status to incomplete, and detaches them. An order already in abandoned/entered_in_error cannot transition, and a completed order can only be cancelled.
API integration notes
- The resource is exposed through
MedicationDispenseViewSet(create, retrieve, update, list, upsert) and tracks the FHIRMedicationDispenseresource. API field names may differ from FHIR —authorizing_request≈ FHIRauthorizingPrescription— and enum values use underscores (in_progress,entered_in_error), not FHIR hyphens. - List and summary need either a
locationorencounterquery param, otherwise they400.include_children=truewidens a location query to descendant locations viaparent_cache. Filters:status,exclude_status,category,encounter,patient,item,authorizing_prescription,authorizing_request,location,order. Thesummaryaction returns per-encounter dispense counts. - Permissions are location-centric. Creating needs only write access to the dispensing
location— pharmacists often lack encounter access — while reads need either location-list or encounter-view permission. - Send
itemas anInventoryItemUUID, not a bare product code; that UUID is what draws down stock. quantityanddays_supplyare constrained to whole units (decimal_places=0) on the write spec, even though the column stores 6 decimal places.- Leave
charge_itemalone. It is server-managed: created from the product's charge item definition and cancelled on dispense or order cancellation. - Set
fully_dispensedto roll the linked Medication Request intocomplete/partial, so pharmacists don't re-dispense an already-fulfilled request. - Use
create_dispense_orderto start an order inline, ororderto attach to an existing one — never both.
Related
- Reference: Medication Request
- Reference: Medication Administration
- Reference: Medication Statement
- Reference: Inventory Item
- Reference: Charge Item
- Reference: Location
- Reference: Encounter
- Reference: Base model
- Source: medication_dispense.py on GitHub
- Spec: dispense/spec.py on GitHub
- Spec: dispense/dispense_order.py on GitHub