The PM panel (PmPanelPage -> iframe /pm/) needs the project-management platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM, branch feature/project-management-platform. Update via: git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""Lightweight schema sync for additive/structural changes.
|
|
|
|
`Base.metadata.create_all` creates NEW tables but never alters existing ones,
|
|
so this module applies the missing pieces at startup:
|
|
|
|
1. `drop_legacy()` — tables whose shape changed or whose feature was removed
|
|
(including all legacy cache & sync tables).
|
|
2. `ensure_schema()` — additive columns and new enum values.
|
|
"""
|
|
import logging
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
|
|
logger = logging.getLogger("pm.schema")
|
|
|
|
# Tables dropped on startup (idempotent):
|
|
# - legacy cache & sync tables completely purged
|
|
# - legacy inspection / activity template tables
|
|
LEGACY_TABLES: list[str] = [
|
|
"material_requirements",
|
|
"reservations",
|
|
"milestones",
|
|
"purchase_requests",
|
|
"inspection_results",
|
|
"inspections",
|
|
"activity_template_materials",
|
|
"activity_templates",
|
|
"projects_cache",
|
|
"task_refs",
|
|
"sync_events",
|
|
"integration_logs",
|
|
"webhook_deliveries",
|
|
"sync_state",
|
|
]
|
|
|
|
LEGACY_COLUMNS: list[tuple[str, str]] = [
|
|
("projects", "client"),
|
|
("projects", "code"),
|
|
("projects", "erp_project_id"),
|
|
("tasks", "planned_progress"),
|
|
("projects", "legacy_op_project_id"),
|
|
("tasks", "legacy_op_wp_id"),
|
|
("task_qc_definitions", "task_op_wp_id"),
|
|
("defects", "task_op_wp_id"),
|
|
("project_assignments", "op_project_id"),
|
|
("tasks", "equipment"),
|
|
]
|
|
|
|
ADDITIVE_COLUMNS: list[tuple[str, str, str]] = [
|
|
# (table, column, type)
|
|
# RBAC: users that predate roles keep their previous (manager) access
|
|
("pm_users", "role", "VARCHAR(32) NOT NULL DEFAULT 'project_manager'"),
|
|
# Native PM Platform linking
|
|
("task_qc_definitions", "task_id", "UUID"),
|
|
("defects", "task_id", "UUID"),
|
|
("project_assignments", "project_id", "UUID"),
|
|
("tasks", "human_resources", "JSONB DEFAULT '[]'"),
|
|
("tasks", "materials", "JSONB DEFAULT '[]'"),
|
|
("task_qc_results", "photo_path", "VARCHAR(512)"),
|
|
]
|
|
|
|
ENUM_VALUES: list[tuple[str, str]] = [
|
|
# (enum type, value) — e.g. the pre-execution QC stage
|
|
("qc_phase", "pre"),
|
|
]
|
|
|
|
|
|
async def drop_legacy(conn: AsyncConnection) -> None:
|
|
# Migrate any existing equipment data to materials before dropping the equipment column
|
|
try:
|
|
check = await conn.execute(
|
|
text("SELECT 1 FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'equipment'")
|
|
)
|
|
if check.scalar():
|
|
await conn.execute(
|
|
text("""
|
|
UPDATE tasks
|
|
SET materials = COALESCE(materials, '[]'::jsonb) || (
|
|
SELECT COALESCE(jsonb_agg(
|
|
jsonb_build_object(
|
|
'name', elem->>'name',
|
|
'unit', COALESCE(elem->>'unit', 'دستگاه'),
|
|
'specifications', COALESCE(elem->'specifications', '[]'::jsonb),
|
|
'count', COALESCE((elem->>'count')::numeric, 1)
|
|
)
|
|
), '[]'::jsonb)
|
|
FROM jsonb_array_elements(equipment) AS elem
|
|
WHERE elem->>'name' IS NOT NULL AND elem->>'name' != ''
|
|
)
|
|
WHERE equipment IS NOT NULL AND jsonb_array_length(equipment) > 0;
|
|
""")
|
|
)
|
|
logger.info("Migrated equipment data into materials in tasks table.")
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("equipment migration skipped: %s", exc)
|
|
|
|
for table in LEGACY_TABLES:
|
|
try:
|
|
await conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE"))
|
|
except Exception as exc: # noqa: BLE001 — never block startup on this
|
|
logger.warning("legacy drop skipped %s: %s", table, exc)
|
|
|
|
for table, column in LEGACY_COLUMNS:
|
|
try:
|
|
await conn.execute(text(f"ALTER TABLE IF EXISTS {table} DROP COLUMN IF EXISTS {column} CASCADE"))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("legacy column drop skipped %s.%s: %s", table, column, exc)
|
|
|
|
|
|
async def ensure_schema(conn: AsyncConnection) -> None:
|
|
for enum_type, value in ENUM_VALUES:
|
|
try:
|
|
await conn.execute(text(f"ALTER TYPE {enum_type} ADD VALUE IF NOT EXISTS '{value}'"))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("enum sync skipped %s.%s: %s", enum_type, value, exc)
|
|
|
|
for table, column, coltype in ADDITIVE_COLUMNS:
|
|
try:
|
|
await conn.execute(text(f"ALTER TABLE IF EXISTS {table} ADD COLUMN IF NOT EXISTS {column} {coltype}"))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("schema sync skipped %s.%s: %s", table, column, exc)
|
|
|
|
try:
|
|
await conn.execute(text("""
|
|
CREATE TABLE IF NOT EXISTS task_progress_logs (
|
|
id UUID PRIMARY KEY,
|
|
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
progress_before INTEGER DEFAULT 0,
|
|
progress_after INTEGER NOT NULL,
|
|
human_resources_used JSONB DEFAULT '[]',
|
|
materials_used JSONB DEFAULT '[]',
|
|
description TEXT DEFAULT '',
|
|
logged_by VARCHAR(255) DEFAULT '',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""))
|
|
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_task_progress_logs_task ON task_progress_logs (task_id)"))
|
|
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_task_progress_logs_created ON task_progress_logs (created_at)"))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("task_progress_logs table sync skipped: %s", exc)
|
|
|