References/Medication Dispense: Difference between revisions
More languages
More actions
OHC identity seed |
Automated edit (via update-page on MediaWiki MCP Server) |
||
| Line 529: | Line 529: | ||
* Set <code>fully_dispensed</code> to roll the linked [[References/Medication Request]] into <code>complete</code> / <code>partial</code>, so pharmacists don't re-dispense an already-fulfilled request. |
* Set <code>fully_dispensed</code> to roll the linked [[References/Medication Request]] into <code>complete</code> / <code>partial</code>, so pharmacists don't re-dispense an already-fulfilled request. |
||
* Use <code>create_dispense_order</code> to start an order inline, or <code>order</code> to attach to an existing one — never both. |
* Use <code>create_dispense_order</code> to start an order inline, or <code>order</code> to attach to an existing one — never both. |
||
{{Navbox medications}} |
|||
{{Related}} |
{{Related}} |
||
Latest revision as of 09:45, 6 July 2026
MedicationDispense records products handed to a patient and draws down the location's inventory as it does so. It closes out the workflow a References/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 References/Charge Item for billing.
Source:
- Model: [https://github.com/ohcnetwork/care/blob/develop/care/emr/models/medication_dispense.py
care/emr/models/medication_dispense.py] - Spec: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/medication/dispense/spec.py
care/emr/resources/medication/dispense/spec.py] - Spec: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/medication/dispense/dispense_order.py
care/emr/resources/medication/dispense/dispense_order.py] - Viewset: [https://github.com/ohcnetwork/care/blob/develop/care/emr/api/viewsets/medication_dispense.py
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 [[#medicationdispensestatus-values |
not_performed_reason
|
CharField(100), null
|
None | Coded reason a dispense was not performed. See [[#medicationdispensenotperformedreason-values |
category
|
CharField(100), null
|
None | Setting the dispense happened in. See [[#medicationdispensecategory-values |
Timing & instructions
| Field | Model type | Spec type | Notes |
|---|---|---|---|
when_prepared
|
DateTimeField, null
|
None | When the product was prepared |
when_handed_over
|
DateTimeField, null
|
None | When the product was handed to the patient |
note
|
TextField, null
|
None | Free-text annotation |
dosage_instruction
|
JSONField (default list)
|
list[DosageInstruction] (default [])
|
Structured dosage directions. See [[#dosageinstruction-shape |
substitution
|
JSONField (default dict)
|
None | Substitution record. See [[#medicationdispensesubstitution-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
|
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 References/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 | |
location |
UUID4 |
yes | |
item |
UUID4 |
yes | |
authorizing_request |
UUID4 |
no | |
quantity |
Decimal |
yes | |
days_supply |
Decimal |
no | |
fully_dispensed |
bool |
no | |
order |
UUID4 |
no | |
create_dispense_order |
CreateDispenseOrder |
no |
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:
DispenseOrderin [https://github.com/ohcnetwork/care/blob/develop/care/emr/models/medication_dispense.pymedication_dispense.py] - Specs: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/medication/dispense/dispense_order.py
dispense_order.py] - Viewset: [https://github.com/ohcnetwork/care/blob/develop/care/emr/api/viewsets/inventory/dispense_order.py
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 References/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: References/Base models & conventions