Skip to main content
Version: 3.1

Schedule & Availability

A schedule is a repeatable block of time during which a resource — a practitioner, location, or healthcare service — can be booked. You attach availabilities to it (these decide how the block splits into slots) and exceptions that block out specific dates.

Source:

The Django model (care/emr/models/scheduling/schedule.py) is storage only, down to the opaque Availability.availability JSONField. The contract lives in the Pydantic resource specs (care/emr/resources/scheduling/...): the enums, the real shape of that JSON field, the validation rules, and the separate read/write schemas. Everything below ## Models describes the specs.

Models

ModelPurpose
SchedulableResourceA bookable resource (a user, location, or healthcare service) within a facility
ScheduleA named, date-bounded block of availability attached to a resource
AvailabilitySlot configuration for a schedule (slot size, tokens, weekly recurrence)
AvailabilityExceptionA date/time range that blocks a resource (leave, holidays)

All four extend EMRBaseModel, which supplies external_id, created_date/modified_date, soft-delete via deleted, created_by/updated_by, and history/meta JSON.

Schedule fields

FieldTypeReq.Notes
resourceFK → SchedulableResource (CASCADE)serverResolved server-side from resource_type + resource_id (get-or-create). Reject this in the body
nameCharField(255)yesDisplay name (e.g. "Morning OPD")
valid_fromDateTimeFieldyesStart of the effective window. Must be ≥ now and ≤ valid_to
valid_toDateTimeFieldyesEnd of the effective window. Must be ≥ now
revisit_allowed_daysIntegerFieldnoNullable. Window within which a follow-up uses the revisit charge. Settable only via the set_charge_item_definition action
is_publicBooleanFieldyesExposes the schedule for public booking. Model default False

Both charge links are set only through the set_charge_item_definition action (see Resource specs) — never the create/update body.

FieldTypeNotes
charge_item_definitionFK → ChargeItemDefinition (PROTECT)Nullable; charge applied to bookings against this schedule. related_name="schedule_charge_item_definition"
revisit_charge_item_definitionFK → ChargeItemDefinition (PROTECT)Nullable; charge applied when a booking falls within revisit_allowed_days. related_name="schedule_revisit_charge_item_definition"

Enums

SchedulableResourceTypeOptions

Discriminates the kind of entity a schedule or exception attaches to. On write, send it as resource_type; the matching resource_id is that entity's external_id.

ValueResolves to
practitionerA User who is a member of the facility (serialized via UserSpec)
locationA FacilityLocation in the facility (serialized via FacilityLocationListSpec)
healthcare_serviceA HealthcareService in the facility (serialized via HealthcareServiceReadSpec)

The SchedulableResource.resource_type model column defaults to the string "practitioner".

SlotTypeOptions

The type of an individual Availability block (Availability.slot_type on the model, slot_type in the spec).

ValueMeaning
openOpen block; slot_size_in_minutes / tokens_per_slot are cleared to null on save
appointmentTime-precise appointment block; requires both slot_size_in_minutes and tokens_per_slot
closedClosed block; slot_size_in_minutes / tokens_per_slot are cleared to null on save

SchedulableResource

The bookable entity within a facility, and the join point that booking hangs off. Exactly one of user, location, or healthcare_service points at the underlying resource; resource_type says which. You never create one directly: Schedule and AvailabilityException writes call get_or_create_resource(resource_type, resource_id, facility) server-side, which checks the target belongs to the facility first.

facility → FK Facility (CASCADE)
resource_type → CharField(255), default "practitioner"
user → FK User (CASCADE, nullable)
location → FK FacilityLocation (CASCADE, nullable)
healthcare_service → FK HealthcareService (CASCADE, nullable)

Three UniqueConstraints keep one SchedulableResource per facility, resource_type, and target:

ConstraintFields
unique_facility_resource_userfacility, resource_type, user
unique_facility_resource_locationfacility, resource_type, location
unique_facility_resource_healthcare_servicefacility, resource_type, healthcare_service

On read, serialize_resource(obj) (resource/spec.py) dispatches on resource_type to UserSpec / HealthcareServiceReadSpec / FacilityLocationListSpec.

Availability

Decides how a schedule's time divides into bookable slots. One schedule can carry several availabilities — say, a morning block and an afternoon block.

FieldTypeReq.Spec detail
scheduleFK Schedule (CASCADE)serverExcluded from the availability spec; set from the URL / parent schedule
nameCharField(255)yesBlock name
slot_typeCharFieldyesOne of SlotTypeOptions
slot_size_in_minutesIntegerField (nullable)conditionalint | None, ge=1. Required when slot_type == appointment; forced to null otherwise
tokens_per_slotIntegerField (nullable)conditionalint | None, ge=1. Capacity per slot. Required when slot_type == appointment; forced to null otherwise
create_tokensBooleanFieldnoDefault False. When True, each booking gets a token (queue/token workflows)
reasonTextField (nullable)noSpec default ""
availabilityJSONField, default dictyesDespite the dict default, the spec models it as a list[AvailabilityDateTimeSpec] (weekly recurrence). See below

availability JSON shape — AvailabilityDateTimeSpec

Each entry is one weekly recurrence window. The field stores a list of these objects, not a single dict.

AvailabilityDateTimeSpec {
day_of_week: int # 0–6, validated le=6 (Monday=0 … Sunday=6, ISO-style)
start_time: time # HH:MM:SS
end_time: time # HH:MM:SS
}

The list is validated by AvailabilityForScheduleSpec.validate_availability + validate_for_slot_type:

  • Every entry needs start_time < end_time (strict).
  • No two entries on the same day_of_week may overlap (has_overlapping_availability: ranges overlap when a.start ≤ b.end and b.start ≤ a.end). On create, overlap is checked across all availabilities of the same schedule.
  • For slot_type == appointment: slot_size_in_minutes and tokens_per_slot are mandatory, each window's duration must be an exact multiple of slot_size_in_minutes, and the resulting slot count must not exceed settings.MAX_SLOTS_PER_AVAILABILITY (default 30).

AvailabilityException

Blocks a resource for a date/time range regardless of its schedules — leave, holidays, one-off closures.

FieldTypeReq.Spec detail
resourceFK SchedulableResource (CASCADE)serverResolved from resource_type + resource_id (get-or-create); excluded from the spec body
nameCharField(255)yesException name
reasonTextField (nullable)nostr | None
valid_fromDateFieldyesdate. Must be ≥ today and ≤ valid_to
valid_toDateFieldyesdate. Must be ≥ today
start_timeTimeFieldyestime
end_timeTimeFieldyestime

Resource specs (API schema)

Every spec extends EMRResource (base.py), which provides serialize() (DB → pydantic), de_serialize() (pydantic → DB), and the perform_extra_serialization / perform_extra_deserialization hooks. __exclude__ lists fields skipped during (de)serialization.

SpecRoleExposes / behaviour
ScheduleBaseSpecsharedid, is_public. __exclude__ = ["resource", "facility"]
ScheduleCreateSpecwrite · createfacility, name, valid_from, valid_to, availabilities: list[AvailabilityForScheduleSpec], resource_type, resource_id, is_public. Validates dates ≥ now, valid_from ≤ valid_to, and cross-availability non-overlap. perform_extra_deserialization stashes facility, _resource_id, _resource_type, and availabilities on the instance for the viewset
ScheduleUpdateSpecwrite · updatename, valid_from, valid_to, is_public. perform_extra_deserialization blocks narrowing validity that would drop allocated slots — it compares Sum(TokenSlot.allocated) across the old vs new range and raises if they differ
ScheduleReadSpecread · list + detailAll of the above plus availabilities (re-serialized from Availability rows), resource_type, charge_item_definition / revisit_charge_item_definition (full ChargeItemDefinitionReadSpec or null), revisit_allowed_days, created_by, updated_by
AvailabilityBaseSpecsharedid. __exclude__ = ["schedule"]
AvailabilityForScheduleSpecwrite/read (nested in schedule)name, slot_type, slot_size_in_minutes, tokens_per_slot, create_tokens, reason, availability: list[AvailabilityDateTimeSpec]. Carries the availability + slot-type validators
AvailabilityCreateSpecwrite · create (standalone)Adds schedule: UUID4; on create, re-checks overlap against all existing availabilities of that schedule
AvailabilityDateTimeSpecnestedday_of_week (le=6), start_time, end_time — the shape of the availability JSON field
AvailabilityExceptionBaseSpecsharedid, reason, valid_from, valid_to, start_time, end_time. __exclude__ = ["resource", "facility"]
AvailabilityExceptionWriteSpecwrite · create/upsertAdds facility, resource_type, resource_id. Validates dates ≥ today and valid_from ≤ valid_to; stashes _resource_type/_resource_id
AvailabilityExceptionReadSpecread · list + detailBase fields; perform_extra_serialization maps idexternal_id
ChargeItemDefinitionSetSpecwrite (action body)charge_item_definition: str | None, re_visit_allowed_days: int, re_visit_charge_item_definition: str | None — body for POST .../set_charge_item_definition (charge fields resolved by slug within the facility)

Server-maintained behaviour (viewset hooks)

  • Resource resolution. On schedule or exception create, get_or_create_resource(resource_type, resource_id, facility) checks the target is in the facility (a practitioner must be a FacilityOrganizationUser; a location or healthcare service must belong to the facility), then gets-or-creates the SchedulableResource. facility is injected from the URL via clean_create_data.
  • Nested availability create. ScheduleViewSet.perform_create (atomic) sets resource, saves the schedule, then de-serializes and saves each availability linked to it.
  • Charge items. revisit_allowed_days, charge_item_definition, and revisit_charge_item_definition move only through the set_charge_item_definition detail action, never the create/update body.
  • Locking + slot guards. Update and destroy take a Lock("booking:resource:<id>"). Deleting a schedule or availability is rejected when future allocated TokenSlots exist; otherwise availabilities and slots are soft-deleted (deleted=True).
  • Exception slot clearing. Creating an AvailabilityException soft-deletes overlapping TokenSlots, but rejects the request if any are already allocated ("There are bookings during this exception").
  • List requires resource. The Schedule and AvailabilityException list endpoints require resource_type and resource_id query params and scope results to that SchedulableResource.

Integration notes

  • Slots are computed from Availability at read time, not stored as rows on the schedule.
  • When reading a SchedulableResource, dispatch on resource_type to pick the populated relation (user, location, or healthcare_service) — only one is set, and you can't assume which.
  • A ChargeItemDefinition referenced by a schedule can't be deleted while the schedule exists (PROTECT).