Jul 17 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT4

This commit is contained in:
2026-07-17 11:36:05 -04:00
parent 253291d5a4
commit d44d761706
10 changed files with 565 additions and 19 deletions
+67 -1
View File
@@ -13,6 +13,20 @@ their queue and fills it out through the normal execute flow.
This is purely additive: no existing inspection behaviour changes. A schedule is
just an automated `inspections.start()`.
phase43 adds the single-tenant "plan" semantics alongside that:
mode='auto' (default, phase34 behaviour)
Cron materialises the Inspection at next_run_at and notifies the inspector.
mode='plan' (ST behaviour)
Nothing is materialised. The schedule is a commitment with a due date; the
assigned inspector clicks "Start", which creates the Inspection linked back
via Inspection.inspection_schedule_id. Reminders fire in advance / on the
due date / once overdue. Completing the inspection calls fulfill(), which
deactivates a one-time schedule or rolls a recurring one forward.
`next_run_at` is the due datetime for both modes.
"""
from app import db
@@ -49,6 +63,11 @@ class InspectionSchedule(db.Model):
)
active = db.Column(db.Boolean, nullable=False, default=True)
# phase43: 'auto' = cron materialises the inspection (phase34 behaviour,
# the default for every pre-existing row); 'plan' = the inspector starts it.
mode = db.Column(db.Enum('auto', 'plan'), nullable=False, default='auto')
notes = db.Column(db.Text, nullable=True)
created_by = db.Column(
db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'),
nullable=True
@@ -57,6 +76,13 @@ class InspectionSchedule(db.Model):
last_run_at = db.Column(db.DateTime, nullable=True) # last successful materialisation
next_run_at = db.Column(db.DateTime, nullable=True) # when the next inspection is due
# phase43 — plan mode bookkeeping
last_completed_at = db.Column(db.DateTime, nullable=True)
# Per-occurrence reminder de-dup flags; reset when a recurring schedule rolls forward.
advance_notified = db.Column(db.Boolean, nullable=False, default=False)
due_notified = db.Column(db.Boolean, nullable=False, default=False)
overdue_notified = db.Column(db.Boolean, nullable=False, default=False)
# Relationships — explicit foreign_keys because two columns point at users.id.
template = db.relationship('InspectionTemplate', foreign_keys=[template_id])
facility = db.relationship('Facility', foreign_keys=[facility_id])
@@ -64,5 +90,45 @@ class InspectionSchedule(db.Model):
inspector = db.relationship('User', foreign_keys=[inspector_id])
creator = db.relationship('User', foreign_keys=[created_by])
FREQUENCY_LABELS = {
'daily': 'Daily',
'weekly': 'Weekly',
'monthly': 'Monthly',
'quarterly': 'Quarterly',
}
@property
def frequency_label(self):
return self.FREQUENCY_LABELS.get(self.frequency, self.frequency)
@property
def due_date(self):
"""The due date (date part of next_run_at), or None."""
return self.next_run_at.date() if self.next_run_at else None
def is_overdue(self, today=None):
"""True when an active schedule's due date has passed."""
if not self.active or self.next_run_at is None:
return False
today = today or now_eastern().date()
return self.next_run_at.date() < today
def fulfill(self, next_run_fn=None):
"""Mark this occurrence complete. Caller commits.
Recurring schedules roll their due date forward past today and reset the
reminder flags; MT has no 'once' frequency, so a schedule stays active.
`next_run_fn(frequency, from_dt)` computes the next due datetime — the
route passes `_compute_next_run` so the cadence math lives in one place.
"""
now = now_eastern()
self.last_completed_at = now
if next_run_fn is not None:
self.next_run_at = next_run_fn(self.frequency, now)
self.advance_notified = False
self.due_notified = False
self.overdue_notified = False
def __repr__(self):
return f'<InspectionSchedule {self.id} {self.name!r} {self.frequency}>'
return (f'<InspectionSchedule {self.id} {self.name!r} '
f'{self.frequency} mode={self.mode}>')