Data Model Reference¶
The data model lives in the config Django app (config/models.py). All core domain entities -- clinicians, shifts, leave, alerts, audit logs -- are defined there using Django's ORM with Python enum.Enum classes for type-safe choice fields.
Enums¶
WorkingTermType¶
| Value | Description |
|---|---|
SALARIED |
Salaried employee with fixed working pattern |
PARTNER |
GP partner with flexible working pattern |
LOCUM |
Locum doctor, works manually specified dates only |
FY_DOCTOR |
Foundation Year trainee doctor |
ST_DOCTOR |
Specialty Trainee doctor |
WorkingTermType.get_resident_types() returns [FY_DOCTOR, ST_DOCTOR].
ShiftType¶
| Value | Counts as Worked | Counts Toward Minimum | Description |
|---|---|---|---|
STANDARD |
Yes | Yes | Regular working shift |
DUTY |
Yes | Yes | Duty doctor shift |
STUDY_LEAVE |
Yes | No | Study leave (counts as a worked session since issue #197) |
CORONERS |
Yes | No | Coroner's court attendance |
ShiftDuration¶
| Value | Description |
|---|---|
FULL |
Full day shift |
HALF |
Half day shift |
ShiftStatus¶
| Value | Description |
|---|---|
SCHEDULED |
Shift is scheduled |
COMPLETED |
Shift has been completed |
CANCELLED |
Shift has been cancelled |
ChangeType¶
| Value | Description |
|---|---|
CREATED |
Shift was created |
UPDATED |
Shift was modified |
DELETED |
Shift was deleted |
Used by PendingShiftNotification for batch-mode change tracking.
LeaveType¶
| Value | Quota-based | Partner-only | Description |
|---|---|---|---|
ANNUAL_LEAVE |
Yes | No | Annual leave/vacation |
NOT_WORKING |
No | Yes | Day not working (partners only) |
PLANNED_SICK |
No | No | Planned sick leave |
STUDY_LEAVE |
No | No | Study leave |
CORONERS |
No | No | Coroner's court attendance |
LeaveType.is_quota_based(value) and LeaveType.is_partner_only(value) are class methods.
LeaveStatus¶
| Value | Description |
|---|---|
REQUESTED |
Submitted, awaiting processing |
APPROVED |
Approved by admin |
DENIED |
Denied by admin |
CANCELLED |
Cancelled (by admin or clinician) |
ManualAdjustmentType¶
| Value | Description |
|---|---|
ANNUAL_LEAVE_ADJUSTMENT |
Adjustment to annual leave entitlement |
SICK_SHIFT_COMPENSATION |
Compensation for sick shifts |
AlertType¶
| Value | Label | Description |
|---|---|---|
BELOW_MINIMUM |
Short-staffed | Staffing below minimum required |
AT_WARNING_THRESHOLD |
At Minimum Staffing | Staffing at minimum threshold |
OVERWORKED_DOCTOR |
Overworked Doctor | Doctor is overworked |
CONSTRAINT_VIOLATION |
Scheduling Concern | Rota constraint violation |
INSUFFICIENT_DUTY_DOCTORS |
Not Enough Duty Doctors | Insufficient duty doctors on a day |
AlertSeverity¶
| Value | Description |
|---|---|
WARNING |
Warning level alert |
CRITICAL |
Critical level alert |
AlertStatus¶
| Value | Auto-sets resolved_at |
Description |
|---|---|---|
ACTIVE |
No | Alert is currently active |
RESOLVED |
Yes | Alert has been resolved |
DISMISSED |
Yes | Alert has been dismissed |
AuditAction¶
| Value | Description |
|---|---|
CREATE |
Entity was created |
UPDATE |
Entity was updated |
DELETE |
Entity was deleted |
SwapStatus¶
| Value | Description |
|---|---|
REQUESTED |
Swap requested, awaiting target response |
ACCEPTED |
Target accepted, pending admin approval |
REJECTED_BY_TARGET |
Target declined |
PENDING_ADMIN |
Accepted by target, awaiting admin |
APPROVED |
Admin approved, swap executed |
REJECTED_BY_ADMIN |
Admin declined |
CANCELLED |
Requester cancelled |
SickLeaveStatus¶
| Value | Description |
|---|---|
ACTIVE |
Currently off sick |
RETURNED |
Back to work |
CANCELLED |
Recorded in error |
Core Models¶
SystemConfiguration (singleton)¶
System-wide configuration. Only one instance should exist (enforced by verbose_name_plural). Access via SystemConfiguration.get_or_create() or SystemConfiguration.get_cached().
| Field | Type | Default | Description |
|---|---|---|---|
target_working_days_per_week |
DecimalField(4,2) | 3.50 | FTE working days per week |
minimum_doctors |
JSONField | dict | Min doctors per weekday: {"Monday": 9, "Tuesday": 7, ...} |
duty_doctors_required |
IntegerField | 1 | Duty doctors on normal days |
duty_doctors_post_bank_holiday |
IntegerField | 2 | Duty doctors after bank holidays |
post_bank_holiday_minimum |
CharField(20) | "Monday" |
Which day's minimum to use post-bank-holiday |
fixed_period_weeks |
IntegerField | 6 | Weeks in the fixed (non-auto-recalc) period |
reporting_periods |
JSONField | list | Reporting period configs |
uk_nation |
CharField(20) | "england" |
UK nation for bank holiday detection |
current_date |
DateField (nullable) | -- | Reference date for fixed period boundary |
enable_wte_balance |
BooleanField | True | Enable WTE balance optimization phase |
enable_shift_notifications |
BooleanField | True | Enable email notifications for shift changes |
shift_notification_batch_mode |
BooleanField | False | Queue notifications instead of sending immediately |
batch_mode_enabled_at |
DateTimeField (nullable) | -- | When batch mode was last enabled |
batch_mode_enabled_by |
CharField(255) | "" |
User who last enabled batch mode |
wte_balance_deficit_weight |
DecimalField(3,2) | 0.60 | Weight for daily deficit in day selection (0-1) |
wte_balance_day_distribution_weight |
DecimalField(3,2) | 0.40 | Weight for day-of-week balance in day selection (0-1) |
optimization_iterations |
IntegerField | 10 | Randomized iterations for optimization |
ai_api_enabled |
BooleanField | False | Enable AI agent API access |
ai_api_approval_mode |
BooleanField | True | Require browser approval for AI transactions (disable for autonomous mode) |
ai_api_approval_timeout_minutes |
IntegerField | 60 | Auto-reject proposed transactions after this many minutes |
leave_booking_cutoff |
DateField (nullable) | -- | Clinicians cannot book normal leave (annual, not-working, planned sick) beyond this date; study/coroners unrestricted; admins bypass |
Notable behaviour:
- save() runs full_clean(), creates audit log entries for critical field changes, and invalidates related caches.
- uk_nation cannot be changed after shifts exist (prevents historical inconsistencies).
- WTE balance weights must sum to 1.0.
- Holiday-period blocking (the former "Christmas period") is now driven by the multi-row HolidayPeriod model, not SystemConfiguration fields.
MinimumStaffingRule¶
Time-limited, conditional override of the daily minimum doctor count (issue #198). A rule fires on a date when it is enabled, the date is inside its window, the weekday matches (or days_of_week is empty), and every clinician in required_clinician_ids is working that date (or the list is empty). A firing rule's minimum_doctors replaces the SystemConfiguration day-of-week base (it may raise or lower it); when several rules fire, the highest wins. Evaluated through config.minimum_staffing.evaluate_minimum by the leave-booking capacity service, StaffingValidator, alerts, deficit ordering, coverage simulation, AI-API floor checks, and reports — each passing its own sense of "working". The CP-SAT rota generator keeps using base minimums. Rules are authored in code or Django admin; the /admin/ Minimum Staffing Rules page exposes only enable/disable and the date window (audited).
| Field | Type | Default | Description |
|---|---|---|---|
name |
CharField(200) | -- | Human-readable label |
description |
CharField(500) | "" |
Optional note explaining the rule |
days_of_week |
JSONField (list) | [] |
Weekday names the rule applies to; empty = every weekday |
required_clinician_ids |
JSONField (list) | [] |
Clinician IDs that must ALL be working; empty = no condition |
minimum_doctors |
PositiveIntegerField | -- | Minimum count used when the rule fires |
enabled |
BooleanField | True | Master switch; disabled rules are ignored everywhere |
start_date |
DateField (nullable) | -- | First active date (inclusive); blank = immediately |
end_date |
DateField (nullable) | -- | Last active date (inclusive); blank = no end |
ClinicianGroup¶
Groups for categorising clinicians (e.g., "FY Doctor", "FY Supervisor"). Separate from Django auth groups.
| Field | Type | Description |
|---|---|---|
name |
CharField(100, unique) | Group name |
description |
TextField (blank) | Group purpose |
Many-to-many relationship with Clinician via Clinician.groups.
Clinician¶
A doctor or clinician in the rota system.
| Field | Type | Description |
|---|---|---|
name |
CharField(255) | Full name |
email |
EmailField (nullable) | Contact email for notifications |
user |
OneToOneField(auth.User, nullable, SET_NULL) | Linked login account; related_name clinician_profile |
active |
BooleanField (default True) | Currently active in the rota |
groups |
ManyToManyField(ClinicianGroup) | Clinician groups for rota rules |
calendar_feed_secret |
CharField(64, unique, nullable) | Secret key for calendar feed |
calendar_feed_enabled |
BooleanField (default False) | Whether calendar feed is active |
Notable behaviour:
- type is a property derived from the active WorkingTerm, not a stored field. Returns SALARIED as default.
- is_resident_doctor() returns True if active WorkingTerm is FY_DOCTOR or ST_DOCTOR.
- regenerate_calendar_secret() generates a new cryptographically secure token.
Invitation¶
Invitation for a user to create an account linked to a Clinician.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | The clinician to invite |
email |
EmailField | Delivery address |
token |
CharField(64, unique) | Cryptographic signup token |
expires_at |
DateTimeField | Expiry (14 days from creation) |
used |
BooleanField (default False) | Whether invitation has been used |
used_at |
DateTimeField (nullable) | When it was used |
Class method create_invitation(clinician, email, days_valid=14) handles token generation.
UserEmailPreferences¶
Admin email notification preferences.
| Field | Type | Default | Description |
|---|---|---|---|
user |
OneToOneField(User, CASCADE) | -- | Owning user |
email_pref_system_failures |
BooleanField | False | System failure emails |
email_pref_admin_actions |
BooleanField | False | Admin action emails |
email_pref_operational |
BooleanField | False | Operational notification emails |
email_pref_ai_mutations |
BooleanField | False | AI API mutation emails |
email_pref_shift_notifications |
BooleanField | True | Shift change notification emails |
All preferences are opt-in (default False) except
email_pref_shift_notifications, which is opt-out (default True) so admins
who never touched their preferences keep receiving shift-change alerts.
Admin-email recipient selection is centralised in
config.permission_helpers.get_admin_email_recipients_for_type() (#164).
WorkingTerm¶
A period of employment with a specific working pattern. Clinicians can have multiple non-overlapping terms over time.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | The clinician; related_name working_terms |
type |
CharField(20, choices WorkingTermType) | Employment type for this term |
start_date |
DateField | When the term begins |
end_date |
DateField (nullable) | When it ends (null = ongoing) |
percentage |
DecimalField(5,2) | FTE percentage, 0-100 (default 100) |
fixed_working_days |
JSONField (list) | Days the clinician must work (salaried) |
fixed_half_days |
JSONField (list) | Days that must be half days |
cannot_work_days |
JSONField (list) | Days the clinician cannot work (partners) |
must_work_days |
JSONField (list) | Days the clinician must work (partners) |
annual_leave_entitlement |
JSONField (dict) | Single-pool entitlement: {"total": 10.5} |
participates_in_duty |
BooleanField (default True) | Whether included in duty rotation |
minimum_shifts_per_week |
IntegerField (nullable) | Min shifts/week for partners (null = no minimum) |
max_shifts_per_week |
IntegerField (nullable) | Max shifts/week for partners (null = no maximum) |
Key class methods:
| Method | Returns | Description |
|---|---|---|
get_active_term_for_date(clinician, date) |
WorkingTerm or None | Term overlapping the given date |
get_active_term(clinician, as_of_date=None) |
WorkingTerm or None | Alias, defaults to today |
get_term_near_date(clinician, date, months_window=6) |
WorkingTerm or None | Active, upcoming, or recent past term |
get_terms_in_period(clinician, start, end) |
QuerySet | All terms overlapping a date range |
get_latest_term(clinician) |
WorkingTerm or None | Most recent term by start_date |
Properties: total_leave_adjustments sums all linked LeaveAdjustment amounts; get_effective_entitlement() adds adjustments to base entitlement.
SupervisionRelationship¶
Links a trainee's WorkingTerm to a supervisor Clinician. One supervisor per WorkingTerm (unique constraint).
| Field | Type | Description |
|---|---|---|
supervisor |
FK(Clinician, CASCADE) | The supervising clinician |
working_term |
FK(WorkingTerm, CASCADE) | The trainee's working term |
Validation: a clinician cannot supervise themselves.
Shift¶
A scheduled working day for a clinician.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | Assigned clinician; related_name shifts |
date |
DateField | Shift date (indexed) |
type |
CharField(20, choices ShiftType) | Shift type (default STANDARD) |
duration |
CharField(10, choices ShiftDuration) | FULL or HALF (default FULL) |
status |
CharField(20, choices ShiftStatus) | SCHEDULED, COMPLETED, CANCELLED |
is_pinned |
BooleanField (default False) | Immune to automatic rebalancing |
is_off_sick |
BooleanField (default False) | Marked as off sick |
systm1_synced |
BooleanField (default False) | Synced to Systm1 |
is_locum_booking |
BooleanField (default False) | Locum booking, bypasses standard validation |
created_by |
CharField(255) | User who created |
modified_by |
CharField(255) | User who last modified |
Database constraint: one SCHEDULED non-locum shift per clinician per day.
Properties:
- counts_as_worked -- True for every session type (STANDARD, DUTY, STUDY_LEAVE, CORONERS), including off-sick shifts (issue #197). Leave types (ANNUAL_LEAVE, NOT_WORKING) are not shift types and never appear here.
- counts_toward_minimum -- False for STUDY_LEAVE, CORONERS, off-sick shifts, and resident doctor (FY/ST) shifts.
save() accepts send_notification=False kwarg to suppress email notifications (for bulk operations). delete() also accepts this kwarg.
PendingShiftNotification¶
Queued shift change notification for batch mode. When SystemConfiguration.shift_notification_batch_mode is active, shift changes are recorded here instead of sending emails immediately.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, SET_NULL, nullable) | Assigned clinician |
clinician_name |
CharField(255) | Denormalised name (survives shift deletion) |
shift_date |
DateField | Date of the shift |
shift_type |
CharField(20) | Shift type |
shift_duration |
CharField(10) | FULL or HALF |
change_type |
CharField(10, choices ChangeType) | CREATED, UPDATED, or DELETED |
change_description |
TextField | Human-readable description |
created_by |
CharField(255) | User who made the change |
Class method consolidate_for_shift(clinician, date) merges redundant pairs (e.g., CREATED + DELETED = remove both).
LeaveRequest¶
A request for time off by a clinician.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | Requesting clinician; related_name leave_requests |
type |
CharField(20, choices LeaveType) | Leave type |
start_date |
DateField | First day of leave |
end_date |
DateField | Last day of leave |
status |
CharField(20, choices LeaveStatus) | REQUESTED, APPROVED, DENIED, CANCELLED |
processed_at |
DateTimeField (nullable) | When processed |
processed_by |
CharField(255) | User who processed |
denial_reason |
TextField (blank) | Required when status is DENIED |
affected_shift_count |
DecimalField(max_digits=10, decimal_places=1) | Calculated shifts affected |
exceeded_quota |
BooleanField (default False) | Whether approval exceeded remaining quota |
note |
CharField(500, blank) | Public note shown on the calendar and ICS feed (issue #200) |
originating_round |
FK(AnnualLeaveRound, nullable, SET_NULL) | Set when the request was created by round conversion |
Properties: is_quota_based delegates to LeaveType.is_quota_based(). type_display returns human-readable label.
save() accepts skip_validation=True kwarg for status-only changes.
LeaveAdjustment¶
Manual adjustment to a clinician's leave quota. This is the only mechanism for carry-over (no automatic carry-over exists).
| Field | Type | Description |
|---|---|---|
working_term |
FK(WorkingTerm, CASCADE) | Term this adjustment applies to; related_name leave_adjustments |
amount |
DecimalField(5,2) | Positive to add, negative to subtract (cannot be zero) |
reason |
TextField | Explanation for the adjustment |
created_by |
CharField(255) | Admin who created it |
ManualAdjustment¶
Legacy adjustment model (superseded by LeaveAdjustment for leave quota changes).
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | Adjusted clinician; related_name manual_adjustments |
type |
CharField(30, choices ManualAdjustmentType) | Adjustment type |
adjustment_value |
FloatField | Value (can be negative) |
reporting_period |
CharField(50, blank) | Reporting period label |
reason |
TextField | Reason |
created_by |
CharField(255) | Admin who created it |
LocumBooking¶
Scheduled dates for a locum doctor. Locums work manually specified dates only and are not part of automatic shift allocation.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | The locum clinician; related_name locum_bookings |
dates |
JSONField (list) | Specific dates: ["2024-06-15", ...] |
start_date |
DateField (nullable) | Range booking start |
end_date |
DateField (nullable) | Range booking end |
created_by |
CharField(255) | User who created |
SickLeave¶
Tracks when a clinician is off sick. Created and managed by admins.
| Field | Type | Description |
|---|---|---|
clinician |
FK(Clinician, CASCADE) | Clinician off sick; related_name sick_leaves |
start_date |
DateField | First day of sick leave |
end_date |
DateField (nullable) | Last day (null = ongoing) |
status |
CharField(20, choices SickLeaveStatus) | ACTIVE, RETURNED, CANCELLED |
notes |
TextField (blank) | Admin notes (not visible to clinician) |
recorded_by |
CharField(255) | Admin who recorded it |
Class method get_active_for_date(clinician, date) returns active sick leave for a specific date.
ShiftSwapRequest¶
Request to swap shifts between two clinicians with approval workflow: target accepts/declines, then admin approves/rejects.
| Field | Type | Description |
|---|---|---|
requester_shift |
FK(Shift, CASCADE) | Shift being offered |
target_shift |
FK(Shift, CASCADE) | Shift being requested |
requester |
FK(Clinician, CASCADE) | Clinician initiating swap |
target_clinician |
FK(Clinician, CASCADE) | Clinician whose shift is requested |
status |
CharField(25, choices SwapStatus) | Current status |
responded_at |
DateTimeField (nullable) | When target responded |
admin_processed_at |
DateTimeField (nullable) | When admin processed |
response_by |
FK(User, SET_NULL, nullable) | User who responded on behalf of target |
admin_response_by |
FK(User, SET_NULL, nullable) | Admin who approved/rejected |
rejection_reason |
TextField (blank) | Reason for rejection |
Same-type rule: shifts must match type (duty with duty, standard with standard, locum with locum).
Alert¶
An alert about a rota issue (staffing, overwork, constraints).
| Field | Type | Description |
|---|---|---|
type |
CharField(30, choices AlertType) | Alert category |
severity |
CharField(10, choices AlertSeverity) | WARNING or CRITICAL |
date |
DateField | Affected date |
message |
CharField(500) | Alert message |
details |
JSONField | Additional context |
status |
CharField(20, choices AlertStatus) | ACTIVE, RESOLVED, DISMISSED |
resolved_at |
DateTimeField (nullable) | Auto-set when status leaves ACTIVE |
AuditLog¶
Tracks all changes to important entities.
| Field | Type | Description |
|---|---|---|
entity_type |
CharField(50) | Entity type name (e.g., "Shift") |
entity_id |
CharField(255) | Entity primary key |
action |
CharField(20, choices AuditAction) | CREATE, UPDATE, DELETE |
user_id |
CharField(255, nullable) | User who made the change |
previous_state |
JSONField | Snapshot before change |
new_state |
JSONField | Snapshot after change |
reason |
TextField (blank) | Reason for change |
CustomRule¶
Defines custom rules for specific rota requirements (e.g., trainer must be present on Mondays).
| Field | Type | Description |
|---|---|---|
name |
CharField(255) | Rule name |
role_required |
CharField(100) | Required role (e.g., "trainer") |
applicable_days |
JSONField (list) | Day names or specific dates |
description |
TextField (blank) | Rule description |
active |
BooleanField (default True) | Currently active |
HolidayPeriod¶
A date range during which the practice operates under holiday rules (the successor to the removed SystemConfiguration.christmas_period_* fields). Typically one row per year (e.g. the Christmas/New Year closure), but supports arbitrary disjoint periods. Leave is blocked during holiday periods and the leave-booking calendar renders these days with a distinct grey/hatched style.
| Field | Type | Description |
|---|---|---|
name |
CharField(100) | Human-readable label (e.g. "Christmas 2026") |
start_date |
DateField | First day of the period (inclusive) |
end_date |
DateField | Last day of the period (inclusive) |
Check constraint: end_date >= start_date. Ordered by -start_date. Queried by is_date_in_holiday_period() in calculations/date_utils.py.
LeaveCancellationRequest¶
A request to cancel already-approved leave that falls inside the Fixed Period (cancellation of in-flight leave requires admin approval). See ADR 0001 and the leave_cancellation_requests[] AI endpoint.
| Field | Type | Description |
|---|---|---|
leave_request |
FK(LeaveRequest, CASCADE) | The approved leave being cancelled |
reason |
TextField | Clinician's reason for the cancellation |
status |
CharField(choices CancellationStatus) | PENDING (default), APPROVED, or DENIED |
denial_reason |
TextField (blank) | Admin's reason when DENIED (required if status is DENIED) |
requested_by |
CharField(255) | Clinician who requested the cancellation |
processed_by |
CharField(255) | Admin who approved/denied it |
created_at |
DateTimeField (auto) | When the request was created |
processed_at |
DateTimeField (nullable) | When it was approved/denied |
is_within_approval_period(leave_request) classmethod returns whether a leave request overlaps the Fixed Period and therefore requires a cancellation request. Approve/deny are atomic with row locking (select_for_update).
AIApiKey¶
An API key authenticating an external AI agent against the v3 API. Linked to a Django user and carries the active-transaction context.
| Field | Type | Description |
|---|---|---|
user |
FK(User) | The owning user |
key |
CharField (unique) | The secret API key |
name |
CharField | Human-readable label |
is_active |
BooleanField | Whether the key is enabled |
created_at / last_used_at |
DateTimeField | Lifecycle timestamps |
AITransaction¶
A staged-mutation transaction against clinician/working-term/shift data, gating AI writes behind optional browser approval.
| Field | Type | Description |
|---|---|---|
api_key |
FK(AIApiKey) | Key that opened the transaction |
status |
CharField | PENDING / PROPOSED / APPROVED / REJECTED / TIMEOUT |
created_at / proposed_at / approved_at (+ approved_by) |
DateTimeField / FK(User) (nullable) | Lifecycle timestamps and approver |
snapshot |
JSONField | Pre-mutation state captured for rollback |
AILock¶
A date-range lock preventing concurrent AI mutations from racing: one live transaction may hold the lock for a given date range, tied one-to-one to that transaction. While a transaction is PROPOSED its lock has no expiry; approval or rejection releases it.
| Field | Type | Description |
|---|---|---|
transaction |
OneToOneField(AITransaction) | The transaction holding the lock |
start_date / end_date |
DateField | The date range being mutated |
created_at |
DateTimeField (auto) | When the lock was taken |
expires_at |
DateTimeField (nullable) | Auto-release time (NULL while PROPOSED) |
ShiftStaging¶
A shift add/delete staged within a PENDING AITransaction, applied atomically when the transaction is approved (or discarded on rollback). Surfaced via the shifts/staged/ endpoint.
| Field | Type | Description |
|---|---|---|
transaction |
FK(AITransaction, CASCADE) | Owning transaction |
clinician |
FK(Clinician) | Assigned clinician |
date |
DateField | Shift date |
type / duration / status |
CharField | STANDARD/DUTY/etc., FULL/HALF, SCHEDULED/etc. |
is_deletion |
BooleanField | True = this stages a delete, False = an add |
is_pinned |
BooleanField | Pinned against optimisation |
constraint_overrides |
JSONField | Validation constraints to bypass |
created_at / modified_at |
DateTimeField (auto) | Timestamps |
Annual Leave Round Models (leave_round app)¶
The staged yearly allocation of next-year annual leave (issues #179–#192). Domain terms are defined in CONTEXT.md ("Annual Leave Round" section); the admin workflow is documented in the Admin Guide.
AnnualLeaveRound¶
One round per leave year, driven through a lifecycle by RoundService (leave_round/services.py).
| Field | Type | Description |
|---|---|---|
leave_year |
IntegerField (unique) | The year this round allocates leave for |
status |
CharField(choices RoundStatus) | DRAFT → OPEN → RESOLUTION → LOCKED |
window_opens_at / window_closes_at |
DateTimeField | When clinicians may submit/change selections |
round_cap_default |
IntegerField | Default max Week Selections per participant (the cap is min(this, entitlement-in-weeks)) |
late_year_limit |
IntegerField | Max selections a participant may place in Aug–Dec |
opened_at / closed_at / locked_at |
DateTimeField (nullable) | Lifecycle timestamps (cleared by a step-back) |
created_by / updated_by |
CharField | Audit usernames |
ALLOWED_TRANSITIONS in services.py defines both forward transitions and the one-step undos (OPEN→DRAFT, RESOLUTION→OPEN, LOCKED→RESOLUTION; unlock refused once a newer round exists). Configuration is editable in DRAFT only.
WeekSelection¶
A participant's ranked claim on one Monday–Friday week.
| Field | Type | Description |
|---|---|---|
round |
FK(AnnualLeaveRound, CASCADE) | Owning round |
clinician |
FK(Clinician, CASCADE) | The participant |
week_commencing |
DateField | The Monday of the selected week |
priority |
IntegerField | 1 = most wanted (advisory, never mechanically enforced) |
Unique on (round, clinician, week_commencing). Hard-validated against the participant's round cap and the late-year limit at submission; admins may exceed the cap manually during Resolution.
SelectionGhost¶
The dotted board record of where a selection originally sat: written when an admin moves or cancels a selection from its creation week, cleared if it returns there.
| Field | Type | Description |
|---|---|---|
round |
FK(AnnualLeaveRound, CASCADE) | Owning round |
clinician |
FK(Clinician, CASCADE) | Whose selection it records |
week_commencing |
DateField | The original week |
priority |
IntegerField | The priority it originally carried |
SchoolHolidayPeriod¶
A named school-holiday date range for one local district (display-only overlay on the selection grid; affects no validation).
| Field | Type | Description |
|---|---|---|
district |
CharField(100) | Which district's pattern (ranges for one district render in one colour) |
name |
CharField(100) | Label (e.g. "February half-term") |
start_date / end_date |
DateField | Inclusive range |
Relationships¶
erDiagram
User ||--o| Clinician : "clinician_profile (SET_NULL)"
Clinician ||--o{ WorkingTerm : "working_terms (CASCADE)"
Clinician ||--o{ Shift : "shifts (CASCADE)"
Clinician ||--o{ LeaveRequest : "leave_requests (CASCADE)"
Clinician ||--o{ ManualAdjustment : "manual_adjustments (CASCADE)"
Clinician ||--o{ LocumBooking : "locum_bookings (CASCADE)"
Clinician ||--o{ Invitation : "invitations (CASCADE)"
Clinician ||--o{ SickLeave : "sick_leaves (CASCADE)"
Clinician }o--o{ ClinicianGroup : "groups (M2M)"
Clinician ||--o{ ShiftSwapRequest : "initiated_swap_requests"
Clinician ||--o{ ShiftSwapRequest : "received_swap_requests"
WorkingTerm ||--o{ LeaveAdjustment : "leave_adjustments (CASCADE)"
WorkingTerm ||--o| SupervisionRelationship : "supervision_relationships (CASCADE)"
Clinician ||--o{ SupervisionRelationship : "supervision_as_supervisor (CASCADE)"
Shift ||--o{ ShiftSwapRequest : "outgoing_swap_requests"
Shift ||--o{ ShiftSwapRequest : "incoming_swap_requests"
Clinician ||--o{ PendingShiftNotification : "pending_notifications (SET_NULL)"
User ||--o| UserEmailPreferences : "email_prefs (CASCADE)"
User ||--o{ ShiftSwapRequest : "swap_responses (SET_NULL)"
User ||--o{ ShiftSwapRequest : "swap_admin_responses (SET_NULL)"
LeaveRequest ||--o{ LeaveCancellationRequest : "cancellation_requests (CASCADE)"
User ||--o{ AIApiKey : "ai_api_keys (CASCADE)"
AIApiKey ||--o{ AITransaction : "transactions (SET_NULL)"
AITransaction ||--o{ ShiftStaging : "staged_shifts (CASCADE)"
AIApiKey ||--o{ AILock : "locks (SET_NULL)"
SystemConfiguration {
int id
}
AuditLog {
string entity_type
string entity_id
}
Alert {
string type
date date
}
CustomRule {
string name
}
Validation Rules¶
SystemConfiguration¶
| Rule | Field(s) |
|---|---|
target_working_days_per_week must be > 0 and <= 7 |
target_working_days_per_week |
minimum_doctors must be a dict with Mon-Fri keys, positive integer values |
minimum_doctors |
duty_doctors_required must be > 0 |
duty_doctors_required |
duty_doctors_post_bank_holiday must be > 0 |
duty_doctors_post_bank_holiday |
post_bank_holiday_minimum must be a weekday name (Mon-Fri; weekend minimums are always 0) |
post_bank_holiday_minimum |
fixed_period_weeks must be > 0 |
fixed_period_weeks |
uk_nation must be one of: england, scotland, wales, northern_ireland |
uk_nation |
uk_nation cannot change after shifts exist |
uk_nation |
| WTE balance weights must be between 0 and 1 and sum to 1.0 | wte_balance_deficit_weight, wte_balance_day_distribution_weight |
optimization_iterations must be >= 1 |
optimization_iterations |
ai_api_approval_timeout_minutes must be positive |
ai_api_approval_timeout_minutes |
WorkingTerm¶
| Rule | Field(s) |
|---|---|
end_date >= start_date (if both set) |
start_date, end_date |
percentage must be 0-100 |
percentage |
| Day fields must contain only valid weekday names | fixed_working_days, fixed_half_days, cannot_work_days, must_work_days |
| No overlapping terms for the same clinician | start_date, end_date |
annual_leave_entitlement.total must be non-negative numeric |
annual_leave_entitlement |
| Partner entitlement capped at 52; salaried at 260 | annual_leave_entitlement |
Shift¶
| Rule | Field(s) |
|---|---|
| Clinician must have an active WorkingTerm on the shift date | date, clinician |
| Only one SCHEDULED non-locum shift per clinician per day (DB constraint) | clinician, date, status, is_locum_booking |
LeaveRequest¶
| Rule | Field(s) |
|---|---|
end_date >= start_date |
start_date, end_date |
denial_reason required when status is DENIED |
denial_reason, status |
NOT_WORKING type only available to partners |
type, clinician |
| Partners must request ANNUAL_LEAVE for full calendar weeks (Mon-Fri) | start_date, end_date, type |
LeaveAdjustment¶
| Rule | Field(s) |
|---|---|
amount cannot be zero |
amount |
SickLeave¶
| Rule | Field(s) |
|---|---|
end_date >= start_date (if set) |
start_date, end_date |
SupervisionRelationship¶
| Rule | Field(s) |
|---|---|
| Supervisor cannot be the same clinician as the working term's clinician | supervisor, working_term |
| One supervisor per working term (unique constraint) | working_term |
Cascade Summary¶
| Parent | Child | On Delete |
|---|---|---|
| Clinician | WorkingTerm | CASCADE |
| Clinician | Shift | CASCADE |
| Clinician | LeaveRequest | CASCADE |
| Clinician | ManualAdjustment | CASCADE |
| Clinician | LocumBooking | CASCADE |
| Clinician | Invitation | CASCADE |
| Clinician | SickLeave | CASCADE |
| Clinician | SupervisionRelationship (as supervisor) | CASCADE |
| WorkingTerm | LeaveAdjustment | CASCADE |
| WorkingTerm | SupervisionRelationship | CASCADE |
| Shift | ShiftSwapRequest | CASCADE |
| User | Clinician (via user FK) | SET_NULL |
| User | UserEmailPreferences | CASCADE |