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:
@@ -1,9 +1,10 @@
|
||||
from app.models.base import Base
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.lookups import Department, DeviationCategory
|
||||
from app.models.lookups import Department, DeviationCategory, RootCauseCategory
|
||||
from app.models.ncr import (
|
||||
JobInfo,
|
||||
Ncr,
|
||||
NcrLink,
|
||||
NcrSecondaryAssignee,
|
||||
NcrSequence,
|
||||
StageTransition,
|
||||
@@ -18,7 +19,9 @@ __all__ = [
|
||||
"UserRole",
|
||||
"Department",
|
||||
"DeviationCategory",
|
||||
"RootCauseCategory",
|
||||
"Ncr",
|
||||
"NcrLink",
|
||||
"NcrSequence",
|
||||
"NcrSecondaryAssignee",
|
||||
"StageTransition",
|
||||
|
||||
@@ -20,3 +20,15 @@ class DeviationCategory(Base):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
|
||||
class RootCauseCategory(Base):
|
||||
"""Root-cause classification for CAPA trend reporting (API Q1 §6.4.2).
|
||||
Seeded with the standard 6M categories; admin-extensible like the other
|
||||
lookups."""
|
||||
|
||||
__tablename__ = "root_cause_categories"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
@@ -77,6 +78,36 @@ class Ncr(Base):
|
||||
qc_closed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
qc_closed_by_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# ── Corrective & Preventive Action (API Q1 §5.9.1.2 / §6.4.2) ────────────
|
||||
# Editable in any non-closed stage via POST /ncrs/{ref}/capa; the costing
|
||||
# action refuses to close the NCR until the CA question is answered and,
|
||||
# when corrective action is required, verified effective.
|
||||
root_cause: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
root_cause_category_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("root_cause_categories.id"), nullable=True
|
||||
)
|
||||
corrective_action_required: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
||||
corrective_action_justification: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
corrective_action_plan: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
corrective_action_owner_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
corrective_action_due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
# Stamped when the CA question is first answered "yes"; feeds the average
|
||||
# CAPA close-time metric (opened → verified effective).
|
||||
corrective_action_opened_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
)
|
||||
effectiveness_result: Mapped[str | None] = mapped_column(
|
||||
String(20), nullable=True
|
||||
) # effective | not_effective
|
||||
effectiveness_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
effectiveness_verified_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
effectiveness_verified_by_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
is_recurring: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# ── Costing ──────────────────────────────────────────────────────────────
|
||||
labor_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
|
||||
material_cost: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
|
||||
@@ -94,6 +125,13 @@ class Ncr(Base):
|
||||
# ── Relationships ────────────────────────────────────────────────────────
|
||||
department = relationship("Department", lazy="selectin")
|
||||
deviation_category = relationship("DeviationCategory", lazy="selectin")
|
||||
root_cause_category = relationship("RootCauseCategory", lazy="selectin")
|
||||
corrective_action_owner: Mapped[User | None] = relationship(
|
||||
foreign_keys=[corrective_action_owner_id], lazy="selectin"
|
||||
)
|
||||
effectiveness_verified_by: Mapped[User | None] = relationship(
|
||||
foreign_keys=[effectiveness_verified_by_id], lazy="selectin"
|
||||
)
|
||||
requester: Mapped[User] = relationship(foreign_keys=[requester_id], lazy="selectin")
|
||||
disposition_authority: Mapped[User] = relationship(
|
||||
foreign_keys=[disposition_authority_id], lazy="selectin"
|
||||
@@ -152,6 +190,21 @@ class NcrSecondaryAssignee(Base):
|
||||
user: Mapped[User] = relationship(lazy="selectin")
|
||||
|
||||
|
||||
class NcrLink(Base):
|
||||
"""Directed link from an NCR to a prior similar NCR (recurring-issue
|
||||
tracking). Intentionally relationship-free: the API loads the light
|
||||
{id, ncr_number, ...} projections it needs with explicit queries."""
|
||||
|
||||
__tablename__ = "ncr_links"
|
||||
|
||||
ncr_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("ncrs.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
related_ncr_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("ncrs.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
|
||||
|
||||
class StageTransition(Base):
|
||||
"""One row per lifecycle event (create, stage change, reopen) — the basis
|
||||
for aging and cycle-time reporting."""
|
||||
|
||||
Reference in New Issue
Block a user