← Knowledge library

Pharmacy

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…

3.1 · versioned documentation · 11 min read

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

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 MANSOQ 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 MANSOQ 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:

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):

On update (perform_update):

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

Related

Source evidence

References are pinned to the private MANSOQ source repository. Repository access is required to open these links.