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
@@ -0,0 +1,120 @@
"""phase43 — plan semantics for inspection_schedules
Extends MT's `inspection_schedules` (phase34) with the single-tenant "plan"
model (ST phase36_scheduled_inspections), rather than adding a second, competing
scheduler table. Adds:
notes TEXT — free-text brief for the inspector
last_completed_at DATETIME — when the last occurrence was fulfilled
advance_notified BOOL — per-occurrence reminder de-dup flags, reset
due_notified BOOL when a recurring schedule rolls forward
overdue_notified BOOL
mode ENUM — 'auto' = cron materialises the Inspection
'plan' = inspector clicks Start (ST behaviour)
inspections.inspection_schedule_id — FK back to the originating schedule
`next_run_at` (phase34) is reused as the due datetime — ST's `next_due_date` by
another name. No duplicate column, no renames (rule 7).
Existing rows keep working exactly as before: `mode` defaults to 'auto', which
is the only behaviour that has ever existed in MT. Nothing flips to 'plan'
unless someone chooses it in the form.
INFORMATION_SCHEMA-guarded throughout — safe to re-run on every tenant DB.
"""
revision = 'phase43_schedule_plan_fields'
down_revision = 'phase42_area_qr_token'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def _column_exists(conn, table, column):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND COLUMN_NAME = :c"
), {"t": table, "c": column}).scalar() > 0
def _fk_exists(conn, table, name):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t "
"AND CONSTRAINT_NAME = :n AND CONSTRAINT_TYPE = 'FOREIGN KEY'"
), {"t": table, "n": name}).scalar() > 0
def _index_exists(conn, table, index):
return conn.execute(sa.text(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
"WHERE TABLE_SCHEMA = DATABASE() "
"AND TABLE_NAME = :t AND INDEX_NAME = :i"
), {"t": table, "i": index}).scalar() > 0
_NEW_COLUMNS = (
('notes', 'TEXT NULL'),
('last_completed_at', 'DATETIME NULL'),
('advance_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
('due_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
('overdue_notified', 'TINYINT(1) NOT NULL DEFAULT 0'),
('mode', "ENUM('auto','plan') NOT NULL DEFAULT 'auto'"),
)
def upgrade():
bind = op.get_bind()
for name, ddl in _NEW_COLUMNS:
if not _column_exists(bind, 'inspection_schedules', name):
op.execute(sa.text(
f"ALTER TABLE inspection_schedules ADD COLUMN {name} {ddl}"
))
# Link a materialised/started Inspection back to its schedule.
if not _column_exists(bind, 'inspections', 'inspection_schedule_id'):
op.execute(sa.text(
"ALTER TABLE inspections ADD COLUMN inspection_schedule_id INT NULL"
))
if not _index_exists(bind, 'inspections', 'ix_inspections_inspection_schedule_id'):
op.execute(sa.text(
"CREATE INDEX ix_inspections_inspection_schedule_id "
"ON inspections (inspection_schedule_id)"
))
if not _fk_exists(bind, 'inspections', 'fk_inspections_inspection_schedule'):
# ON DELETE SET NULL: deleting a schedule must never delete completed
# inspection history.
op.execute(sa.text(
"ALTER TABLE inspections "
"ADD CONSTRAINT fk_inspections_inspection_schedule "
"FOREIGN KEY (inspection_schedule_id) "
"REFERENCES inspection_schedules (id) ON DELETE SET NULL"
))
def downgrade():
bind = op.get_bind()
if _fk_exists(bind, 'inspections', 'fk_inspections_inspection_schedule'):
op.execute(sa.text(
"ALTER TABLE inspections DROP FOREIGN KEY fk_inspections_inspection_schedule"
))
if _index_exists(bind, 'inspections', 'ix_inspections_inspection_schedule_id'):
op.execute(sa.text(
"DROP INDEX ix_inspections_inspection_schedule_id ON inspections"
))
if _column_exists(bind, 'inspections', 'inspection_schedule_id'):
op.execute(sa.text(
"ALTER TABLE inspections DROP COLUMN inspection_schedule_id"
))
for name, _ddl in reversed(_NEW_COLUMNS):
if _column_exists(bind, 'inspection_schedules', name):
op.execute(sa.text(
f"ALTER TABLE inspection_schedules DROP COLUMN {name}"
))