Add API Q1 CAPA: root cause, corrective action gate, effectiveness verification
Implements tester feedback against API Q1 §5.9.1.2 / §6.4.2: - Root Cause field + 6M Root Cause Category lookup (Man/Machine/Method/ Material/Measurement/Environment), separate from Deviation Detail/Category - "Corrective Action Required?" Yes/No gate on every NCR with a required justification - Corrective action plan with owner + due date; owner is notified by email - Effectiveness verification (result, notes, server-stamped verifier/date) required before an NCR can close when corrective action is required — costing returns 409 listing the missing pieces - Recurring-issue flag with bidirectional NCR-to-NCR links; prior NCRs show a warning when later NCRs reference them - Dashboard metrics: % root cause completed, % CAPA verified effective, avg CAPA close time, overdue CAPA count, NCRs by root cause category - CAPA section in the NCR detail UI, printable PDF, CSV export, and the vw_ncr_full Power BI view; admin list manager for root cause categories - Migrations 0003 (schema + seeded 6M lookup) and 0004 (view refresh); demo seed data exercises every metric Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.auth.deps import CurrentUser, get_current_user
|
||||
from app.database import get_db
|
||||
from app.domain import STAGE_LABELS, Stage
|
||||
from app.models import Department, DeviationCategory, Ncr, StageTransition
|
||||
from app.models import Department, DeviationCategory, Ncr, RootCauseCategory, StageTransition
|
||||
from app.models.base import utcnow
|
||||
from app.schemas.report import (
|
||||
AgingBucket,
|
||||
@@ -70,9 +70,14 @@ async def reports_summary(
|
||||
c.id: c.name
|
||||
for c in (await db.execute(select(DeviationCategory))).scalars().all()
|
||||
}
|
||||
rcc_names = {
|
||||
c.id: c.name
|
||||
for c in (await db.execute(select(RootCauseCategory))).scalars().all()
|
||||
}
|
||||
|
||||
by_dept: dict[str, int] = defaultdict(int)
|
||||
by_cat: dict[str, int] = defaultdict(int)
|
||||
by_rcc: dict[str, int] = defaultdict(int)
|
||||
by_month: dict[str, int] = defaultdict(int)
|
||||
cost_by_month: dict[str, dict[str, Decimal]] = defaultdict(
|
||||
lambda: {"labor": Decimal(0), "material": Decimal(0), "service": Decimal(0), "other": Decimal(0)}
|
||||
@@ -83,12 +88,38 @@ async def reports_summary(
|
||||
open_count = 0
|
||||
closed_count = 0
|
||||
now = utcnow()
|
||||
today = now.date()
|
||||
|
||||
# ── CAPA metrics (API Q1 §6.4.2) ─────────────────────────────────────────
|
||||
root_cause_done = 0
|
||||
ca_required_count = 0
|
||||
ca_verified_effective = 0
|
||||
capa_close_days: list[float] = []
|
||||
overdue_capa = 0
|
||||
|
||||
for n in ncrs:
|
||||
by_dept[dept_names.get(n.department_id, "?")] += 1
|
||||
by_cat[cat_names.get(n.deviation_category_id, "?")] += 1
|
||||
by_month[n.created_at.strftime("%Y-%m")] += 1
|
||||
job_counts[n.job_number] += 1
|
||||
if (n.root_cause or "").strip():
|
||||
root_cause_done += 1
|
||||
if n.root_cause_category_id is not None:
|
||||
by_rcc[rcc_names.get(n.root_cause_category_id, "?")] += 1
|
||||
if n.corrective_action_required:
|
||||
ca_required_count += 1
|
||||
effective = n.effectiveness_result == "effective"
|
||||
if effective:
|
||||
ca_verified_effective += 1
|
||||
if n.corrective_action_opened_at and n.effectiveness_verified_at:
|
||||
capa_close_days.append(
|
||||
(
|
||||
n.effectiveness_verified_at - n.corrective_action_opened_at
|
||||
).total_seconds()
|
||||
/ 86400
|
||||
)
|
||||
elif n.corrective_action_due_date and n.corrective_action_due_date < today:
|
||||
overdue_capa += 1
|
||||
if n.stage == Stage.CLOSED.value:
|
||||
closed_count += 1
|
||||
month = (n.closed_at or n.created_at).strftime("%Y-%m")
|
||||
@@ -152,6 +183,24 @@ async def reports_summary(
|
||||
open_ncrs=open_count,
|
||||
closed_ncrs=closed_count,
|
||||
total_cost=total_cost,
|
||||
root_cause_pct=(
|
||||
round(100 * root_cause_done / len(ncrs), 1) if ncrs else None
|
||||
),
|
||||
effectiveness_verified_pct=(
|
||||
round(100 * ca_verified_effective / ca_required_count, 1)
|
||||
if ca_required_count
|
||||
else None
|
||||
),
|
||||
avg_capa_close_days=(
|
||||
round(sum(capa_close_days) / len(capa_close_days), 2)
|
||||
if capa_close_days
|
||||
else None
|
||||
),
|
||||
overdue_capa_count=overdue_capa,
|
||||
by_root_cause_category=sorted(
|
||||
(CountByName(name=k, count=v) for k, v in by_rcc.items()),
|
||||
key=lambda x: -x.count,
|
||||
),
|
||||
by_department=sorted(
|
||||
(CountByName(name=k, count=v) for k, v in by_dept.items()),
|
||||
key=lambda x: -x.count,
|
||||
|
||||
Reference in New Issue
Block a user