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>
66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""Printable NCR PDF (WeasyPrint) — a clean single-document rendering of the
|
|
complete NCR for hard-copy travelers and audits.
|
|
|
|
WeasyPrint is imported lazily so environments without the Pango/Cairo system
|
|
libraries (e.g. unit tests) can still import the app.
|
|
"""
|
|
from functools import partial
|
|
from pathlib import Path
|
|
|
|
import anyio
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
|
from app.domain import STAGE_LABELS, Stage
|
|
from app.models import Ncr
|
|
from app.models.base import utcnow
|
|
from app.services.storage import attachment_abs_path
|
|
|
|
_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
|
|
|
|
_env = Environment(
|
|
loader=FileSystemLoader(_TEMPLATES_DIR),
|
|
autoescape=select_autoescape(["html"]),
|
|
)
|
|
|
|
|
|
def _render_html(ncr: Ncr, related_ncr_numbers: list[str]) -> str:
|
|
images = []
|
|
other_files = []
|
|
for att in ncr.attachments:
|
|
entry = {
|
|
"filename": att.original_filename,
|
|
"uploaded_by": att.uploaded_by.display_name,
|
|
"uploaded_at": att.uploaded_at,
|
|
"size_kb": max(1, att.size_bytes // 1024),
|
|
}
|
|
path = attachment_abs_path(att.stored_path)
|
|
if att.is_image and path.is_file():
|
|
entry["src"] = path.as_uri()
|
|
images.append(entry)
|
|
else:
|
|
other_files.append(entry)
|
|
|
|
template = _env.get_template("ncr_pdf.html")
|
|
return template.render(
|
|
ncr=ncr,
|
|
stage_label=STAGE_LABELS[Stage(ncr.stage)],
|
|
stage_labels=STAGE_LABELS,
|
|
Stage=Stage,
|
|
images=images,
|
|
other_files=other_files,
|
|
related_ncr_numbers=related_ncr_numbers,
|
|
generated_at=utcnow(),
|
|
)
|
|
|
|
|
|
def _html_to_pdf(html: str) -> bytes:
|
|
from weasyprint import HTML # lazy: needs Pango/Cairo system libs
|
|
|
|
return HTML(string=html).write_pdf()
|
|
|
|
|
|
async def render_ncr_pdf(ncr: Ncr, related_ncr_numbers: list[str] | None = None) -> bytes:
|
|
html = _render_html(ncr, related_ncr_numbers or [])
|
|
# WeasyPrint rendering is CPU-bound; keep it off the event loop.
|
|
return await anyio.to_thread.run_sync(partial(_html_to_pdf, html))
|