Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

References/Medication Dispense: Difference between revisions

From OHC Network Wiki
Content deleted Content added
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

referencemedicationsCARE 3.0+

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:

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.

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

  • 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 References/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.