Jul 8 - Update scheduled inspection

This commit is contained in:
2026-07-08 15:46:14 -04:00
parent f08ca997d7
commit 4d661e9776
15 changed files with 795 additions and 6 deletions
+117
View File
@@ -0,0 +1,117 @@
"""
app/models/scheduled_inspection.py
-----------------------------------
Planned/recurring inspection assignments (phase36).
A ScheduledInspection is a PLAN, not an inspection: it names a facility, a
template, the responsible inspector, and a due date. The inspector opens it
("Start"), which creates a normal in_progress Inspection linked back via
Inspection.scheduled_inspection_id; when that inspection is completed the
schedule is marked fulfilled — deactivated (one-time) or rolled forward to the
next occurrence (recurring).
Reminders are dispatched by the cron endpoint
POST /scheduled-inspections/run?token=DIGEST_SECRET:
- advance reminder to the inspector 1 day before the due date
- due reminder to the inspector on the due date
- overdue alert to admin/director once the due date passes uncompleted
The *_notified flags make each of those fire at most once per occurrence and
reset when a recurring schedule rolls forward.
"""
from datetime import timedelta
from app import db
from app.utils.time_utils import now_eastern
FREQUENCY_CHOICES = ('once', 'daily', 'weekly', 'monthly')
class ScheduledInspection(db.Model):
__tablename__ = 'scheduled_inspections'
id = db.Column(db.Integer, primary_key=True)
facility_id = db.Column(db.Integer,
db.ForeignKey('facilities.id', ondelete='CASCADE'),
nullable=False, index=True)
template_id = db.Column(db.Integer,
db.ForeignKey('inspection_templates.id', ondelete='CASCADE'),
nullable=False)
inspector_id = db.Column(db.Integer,
db.ForeignKey('users.id', ondelete='SET NULL'),
nullable=True, index=True)
frequency = db.Column(db.Enum(*FREQUENCY_CHOICES), nullable=False, default='once')
next_due_date = db.Column(db.Date, nullable=False, index=True)
active = db.Column(db.Boolean, nullable=False, default=True)
notes = db.Column(db.Text, nullable=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'),
nullable=True)
created_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
last_completed_at = db.Column(db.DateTime, nullable=True)
# Per-occurrence reminder de-dup flags (reset when a recurring one 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
facility = db.relationship('Facility', foreign_keys=[facility_id])
template = db.relationship('InspectionTemplate', foreign_keys=[template_id])
inspector = db.relationship('User', foreign_keys=[inspector_id])
creator = db.relationship('User', foreign_keys=[created_by])
FREQUENCY_LABELS = {
'once': 'One-time',
'daily': 'Daily',
'weekly': 'Weekly',
'monthly': 'Monthly',
}
@property
def frequency_label(self):
return self.FREQUENCY_LABELS.get(self.frequency, self.frequency)
@staticmethod
def _add_interval(d, frequency):
"""Return d advanced by one interval of the given frequency."""
if frequency == 'daily':
return d + timedelta(days=1)
if frequency == 'weekly':
return d + timedelta(weeks=1)
if frequency == 'monthly':
# Add ~1 month by stepping 2831 days to the same day-of-month where possible.
month = d.month + 1
year = d.year + (1 if month > 12 else 0)
month = 1 if month > 12 else month
day = min(d.day, 28) # clamp to avoid invalid dates (e.g. Feb 30)
return d.replace(year=year, month=month, day=day)
return d # 'once' has no next interval
def is_overdue(self, today=None):
today = today or now_eastern().date()
return self.active and self.next_due_date < today
def fulfill(self):
"""Mark this occurrence complete. One-time schedules deactivate;
recurring ones roll their due date forward past today and reset the
reminder flags. Caller commits."""
self.last_completed_at = now_eastern()
if self.frequency == 'once':
self.active = False
return
# Recurring: advance until the next due date is in the future.
today = now_eastern().date()
nxt = self._add_interval(self.next_due_date, self.frequency)
guard = 0
while nxt <= today and guard < 400:
nxt = self._add_interval(nxt, self.frequency)
guard += 1
self.next_due_date = nxt
self.advance_notified = False
self.due_notified = False
self.overdue_notified = False
def __repr__(self):
return (f'<ScheduledInspection {self.id} facility={self.facility_id} '
f'freq={self.frequency} due={self.next_due_date}>')