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:
@@ -23,7 +23,9 @@ from app.models import (
|
||||
Department,
|
||||
DeviationCategory,
|
||||
Ncr,
|
||||
NcrLink,
|
||||
NcrSecondaryAssignee,
|
||||
RootCauseCategory,
|
||||
StageTransition,
|
||||
User,
|
||||
UserRole,
|
||||
@@ -45,6 +47,19 @@ CATEGORIES = [
|
||||
"Process Deviation", "Supplier Nonconformance", "Damage / Handling", "Other",
|
||||
]
|
||||
|
||||
# Standard 6M root-cause categories (API Q1 §6.4.2 trend reporting).
|
||||
ROOT_CAUSE_CATEGORIES = [
|
||||
"Man", "Machine", "Method", "Material", "Measurement", "Environment",
|
||||
]
|
||||
|
||||
ROOT_CAUSES = [
|
||||
"Operator used a superseded revision of the work instruction.",
|
||||
"Fixture clamping force drifted out of spec; no PM interval defined.",
|
||||
"Incoming material certified to the wrong specification revision.",
|
||||
"Measurement performed with a gauge past its calibration due date.",
|
||||
"Setup sheet did not call out the datum change from the ECN.",
|
||||
]
|
||||
|
||||
DEV_USERS = [
|
||||
("admin@pescoinc.biz", "Dev Admin", list(r.value for r in Role)),
|
||||
("dispo@pescoinc.biz", "Dana Disposition", [Role.REQUESTER.value, Role.DISPOSITION_AUTHORITY.value]),
|
||||
@@ -85,6 +100,13 @@ async def seed() -> None:
|
||||
for name in CATEGORIES:
|
||||
if name not in existing:
|
||||
db.add(DeviationCategory(name=name, is_active=True))
|
||||
existing = {
|
||||
c.name
|
||||
for c in (await db.execute(select(RootCauseCategory))).scalars().all()
|
||||
}
|
||||
for name in ROOT_CAUSE_CATEGORIES:
|
||||
if name not in existing:
|
||||
db.add(RootCauseCategory(name=name, is_active=True))
|
||||
|
||||
if await db.get(AppSetting, NOTIFICATIONS_ENABLED_KEY) is None:
|
||||
db.add(
|
||||
@@ -124,7 +146,9 @@ async def seed() -> None:
|
||||
|
||||
departments = (await db.execute(select(Department))).scalars().all()
|
||||
categories = (await db.execute(select(DeviationCategory))).scalars().all()
|
||||
root_causes = (await db.execute(select(RootCauseCategory))).scalars().all()
|
||||
rng = random.Random(42)
|
||||
seeded_ncrs: list[Ncr] = []
|
||||
|
||||
dispo = users["dispo@pescoinc.biz"]
|
||||
second = users["second@pescoinc.biz"]
|
||||
@@ -144,7 +168,7 @@ async def seed() -> None:
|
||||
]
|
||||
|
||||
for target, count in plan:
|
||||
for _ in range(count):
|
||||
for i in range(count):
|
||||
days_ago = rng.randint(5, 120)
|
||||
created = utcnow() - timedelta(days=days_ago)
|
||||
number, year, seq = await allocate_ncr_number(db, now=created)
|
||||
@@ -166,6 +190,7 @@ async def seed() -> None:
|
||||
)
|
||||
db.add(ncr)
|
||||
await db.flush()
|
||||
seeded_ncrs.append(ncr)
|
||||
|
||||
t = created
|
||||
db.add(StageTransition(
|
||||
@@ -222,6 +247,38 @@ async def seed() -> None:
|
||||
ncr.qc_closed_by_id = qc.id
|
||||
advance(Stage.COSTING, "qc_close", qc)
|
||||
ncr.qc_closed_at = t
|
||||
|
||||
# CAPA — the API Q1 closure gate requires the CA question to be
|
||||
# answered (and any required action verified) before costing
|
||||
# can close the NCR, so seed it here.
|
||||
ncr.root_cause = rng.choice(ROOT_CAUSES)
|
||||
ncr.root_cause_category_id = rng.choice(root_causes).id
|
||||
# Alternate yes/no so every dashboard metric has data.
|
||||
if i % 2 == 0 or target == Stage.COSTING:
|
||||
ncr.corrective_action_required = True
|
||||
ncr.corrective_action_justification = (
|
||||
"Recurrence risk without a process control update."
|
||||
)
|
||||
ncr.corrective_action_plan = (
|
||||
"Update the work instruction and add an in-process check "
|
||||
"at the affected operation."
|
||||
)
|
||||
ncr.corrective_action_owner_id = ops.id
|
||||
ncr.corrective_action_opened_at = t
|
||||
ncr.corrective_action_due_date = (t + timedelta(days=14)).date()
|
||||
if target == Stage.CLOSED:
|
||||
ncr.effectiveness_result = "effective"
|
||||
ncr.effectiveness_notes = (
|
||||
"No recurrence observed over three subsequent runs."
|
||||
)
|
||||
ncr.effectiveness_verified_by_id = qc.id
|
||||
ncr.effectiveness_verified_at = hop()
|
||||
else:
|
||||
ncr.corrective_action_required = False
|
||||
ncr.corrective_action_justification = (
|
||||
"Isolated incident fully contained by the disposition."
|
||||
)
|
||||
|
||||
if target == Stage.COSTING:
|
||||
continue
|
||||
|
||||
@@ -235,6 +292,15 @@ async def seed() -> None:
|
||||
ncr.costing_completed_at = t
|
||||
ncr.closed_at = t
|
||||
|
||||
# Recurring-issue demo: the newest open NCR references two closed ones.
|
||||
closed = [n for n in seeded_ncrs if n.stage == Stage.CLOSED.value]
|
||||
still_open = [n for n in seeded_ncrs if n.stage != Stage.CLOSED.value]
|
||||
if closed and still_open:
|
||||
repeat = still_open[0]
|
||||
repeat.is_recurring = True
|
||||
for prior in closed[:2]:
|
||||
db.add(NcrLink(ncr_id=repeat.id, related_ncr_id=prior.id))
|
||||
|
||||
await db.commit()
|
||||
log.info("Demo NCRs seeded. Done.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user