Skip to main content
Version: 3.0

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:

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

ModelPurpose
MedicationDispenseA single dispense event: a quantity of one inventory item handed to a patient
DispenseOrderGroups 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

FieldModel typeSpec typeNotes
statusCharField(100)MedicationDispenseStatusRequired. See status values. A cancelling status is terminal and blocks all further updates
not_performed_reasonCharField(100), nullMedicationDispenseNotPerformedReason | NoneCoded reason a dispense was not performed. See values
categoryCharField(100), nullMedicationDispenseCategory | NoneSetting the dispense happened in. See values

Timing & instructions

FieldModel typeSpec typeNotes
when_preparedDateTimeField, nulldatetime | NoneWhen the product was prepared
when_handed_overDateTimeField, nulldatetime | NoneWhen the product was handed to the patient
noteTextField, nullstr | NoneFree-text annotation
dosage_instructionJSONField (default list)list[DosageInstruction] (default [])Structured dosage directions. See DosageInstruction shape
substitutionJSONField (default dict)MedicationDispenseSubstitution | NoneSubstitution record. See shape

Quantities

FieldModel typeSpec typeNotes
quantityDecimalField(20, 6)Decimal (write: max_digits=20, decimal_places=0)Required. Amount dispensed; checked against inventory stock on create
days_supplyDecimalField(20, 6), nullDecimal | None (max_digits=20, decimal_places=0)Expected number of days the dispensed amount lasts

Relationships

FieldModel typeNotes
encounterFK → EncounterCASCADE. Resolved from the encounter UUID on write; patient is derived from it rather than sent directly
patientFK → PatientCASCADE. Server-set from encounter.patient
locationFK → FacilityLocationCASCADE. Must belong to the encounter's facility
authorizing_requestFK → MedicationRequestSET_NULL, null. The prescription this dispense fulfils; must be on the same encounter. Cleared server-side when the dispense is cancelled
itemFK → InventoryItemCASCADE. Stock item consumed; must sit in a location of the encounter's facility
charge_itemFK → ChargeItemCASCADE, null. Server-created from the product's charge item definition when one exists
orderFK → DispenseOrderCASCADE, 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.

ValueMeaning
outofstockOut of stock
washoutWashout
surgSurgery
sintolSensitivity / intolerance to drug
sddiDrug interaction
sduptherDuplicate therapy
saigAllergy to ingredient of medication
pregPatient pregnant

MedicationDispenseCategory values

From MedicationDispenseCategory (spec.py).

Value
inpatient
outpatient
community
discharge

SubstitutionType values

From SubstitutionType (spec.py) — used in substitution.substitution_type.

ValueMeaning
EEquivalent
ECEquivalent composition
BCBrand composition
GGeneric composition
TETherapeutic alternative
TBTherapeutic brand
TGTherapeutic generic
FFormulary
NNone

SubstitutionReason values

From SubstitutionReason (spec.py) — used in substitution.reason.

ValueMeaning
CTContinuing therapy
FPFormulary policy
OSOut of stock
RRRegulatory 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 classRoleNotes
BaseMedicationDispenseSpecshared base__model__ = MedicationDispense. Exposes status, not_performed_reason, category, when_prepared, when_handed_over, note, dosage_instruction, substitution. Excludes the FK fields
MedicationDispenseWriteSpecwrite · createAdds encounter, location, authorizing_request, item (UUIDs), quantity, days_supply, fully_dispensed, order, create_dispense_order. Resolves FKs and runs create-time logic
MedicationDispenseUpdateSpecwrite · updateBase fields plus fully_dispensed and order. Cannot re-point encounter/item/location
MedicationDispenseReadSpecread · listSerializes nested item, charge_item, location, authorizing_request, order; adds created_date, modified_date
MedicationDispenseRetrieveSpecread · detailExtends ReadSpec, also serializing the full encounter
MedicationDispenseSubstitutionnestedShape of the substitution JSON field
CreateDispenseOrdernested (write)Inline order creation payload — see below

Write-spec fields

FieldTypeRequiredNotes
encounterUUID4yesResolved to Encounter; patient derived from it
locationUUID4yesMust be in encounter.facility
itemUUID4yesInventoryItem in a location of encounter.facility
authorizing_requestUUID4 | NonenoMedicationRequest on the same encounter
quantityDecimalyesWrite spec constrains to decimal_places=0 (whole units)
days_supplyDecimal | Nonenodecimal_places=0
fully_dispensedbool | NonenoDrives the authorizing request's dispense_status (see below). Stashed on instance._fully_dispensed, not persisted on MedicationDispense
orderUUID4 | NonenoExisting DispenseOrder to attach to
create_dispense_orderCreateDispenseOrder | NonenoInline 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_instructionsystem-additional-instruction
as_needed_forsystem-as-needed-reason
sitesystem-body-site
routesystem-route
methodsystem-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.

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:

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 classRoleNotes
BaseMedicationDispenseOrderSpecshared base / updateExposes status, name, note. Doubles as the update spec
MedicationDispenseOrderWriteSpecwrite · createAdds patient, location (UUIDs), resolved in deserialization. facility is set server-side from the URL
MedicationDispenseOrderReadSpecread · listSerializes nested patient (list spec), location; adds created_date, modified_date
MedicationDispenseOrderRetrieveSpecread · detailFull 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 InventoryItemLock on item and rejects if item.net_content < quantity ("Inventory item does not have enough stock").
  • When item.product.charge_item_definition exists, applies it to create a ChargeItem (resource medication_dispense, linked to this dispense's external_id; performer_actor is the authorizing request's requester when present) and attaches it via charge_item.
  • Calls sync_inventory_item(item.location, item.product) to draw down stock.
  • When fully_dispensed is set and an authorizing_request exists, sets that request's dispense_status to complete (on True) or partial (on False).
  • Order resolution: create_dispense_order does a get-or-create by (alternate_identifier, patient, location); an existing non-draft/in_progress order is rejected.

On update (perform_update):

  • validate_data rejects any update while the current status is in MEDICATION_DISPENSE_CANCELLED_STATUSES.
  • A transition into a cancelling status with a charge_item attached cancels that charge item (handle_charge_item_cancel, status → aborted), sets the authorizing request's dispense_status to incomplete, and detaches authorizing_request (SET_NULL).
  • Re-syncs inventory and re-applies the fully_dispenseddispense_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 FHIR MedicationDispense resource. API field names may differ from FHIR — authorizing_request ≈ FHIR authorizingPrescription — and enum values use underscores (in_progress, entered_in_error), not FHIR hyphens.
  • List and summary need either a location or encounter query param, otherwise they 400. include_children=true widens a location query to descendant locations via parent_cache. Filters: status, exclude_status, category, encounter, patient, item, authorizing_prescription, authorizing_request, location, order. The summary action 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 item as an InventoryItem UUID, not a bare product code; that UUID is what draws down stock.
  • quantity and days_supply are constrained to whole units (decimal_places=0) on the write spec, even though the column stores 6 decimal places.
  • Leave charge_item alone. It is server-managed: created from the product's charge item definition and cancelled on dispense or order cancellation.
  • Set fully_dispensed to roll the linked Medication Request into complete / partial, so pharmacists don't re-dispense an already-fulfilled request.
  • Use create_dispense_order to start an order inline, or order to attach to an existing one — never both.