Initial commit: PESCO NCR system

Complete Non-Conformance Report system replacing the PowerApps/SharePoint
prototype: FastAPI + SQLAlchemy 2 (async) + Alembic + MySQL 8 backend,
React 18 + Vite + TypeScript + MUI frontend, Entra ID auth (MSAL / JWKS,
group-gated), Microsoft Graph delegated Mail.Send notifications (OBO),
six-stage workflow state machine with server-side enforcement, atomic
NCR-YYYY-NNNN numbering, attachments with camera capture, immutable
field-level audit trail, admin reopen, reports + CSV export, WeasyPrint
PDF traveler, Power BI reporting views + read-only DB user, documented
VISUAL ERP job-lookup stub, pytest suite (26 tests), docker-compose
deployment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ang3l12
2026-07-13 11:41:22 -06:00
commit dea316b113
111 changed files with 13817 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
"""Server-side workflow state machine. Every stage change flows through
`transition()`, which validates against ALLOWED_TRANSITIONS and records both
a StageTransition row (timestamps + acting user, for aging/cycle-time
reporting) and an audit entry."""
from app.domain import ALLOWED_TRANSITIONS, Stage
from app.models import Ncr, StageTransition
from app.models.base import utcnow
from app.services.audit import audit_event
from sqlalchemy.ext.asyncio import AsyncSession
class InvalidTransitionError(Exception):
def __init__(self, from_stage: str, to_stage: str):
self.from_stage = from_stage
self.to_stage = to_stage
super().__init__(f"Invalid stage transition: {from_stage} -> {to_stage}")
def transition(
db: AsyncSession,
ncr: Ncr,
to_stage: Stage,
*,
action: str,
actor_id: int,
note: str | None = None,
) -> None:
from_stage = Stage(ncr.stage)
if to_stage not in ALLOWED_TRANSITIONS.get(from_stage, set()):
raise InvalidTransitionError(from_stage.value, to_stage.value)
now = utcnow()
ncr.stage = to_stage.value
ncr.stage_entered_at = now
db.add(
StageTransition(
ncr_id=ncr.id,
from_stage=from_stage.value,
to_stage=to_stage.value,
action=action,
acted_by_id=actor_id,
acted_at=now,
note=note,
)
)
audit_event(
db,
ncr_id=ncr.id,
user_id=actor_id,
action=action,
field_name="stage",
old_value=from_stage.value,
new_value=to_stage.value,
detail=note,
)
def record_creation(db: AsyncSession, ncr: Ncr, actor_id: int) -> None:
db.add(
StageTransition(
ncr_id=ncr.id,
from_stage=None,
to_stage=Stage.NEW_REQUEST.value,
action="create",
acted_by_id=actor_id,
acted_at=ncr.created_at,
)
)
audit_event(
db,
ncr_id=ncr.id,
user_id=actor_id,
action="create",
detail=f"NCR {ncr.ncr_number} created",
)