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,87 @@
"""Attachment storage on the local filesystem (a named Docker volume in
production). Files live at ATTACHMENTS_DIR/<ncr_id>/<uuid><ext>; metadata is
kept in the attachments table."""
import re
import uuid
from pathlib import Path
from fastapi import UploadFile
from app.config import get_settings
ALLOWED_EXTENSIONS = {
# images (camera capture on tablets produces jpg/png/heic)
".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".heif", ".bmp", ".tiff", ".tif",
# documents
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".csv", ".txt", ".msg", ".eml",
}
IMAGE_EXTENSIONS = {
".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".heif", ".bmp", ".tiff", ".tif",
}
CHUNK_SIZE = 1024 * 1024
class UploadValidationError(Exception):
pass
def _safe_filename(name: str) -> str:
name = Path(name or "upload").name
return re.sub(r"[^\w.\- ()]", "_", name)[:255] or "upload"
async def save_attachment(upload: UploadFile, ncr_id: int) -> dict:
"""Validate and persist an uploaded file. Returns metadata for the
Attachment row. Raises UploadValidationError on type/size violations."""
settings = get_settings()
original = _safe_filename(upload.filename or "upload")
ext = Path(original).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise UploadValidationError(
f"File type '{ext or 'unknown'}' is not allowed. "
f"Allowed: {', '.join(sorted(ALLOWED_EXTENSIONS))}"
)
stored_rel = f"{ncr_id}/{uuid.uuid4().hex}{ext}"
dest = Path(settings.attachments_dir) / stored_rel
dest.parent.mkdir(parents=True, exist_ok=True)
size = 0
max_bytes = settings.max_upload_bytes
try:
with dest.open("wb") as out:
while chunk := await upload.read(CHUNK_SIZE):
size += len(chunk)
if size > max_bytes:
raise UploadValidationError(
f"File exceeds the {settings.max_upload_mb} MB limit."
)
out.write(chunk)
except UploadValidationError:
dest.unlink(missing_ok=True)
raise
except Exception:
dest.unlink(missing_ok=True)
raise
if size == 0:
dest.unlink(missing_ok=True)
raise UploadValidationError("Uploaded file is empty.")
return {
"original_filename": original,
"stored_path": stored_rel,
"content_type": upload.content_type or "application/octet-stream",
"size_bytes": size,
"is_image": ext in IMAGE_EXTENSIONS,
}
def attachment_abs_path(stored_path: str) -> Path:
settings = get_settings()
base = Path(settings.attachments_dir).resolve()
p = (base / stored_path).resolve()
if not str(p).startswith(str(base)):
raise UploadValidationError("Invalid attachment path.")
return p