Files
LT_Janitorial_Quality_Control/app/models/scheduled_inspection.py
T

118 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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}>')