# -*- coding: utf-8 -*-
import re, os
from docx import Document
from docx.shared import Pt, Cm, RGBColor, Mm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.enum.section import WD_SECTION
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

LOGO = '/Users/liuxinyuan/work/公司资料/logo_transparent.png'
OUT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/经营分析/质量复核/MD2104-003006-DB_成品功能性检验记录复核报告_20261106_V01.docx'
BLUE = '1F4E79'
DARK = '333333'
GREY = '667085'
LIGHT = 'F2F6FA'
RED = 'C00000'
RED_FILL = 'FCE4E4'

doc = Document()
sec = doc.sections[0]
sec.page_width = Mm(210); sec.page_height = Mm(297)
sec.top_margin = Mm(21); sec.bottom_margin = Mm(20)
sec.left_margin = Mm(22); sec.right_margin = Mm(22)
sec.header_distance = Mm(10); sec.footer_distance = Mm(9)
sec.different_first_page_header_footer = True

# ---------- helpers ----------
def set_run(run, size=10.5, bold=False, color=DARK, italic=False, font_cn='宋体', font_en='Times New Roman'):
    run.font.name = font_en
    run._element.get_or_add_rPr().rFonts.set(qn('w:eastAsia'), font_cn)
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.italic = italic
    run.font.color.rgb = RGBColor.from_string(color)

def add_rich(par, text, size=10.5, bold=False, color=DARK, italic=False):
    # color placeholders red, keep other text normal
    parts = re.split(r'(【待补：[^】]*】)', text)
    for part in parts:
        if not part: continue
        r = par.add_run(part)
        if part.startswith('【待补：'):
            set_run(r, size=size, bold=True, color=RED, italic=italic)
        else:
            set_run(r, size=size, bold=bold, color=color, italic=italic)

def p(text='', size=10.5, bold=False, color=DARK, align=WD_ALIGN_PARAGRAPH.JUSTIFY,
      indent=True, before=0, after=4, line=1.4):
    par = doc.add_paragraph()
    pf = par.paragraph_format
    pf.alignment = align
    pf.space_before = Pt(before)
    pf.space_after = Pt(after)
    pf.line_spacing = line
    if indent: pf.first_line_indent = Pt(size*2)
    add_rich(par, text, size=size, bold=bold, color=color)
    return par

def bullet(text, size=10.5):
    par = doc.add_paragraph(style='List Bullet')
    par.paragraph_format.space_before = Pt(0)
    par.paragraph_format.space_after = Pt(3)
    par.paragraph_format.line_spacing = 1.35
    add_rich(par, text, size=size)

def h(text, level=1):
    par = doc.add_paragraph(style='Heading 1' if level==1 else 'Heading 2')
    pf = par.paragraph_format
    pf.space_before = Pt(14 if level==1 else 9)
    pf.space_after = Pt(6 if level==1 else 4)
    pf.keep_with_next = True
    size = 15 if level==1 else 12
    add_rich(par, text, size=size, bold=True, color=BLUE)
    # outline level
    pPr = par._p.get_or_add_pPr()
    outline = OxmlElement('w:outlineLvl'); outline.set(qn('w:val'), '0' if level==1 else '1')
    pPr.append(outline)

def caption(text):
    par = doc.add_paragraph()
    par.paragraph_format.space_before = Pt(4)
    par.paragraph_format.space_after = Pt(2)
    par.paragraph_format.keep_with_next = True
    add_rich(par, text, size=9, bold=True, color=GREY)

def shade(cell, hexcolor):
    tcPr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement('w:shd'); shd.set(qn('w:fill'), hexcolor)
    tcPr.append(shd)

def cell_margins(table, top=40, bottom=40, left=70, right=70):
    tblPr = table._tbl.tblPr
    mar = OxmlElement('w:tblCellMar')
    for tag,val in [('w:top',top),('w:bottom',bottom),('w:left',left),('w:right',right)]:
        el = OxmlElement(tag); el.set(qn('w:w'), str(val)); el.set(qn('w:type'),'dxa'); mar.append(el)
    tblPr.append(mar)

def borders(table, color='D9E2F3', sz='4'):
    tblPr = table._tbl.tblPr
    b = OxmlElement('w:tblBorders')
    for edge in ['top','left','bottom','right','insideH','insideV']:
        el = OxmlElement('w:'+edge); el.set(qn('w:val'),'single'); el.set(qn('w:sz'),sz); el.set(qn('w:space'),'0'); el.set(qn('w:color'),color); b.append(el)
    tblPr.append(b)

def repeat_header(row):
    trPr = row._tr.get_or_add_trPr()
    hdr = OxmlElement('w:tblHeader'); hdr.set(qn('w:val'),'true'); trPr.append(hdr)

def cant_split(row):
    trPr = row._tr.get_or_add_trPr()
    el = OxmlElement('w:cantSplit'); el.set(qn('w:val'),'true'); trPr.append(el)

def set_cell(cell, text, size=8.5, bold=False, color=DARK, align=WD_ALIGN_PARAGRAPH.LEFT, fill=None, red=False):
    cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
    if fill: shade(cell, fill)
    par = cell.paragraphs[0]
    par.alignment = align
    par.paragraph_format.space_before = Pt(0)
    par.paragraph_format.space_after = Pt(0)
    par.paragraph_format.line_spacing = 1.05
    # clear
    for r in list(par.runs): r._element.getparent().remove(r._element)
    if red:
        add_rich(par, text, size=size, bold=True, color=RED)
    else:
        add_rich(par, text, size=size, bold=bold, color=color)

def add_table(headers, rows, widths, font_size=8.5, header_fill=BLUE, zebra=True,
              center_cols=None, red_cells=None, caption_text=None):
    if caption_text: caption(caption_text)
    table = doc.add_table(rows=1, cols=len(headers))
    table.style = 'Table Grid'
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    table.width = Cm(sum(widths))
    borders(table)
    cell_margins(table)
    center_cols = set(center_cols or [])
    red_cells = set(red_cells or [])
    hdr = table.rows[0]
    repeat_header(hdr); cant_split(hdr)
    for i, htext in enumerate(headers):
        c = hdr.cells[i]
        c.width = Cm(widths[i])
        set_cell(c, htext, size=font_size, bold=True, color='FFFFFF', fill=header_fill, align=WD_ALIGN_PARAGRAPH.CENTER)
    for ri, row in enumerate(rows, start=1):
        tr = table.add_row(); cant_split(tr)
        for ci, val in enumerate(row):
            c = tr.cells[ci]
            c.width = Cm(widths[ci])
            is_red = (ri-1, ci) in red_cells
            fill = LIGHT if (zebra and ri % 2 == 0 and not is_red) else ('FCE4E4' if is_red else None)
            align = WD_ALIGN_PARAGRAPH.CENTER if ci in center_cols else WD_ALIGN_PARAGRAPH.LEFT
            set_cell(c, str(val), size=font_size, align=align, fill=fill, red=is_red)
    return table

def page_break():
    doc.add_page_break()

def add_field(par, instr):
    run = par.add_run()
    fld1 = OxmlElement('w:fldChar'); fld1.set(qn('w:fldCharType'),'begin')
    it = OxmlElement('w:instrText'); it.set(qn('xml:space'),'preserve'); it.text = instr
    fld2 = OxmlElement('w:fldChar'); fld2.set(qn('w:fldCharType'),'separate')
    t = OxmlElement('w:t'); t.text = '1'
    fld3 = OxmlElement('w:fldChar'); fld3.set(qn('w:fldCharType'),'end')
    run._r.append(fld1); run._r.append(it); run._r.append(fld2); run._r.append(t); run._r.append(fld3)
    return run

def header_footer():
    # default header
    hdr = sec.header
    hp = hdr.paragraphs[0]
    hp.text = ''
    hp.alignment = WD_ALIGN_PARAGRAPH.LEFT
    # table 2 cols
    t = hdr.add_table(rows=1, cols=2, width=Cm(16.6))
    t.autofit = False
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    # remove borders
    tblPr = t._tbl.tblPr
    b = OxmlElement('w:tblBorders')
    for edge in ['top','left','bottom','right','insideH','insideV']:
        el = OxmlElement('w:'+edge); el.set(qn('w:val'),'nil'); b.append(el)
    tblPr.append(b)
    t.columns[0].width = Cm(4.0); t.columns[1].width = Cm(12.6)
    c0,c1 = t.rows[0].cells
    c0.width = Cm(4.0); c1.width = Cm(12.6)
    p0 = c0.paragraphs[0]; p0.alignment = WD_ALIGN_PARAGRAPH.LEFT
    run = p0.add_run(); run.add_picture(LOGO, width=Cm(2.6))
    p1 = c1.paragraphs[0]; p1.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    add_rich(p1, 'BIORI BIOTECH · 宝锐生物', size=8.5, bold=True, color=BLUE)
    p1.add_run().add_break()
    add_rich(p1, '成品功能性检验记录复核报告｜MD2104-003006-DB', size=7.5, color=GREY)
    p1.add_run().add_break()
    add_rich(p1, '守护生命健康，缔造美好生活', size=7.5, color=GREY)
    # bottom border on header table
    tblPr = t._tbl.tblPr
    b2 = OxmlElement('w:tblBorders')
    for edge in ['bottom']:
        el = OxmlElement('w:'+edge); el.set(qn('w:val'),'single'); el.set(qn('w:sz'),'4'); el.set(qn('w:space'),'0'); el.set(qn('w:color'),'D8E2EE'); b2.append(el)
    tblPr.append(b2)

    # default footer
    ftr = sec.footer
    fp = ftr.paragraphs[0]
    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
    fp.paragraph_format.space_before = Pt(0); fp.paragraph_format.space_after = Pt(0)
    add_rich(fp, '守护生命健康，缔造美好生活', size=8.5, bold=True, color=BLUE)
    fp.add_run().add_break()
    add_rich(fp, 'BIORI BIOTECH · 宝锐生物｜内部技术参考｜第 ', size=7.5, color=GREY)
    r1=add_field(fp,' PAGE '); set_run(r1, size=7.5, color=GREY)
    add_rich(fp, ' 页 / 共 ', size=7.5, color=GREY)
    r2=add_field(fp,' NUMPAGES '); set_run(r2, size=7.5, color=GREY)
    add_rich(fp, ' 页', size=7.5, color=GREY)

    # first page header/footer
    sec.first_page_header.paragraphs[0].text = ''
    ff = sec.first_page_footer.paragraphs[0]
    ff.alignment = WD_ALIGN_PARAGRAPH.CENTER
    add_rich(ff, '守护生命健康，缔造美好生活 ｜ BIORI BIOTECH · 宝锐生物', size=8.5, bold=True, color=BLUE)

# ---------- build ----------
header_footer()

# COVER
p('', indent=False, after=10)
pic = doc.add_paragraph(); pic.alignment = WD_ALIGN_PARAGRAPH.LEFT
pic.add_run().add_picture(LOGO, width=Cm(4.2))
p('', indent=False, after=34)
p('成品功能性检验记录复核报告', size=25, bold=True, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False, after=8)
p('MD2104-003006-DB', size=14, bold=True, color=DARK, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False, after=2)
p('2×SensiDirect RT Premix-UNG (Probe qRT-PCR) (003006)', size=11, color=GREY, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False, after=24)

cover_rows = [
 ('待检批号','20261103'),
 ('对照批号','20261014（25× Mix）/ 20261012（2× Buffer）'),
 ('检验日期','2026-11-04'),
 ('复核日期','2026-11-06'),
 ('报告版本','V01（工作稿）'),
 ('报告性质','内部技术复核 · 不替代 QA 批放行结论'),
]
t = doc.add_table(rows=0, cols=2); t.alignment=WD_TABLE_ALIGNMENT.CENTER; t.autofit=False
borders(t); cell_margins(t)
for k,v in cover_rows:
 tr=t.add_row(); cant_split(tr)
 tr.cells[0].width=Cm(4.0); tr.cells[1].width=Cm(10.0)
 set_cell(tr.cells[0], k, size=9.5, bold=True, color=BLUE, fill='EAF1FA')
 set_cell(tr.cells[1], v, size=9.5)
p('', indent=False, after=30)
# conclusion box
t2 = doc.add_table(rows=1, cols=1); t2.alignment=WD_TABLE_ALIGNMENT.CENTER
borders(t2, color='D8E2EE'); cell_margins(t2, 100,100,120,120)
set_cell(t2.rows[0].cells[0], '复核结论：方案完整性不完整；数据可靠性中低；两批次中高浓度有效通道未见系统性偏移，低浓度/内标数据不足，暂不能判定批放行。', size=10, bold=True, color=RED, fill='FDF2F2')
p('', indent=False, after=26)
p('守护生命健康，缔造美好生活', size=10, bold=True, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False, after=4)
p('珠海宝锐生物科技有限公司 ｜ 营销中心销售管理', size=10, bold=True, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False)
p('2026 年 11 月', size=9.5, color=GREY, align=WD_ALIGN_PARAGRAPH.CENTER, indent=False)
page_break()

# Document control + summary
h('文档信息与复核结论', 1)
info_rows = [
 ('报告编号','【待补：报告编号】'),
 ('报告版本','V01（工作稿）'),
 ('复核对象','MD2104-003006-DB 成品功能性项目检验记录'),
 ('原记录文件','MD2104-003006-DB成品功能性项目检验记录-20261106.doc'),
 ('待检批号','20261103'),
 ('对照批号','20261014（25× SensiDirect RTase/UNG Mix）/ 20261012（2× SensiDirect RT Premix Buffer）'),
 ('检验日期','2026-11-04'),
 ('复核日期','2026-11-06'),
 ('复核人','【待补：姓名/岗位】'),
 ('审核人','【待补：姓名/岗位】'),
 ('批准人','【待补：姓名/岗位】'),
 ('报告用途','内部技术复核；不替代 QA 批放行、注册申报或客户审核结论'),
]
add_table(['项目','内容'], info_rows, [3.6,12.8], font_size=9, zebra=False, caption_text='表 0-1 报告信息')
p('填写说明：原记录已有数据已填入；缺失信息统一用【待补：…】标记，不推测、不补造。', size=9, color=GREY)
p('', indent=False, after=2)
h('复核结论摘要', 2)
bullet('方案完整性：不完整。缺少检验依据、样品浓度/单位、RNA 质量、全部试剂效期、NTC/阳性/UNG/no-RT 对照、重复数与统计、原始数据和签字。')
bullet('数据可靠性：中低。13/16 个 CT 组合有效；第 4 通道 3 个浓度样品 CT 缺失或列错位；有效 ΔCT 与 Rn 重算一致，但原始数据、run ID、阈值和签字缺失。')
bullet('两批次差异：中高浓度有效通道未见方向一致的系统性偏移；低浓度有单通道偏移，内标通道数据不足，不能写“无差异”。')
bullet('放行建议：暂不能判定批放行。先补第 4 通道数据、对照、记录头和原始数据，由 QA/QC 复核后再定。')
p('本报告只基于所给 Word 记录及其汇总表；未核对仪器原始导出、实验室现场和批生产记录。报告中的“不完整/中低/不能判定”是复核判断，不是对产品安全有效性的最终结论。', size=9.5, color=GREY)

# 1 scope/basis
h('1. 复核范围与依据', 1)
h('1.1 复核范围', 2)
p('本次复核对象是一份单批成品功能性检验记录，检验目的为“用荧光定量 PCR 法检验成品的扩增功能性”，覆盖批间差与灵敏度两个项目。复核重点是：检验方案是否完整、数据是否可靠、对照批与待检批之间是否存在明显差异。')
p('本报告不把这张表当作完整的核酸试剂盒质量标准，也不要求它覆盖全部注册分析性能项目；但凡记录声称“符合规定”，必须有可追溯的检验依据、对照、原始数据和统计。完整分析性能（精密度、最低检出限、准确度、分析特异性、稳定性等）应由相应研究/检验报告另行覆盖。')
h('1.2 复核依据', 2)
basis_rows = [
 ('《体外诊断试剂注册与备案管理办法》','市场监管总局令第 48 号','第二十七、二十九、三十条：产品技术要求含可客观判定的功能性/安全性指标和检测方法；研究样品应有代表性，必要时做方法学验证/统计分析。'),
 ('《医疗器械产品技术要求编写指导原则》','国家药监局 2022 年第 8 号','产品技术要求应明确性能指标和检验方法；本记录应能追溯到对应产品技术要求/SOP。'),
 ('《定性检测体外诊断试剂分析性能评估注册审查指导原则》','器审中心 2022 年第 36 号','分析性能包括准确度、精密度、检出限、分析特异性等；精密度含重复性、中间精密度、再现性、批间；至少三个浓度水平；LoD 应多批、多样本、多天。'),
 ('《医疗器械生产质量管理规范附录体外诊断试剂》','2015 年第 103 号','检验记录应可追溯；批记录/检验记录应完整；PCR 生产与检验应有独立空间/防污染要求。'),
]
add_table(['依据','文号','与本报告相关的要点'], basis_rows, [5.0,3.6,8.0], font_size=8.5, caption_text='表 1-1 复核依据')
p('上述指导原则不是强制法规，但可作为质量标准和审评尺度；企业内控可以更严。具体判定仍以公司产品技术要求、SOP 和注册资料为准。', size=9, color=GREY)
h('1.3 复核方法', 2)
p('用 textutil 提取旧 .doc 文本；按 ASCII BEL（chr(7)）和 U+2028 重建表格单元格；逐项重算 ΔCT 与 Rn 差；对有效 CT 做 log10 浓度内部一致性检查；对缺失值、重复数和检出率分母做完整性核对。没有原始仪器文件，因此不能做数据真实性确认。')

# 2 record overview
h('2. 受检记录概况', 1)
h('2.1 产品与批次', 2)
prod_rows = [
 ('产品代号','MD2104-003006-DB'),
 ('产品名称','2×SensiDirect RT Premix-UNG (Probe qRT-PCR) (003006)'),
 ('待检批号','20261103'),
 ('对照批号','20261014（25× SensiDirect RTase/UNG Mix）；20261012（2× SensiDirect RT Premix Buffer）'),
 ('检验目的','用荧光定量 PCR 法检验成品的扩增功能性'),
 ('检验项目','批间差、灵敏度'),
 ('检验日期','2026-11-04'),
 ('记录日期','2026-11-06'),
 ('记录状态','Word 汇总记录；操作人/复核人栏空白；结论段有“符合规定”文字，无勾选/签字'),
]
add_table(['项目','内容'], prod_rows, [3.6,12.8], font_size=9, zebra=False, caption_text='表 2-1 产品与批次')
p('严格说，文件里只有 1 个待检成品批（20261103）和 2 个对照组分批（20261014/20261012），不是两批成品。下文“两批次”均指“对照批 vs 待检批”。', size=9, color=GREY)

h('2.2 仪器设备', 2)
inst_rows = [
 ('水平离心机','L500','BR-B03-202205001','湘仪','【待补：校准有效期】'),
 ('高速离心机','Min1412','BR-B03-201708001','珠海黑马','【待补：校准有效期】'),
 ('荧光定量扩增仪','SLAN®-96P','BR-B03-202008002','上海宏石','【待补：校准有效期】'),
 ('低速离心机','6K','BR-B03-202008019L','珠海黑马','【待补：校准有效期】'),
 ('混匀离心双功能机','ExiSpin TM','BR-B03-201707003L','Bioneer','【待补：校准有效期】'),
 ('生物安全柜','BSC-1500IIB2-X','BR-B03-202209007','BIOBASE','【待补：校准有效期】'),
]
add_table(['仪器','型号','设备编码','厂家','校准状态'], inst_rows, [3.0,2.2,4.2,2.8,4.4], font_size=8.5, caption_text='表 2-2 仪器设备')
p('文件只勾选了设备编码，没有记录校准/检定有效期、运行日期/时间、仪器维护状态和 PCR 实验室区域。', size=9, color=GREY)

h('2.3 样品与试剂', 2)
sample_rows = [
 ('25×SensiDirect RTase/UNG Mix (003006)','20261014','20261103','【待补：浓度/效期/储存】'),
 ('2×SensiDirect RT Premix Buffer (dUTP) (003001)','20261012','20261103','【待补：浓度/效期/储存】'),
 ('qRT四重 pbmix','20260310','—','【待补：浓度/效期/是否属于产品组分】'),
 ('cRNA','181025362','—','【待补：浓度/单位/效期】'),
 ('浓人总 10⁻²','20260310','—','【待补：浓度/单位/效期/RNA 完整性】'),
]
add_table(['组分','对照批号','待检批号','待补信息'], sample_rows, [5.8,3.0,3.0,4.6], font_size=8.5, caption_text='表 2-3 样品与主要试剂')
p('记录没有写 cRNA/人总 RNA 的 stock 浓度和每反应输入拷贝数；稀释表以“IU/ml”描述，单位不适用于核酸模板，无法换算实际输入量。', size=9, color=GREY)

h('2.4 反应体系与程序', 2)
mix_rows = [
 ('25×SensiDirect RTase/UNG Mix (003006)','1','13'),
 ('2×SensiDirect RT Premix Buffer (dUTP) (003001)','12.5','162.5'),
 ('H2O','5.5','71.5'),
 ('qRT四重 pbmix','1','13'),
 ('反应液合计','20','260'),
 ('模板','5','65'),
 ('终体积','25','325'),
]
add_table(['组分','V/T（μl）','13T（μl）'], mix_rows, [9.0,3.4,4.0], font_size=8.5, center_cols=[1,2], caption_text='表 2-4 反应体系')
p('反应程序：50 ℃ 4 min；95 ℃ 30 s；95 ℃ 1 s，60 ℃ 10 s，41 cycles。文件中未写荧光采集通道映射、阈值/基线设置和 run ID。', size=9, color=GREY)

h('2.5 稀释与加样设计', 2)
dil_rows = [
 ('A','10⁻²','10⁻³','5','45','/'),
 ('B','10⁻³','10⁻⁴','5','45','2'),
 ('C','10⁻⁴','10⁻⁵','20','180','2'),
 ('D','10⁻⁵','10⁻⁶','20','180','4'),
 ('E','10⁻⁶','10⁻⁷','20','180','4'),
]
add_table(['管号','母液浓度','稀释后浓度','母液加量（μl）','稀释液加量（μl）','每组反应孔数'], dil_rows, [1.6,3.0,3.4,3.4,3.4,1.8], font_size=8.5, center_cols=[0,1,2,3,4,5], caption_text='表 2-5 模板稀释')
p('方法写“待检品及对照品均有 12 个反应孔”：4 孔超低浓度、4 孔低浓度、2 孔中间浓度、2 孔高浓度。文件没有写 A 管用途、B–E 与 10⁻⁷–10⁻⁴ 的对应关系，也没有说明 12 孔如何汇总成表中的 16 个 CT 值。', size=9, color=GREY)

# 3 completeness
page_break()
h('3. 方案完整性评价', 1)
p('按“单批成品功能性放行记录”的尺度逐项核对，结论是：不完整。缺项不是文字润色问题，而是会让 QA/审核方无法判断“这批到底按什么标准、在什么条件下、由谁判定为合格”。')
comp_rows = [
 ('记录头','文件编号/版本/生效日期、检验依据（SOP/质量标准编号/版本）、抽样方案、检验日期','只有产品名/批号/日期；无文件编号、无检验依据','不完整'),
 ('样品','批号、浓度/单位、储存条件、RNA 完整性/纯度、提取/稀释方式、输入体积/拷贝数','只有批号；无浓度、无 RNA 质量、无输入量','不完整'),
 ('试剂/耗材','所有组分批号/效期、引物探针 mix、水、耗材、RNase-free 条件','只列部分组分；无有效期；pbmix 是否属于产品未说明','不完整'),
 ('对照','NTC、阳性对照、UNG 对照（含 U/不含 U）、no-RT、内标通道映射','只有对照批；无 NTC/阳性/UNG/no-RT；通道映射未写','关键缺失'),
 ('设计','浓度点、每点孔数、重复数、随机化、批内/批间/日间、统计方法','12 孔与表中 16 个 CT 值未对齐；无 n/SD/CV','不完整'),
 ('接受标准','中高浓度定义、组间定义、内标定义、低浓度检出率分母、Rn 判据','只有一句“符合规定”；术语和分母未定义','不完整'),
 ('数据','原始曲线/导出文件、run ID、阈值/基线、计算表、修约规则、异常/复测/偏差','只有汇总 CT/Rn 表；无原始数据、无异常/复测记录','不可追溯'),
 ('结论','勾选、操作/复核/批准签字、日期、批放行/不放行结论','签字空；勾选不明；无批准日期','未完成'),
 ('批次','两个成品批或 ≥3 批批间','1 个待检批 + 2 个对照组分批','不足'),
]
h('3.1 完整性矩阵', 2)
add_table(['维度','应有内容','本记录现状','判断'], comp_rows, [2.4,6.2,6.2,1.8], font_size=8.5, center_cols=[3], caption_text='表 3-1 方案完整性矩阵')

h('3.2 关键缺口说明', 2)
bullet('没有检验依据。记录没有引用产品技术要求、质量标准或 SOP 的编号/版本，无法确认“0.5”“70%”这些判据来自哪里，也无法确认是否是最新版本。')
bullet('没有污染与特异性对照。产品名含 UNG，但记录没有 UNG 功能对照（含 U 模板/不含 U 模板）、NTC、阳性对照和 no-RT 对照；UNG 防污染功能、RNA 依赖性和污染控制都没有被验证。')
bullet('样品与试剂信息不足。cRNA/人总 RNA 没有 stock 浓度、输入拷贝数、RNA 完整性/纯度；没有 DNase 处理；所有组分没有效期。')
bullet('设计无法复核。方法写 12 孔（4/4/2/2），表中却是 4 个浓度 × 4 通道 = 16 个 CT 值；没有写每格是单孔、均值还是中位数，也没有 n、SD、CV。')
bullet('判据不完整。“中高浓度”“组间”“内标模板”“低浓度检出率”都没有定义；Rn 差没有判据；4/4 的分母是孔数、通道数还是样本数也没有写。')
bullet('原始数据与签字缺失。没有仪器导出、扩增曲线、run ID、阈值/基线、计算表；操作人/复核人空白，不能作为批放行记录。')
p('结论：这份记录可以作为研发/功能性初筛的原始素材，但不能作为完整的批放行记录，更不能单独证明产品符合核酸试剂盒质量标准。', bold=True, color=BLUE)

# 4 data reliability
page_break()
h('4. 数据可靠性评价', 1)
p('先说明：以下“可靠/不可靠”只针对记录中的数据完整性和可复核性，不等于对产品性能的最终判定。')
h('4.1 算术复核', 2)
rel_rows = [
 ('有效 CT 组合','13/16；第 4 通道 10⁻⁶/10⁻⁵/10⁻⁴ 三个样品 CT 缺失或列错位','18.75% 的组合不可复核'),
 ('ΔCT 重算','13 个有效组合的“对照 CT − 样品 CT”与表内值最大差 0.01','在修约容差内'),
 ('Rn 差重算','16 个组合的 (样品−对照)/对照×100 与表内值最大差 0.047 个百分点','在修约容差内'),
 ('稀释线性（内部一致性）','对照批 ch1–ch4 效率 95.8%–104.1%，R²≥0.997；样品 ch1–ch3 效率 97.3%–104.4%，R²≥0.9996','有效通道未见明显扩增异常'),
 ('原始数据','无仪器导出、无曲线、无 run ID、无阈值/基线','不可追溯'),
 ('签字','操作人/复核人空白；结论段无勾选','不能批放行'),
]
add_table(['检查项','结果','判断'], rel_rows, [3.6,9.4,3.6], font_size=8.5, caption_text='表 4-1 数据可靠性检查')
p('计算层面没有发现系统性算错：有效 ΔCT 和全部 Rn 差都能在修约误差内复算。可靠性问题主要在缺失值、重复数、原始数据和签字。', size=9, color=GREY)

h('4.2 第 4 通道异常值', 2)
ab_rows = [
 ('10⁻⁶','33.20','0.20','0.20','33.00','不勾稽'),
 ('10⁻⁵','29.82','−0.05','−0.05','29.87','不勾稽'),
 ('10⁻⁴','26.46','−0.11','−0.11','26.57','不勾稽'),
]
add_table(['浓度','对照 CT','样品 CT（表内）','表内 ΔCT','按“对照−样品”重算','判断'], ab_rows, [1.8,2.6,3.2,2.6,4.2,2.2], font_size=8.5, center_cols=[0,1,2,3,4,5], red_cells={(0,2),(0,3),(1,2),(1,3),(2,2),(2,3)}, caption_text='表 4-2 第 4 通道异常值')
p('样品 CT 写成 0.20 / −0.05 / −0.11，和对照 CT、ΔCT 对不上；更像是样品 CT 单元格漏填或列错位，不是正常低值。若样品 CT 真是 0.20，ΔCT 应为 33.00；若 ΔCT 真是 0.20，样品 CT 应为 33.00。两种解释都说明表内数据不完整。', size=9, color=GREY)

h('4.3 检出率与置信区间', 2)
p('表中每个浓度/通道都写“4/4”。方法却写中间/高浓度各 2 孔；如果 4/4 指孔数，分母不对；如果指 4 个通道，文件没有定义。')
p('即使按 4/4 点估计为 100%，4 孔的 Clopper-Pearson 95% 置信下限约 39.8%；按 8/8 计算约 63.1%，都低于 70%。点估计通过，不能证明真实检出率≥70%。需要更多重复，或按 LoD 方案做多批、多样本、多天。')

h('4.4 原始性与可追溯性', 2)
p('文件是 Word 汇总表，不是仪器原始导出；没有 run ID、扩增曲线、阈值/基线、孔位图、异常孔/复测记录。记录可被直接编辑，且操作人/复核人/批准人栏空白，无法满足“可归因、可追溯、原始、准确”的记录要求。')

# 5 batch comparison
page_break()
h('5. 两批次差异分析', 1)
p('口径：对照批（20261014 酶 Mix / 20261012 Buffer）vs 待检批（20261103）。这不是两个成品批；如果要比较两个成品批，需要再提供另一批成品记录。')
p('ΔCT = 对照 CT − 样品 CT；正数表示样品更早。缺失写“缺失”，不按 0 处理。')
dct_rows = [
 ('10⁻⁴','+0.04','0.00','+0.02','缺失','中高浓度'),
 ('10⁻⁵','0.00','−0.09','+0.03','缺失','中高浓度'),
 ('10⁻⁶','−0.17','−0.20','−0.15','缺失','低浓度'),
 ('10⁻⁷','+0.77','−0.20','−0.34','−0.12','低浓度'),
]
add_table(['浓度','通道 1 ΔCT','通道 2 ΔCT','通道 3 ΔCT','通道 4 ΔCT','备注'], dct_rows, [2.0,2.8,2.8,2.8,2.8,3.2], font_size=9, center_cols=[0,1,2,3,4], red_cells={(0,4),(1,4),(2,4)}, caption_text='表 5-1 对照批 vs 待检批 ΔCT')
h('5.1 中高浓度', 2)
p('10⁻⁴ 和 10⁻⁵ 的通道 1–3 ΔCT 在 −0.09～+0.04 内；10⁻⁶ 在 −0.20～−0.15 内。有效通道没有方向一致的系统性偏移。')
h('5.2 低浓度', 2)
p('10⁻⁷ 通道 1 的 ΔCT 为 +0.77，超过 0.5；通道 2/3/4 分别为 −0.20/−0.34/−0.12。低浓度、高 CT 区单通道波动较大，不能据此判整批不合格，但应复测确认。')
h('5.3 Rn 差', 2)
p('10⁻⁶～10⁻⁴ 的 Rn 差为 +2.7%～+7.3%；10⁻⁷ 通道 4 为 −21.2%，其余通道为 −4.3%～+5.5%。文件没有 Rn 判据，不能判合格/不合格；−21.2% 应作为低浓度异常值复查。')
h('5.4 结论', 2)
p('中高浓度有效通道未见方向一致的系统性偏移；低浓度有单通道偏移；第 4 通道三个浓度缺失，内标无法评估。因此只能说“中高浓度未见明显系统性差异”，不能写“两批次无明显差异/完全一致”。没有 n、SD、CI 或等效性检验，也没有统计显著性结论。', bold=True, color=BLUE)

# 6 findings/actions
page_break()
h('6. 问题分级与整改建议', 1)
h('6.1 问题清单', 2)
prob_rows = [
 ('P0-01','严重','第 4 通道 10⁻⁶/10⁻⁵/10⁻⁴ 样品 CT 缺失或列错位','表 4-2；3/16 组合不勾稽','内标结论不可复核；可能掩盖真实批间差'),
 ('P0-02','严重','无 NTC/阳性/UNG/no-RT 对照','记录只有对照批；产品含 UNG','污染、gDNA、UNG 功能均未验证'),
 ('P0-03','严重','无检验依据/接受标准','记录无产品技术要求/SOP 编号/版本','无法判断“符合规定”依据'),
 ('P0-04','严重','无原始数据/run ID/阈值/签字','Word 汇总表；签字空','不可追溯，不能批放行'),
 ('P1-01','主要','样品/试剂信息不完整','无浓度、RNA 质量、效期、耗材','无法复现/排除试剂影响'),
 ('P1-02','主要','cRNA/人总 RNA 用 IU/ml','稀释表单位','无法换算输入拷贝数'),
 ('P1-03','主要','重复数/统计/检出率分母未定义','12 孔 vs 16 CT；4/4 vs 2 孔','无法判断重复性和 70% 检出率'),
 ('P1-04','主要','1 个待检批 vs 2 个对照组分批','批次表','不能代表成品批间差'),
 ('P2-01','一般','A 管用途、B–E 与浓度映射未写','稀释表','记录可读性/可复现性不足'),
 ('P2-02','一般','qRTt 拼写、记录格式/版本控制','反应体系表','记录规范性不足'),
]
add_table(['编号','级别','问题','证据','影响'], prob_rows, [1.4,1.3,5.0,4.8,4.1], font_size=8.3, center_cols=[0,1], caption_text='表 6-1 问题清单')

h('6.2 整改动作', 2)
act_rows = [
 ('P0','找回/补录第 4 通道 3 个样品 CT；核对列位；按原始导出重算','检验','【待补：责任人】','【待补：日期】','原始数据+重算表+QA 复核'),
 ('P0','补 NTC、阳性对照、UNG 对照（含 U/不含 U）、no-RT','研发/检验','【待补：责任人】','【待补：日期】','对照结果+判定'),
 ('P0','补检验依据、接受标准、记录头、操作/复核/批准签字','质量部','【待补：责任人】','【待补：日期】','完整记录'),
 ('P0','补原始曲线、run ID、阈值/基线、孔位图、异常/复测记录','检验','【待补：责任人】','【待补：日期】','原始数据附件'),
 ('P1','补样品浓度/单位、RNA 完整性/纯度、DNase/储存；补全部试剂效期','检验/研发','【待补：责任人】','【待补：日期】','样品/试剂表完整'),
 ('P1','重新设计统计：每浓度/通道 n、均值、SD、CV；低浓度增加重复；明确检出率分母','质量部/研发','【待补：责任人】','【待补：日期】','统计表+SOP'),
 ('P1','批间差至少 3 个成品批；每批每浓度足够重复','研发/质量','【待补：责任人】','【待补：日期】','3 批报告'),
 ('P1','统一浓度单位（copies/μl 或 ng/μl）和 A–E 稀释映射','研发','【待补：责任人】','【待补：日期】','SOP/记录 V02'),
 ('P2','修正 qRTt 等笔误；建立受控记录模板/电子记录','质量部','【待补：责任人】','【待补：日期】','模板 V02'),
 ('P2','QA 复核并给出批放行/不放行结论','QA','【待补：责任人】','【待补：日期】','签字结论'),
]
add_table(['优先级','动作','主责','责任人','时限','验收'], act_rows, [1.2,6.4,1.8,2.2,2.2,2.8], font_size=8.2, center_cols=[0,2], caption_text='表 6-2 整改动作')

h('6.3 放行建议', 2)
p('在 P0-01 至 P0-04 关闭前，不建议把该批签为“符合规定”或作为批放行依据。若第 4 通道是转录错误，按记录更改规程更正并重新审核；若原始数据不存在，应重测。不要直接在原 Word 上改数字，按记录更改规程划改、签名、注明原因。', bold=True, color=RED)

# 7 placeholders
page_break()
h('7. 待补数据与占位符清单', 1)
p('本报告统一用【待补：…】标记缺失信息；填写后请删除本表或把状态改为“已补”。')
h('7.1 占位符清单', 2)
ph_rows = [
 ('【待补：报告编号】','封面/文档信息','质量部','按公司文件编号规则'),
 ('【待补：SOP/质量标准编号及版本】','1.2/3.1','质量部','必须能追溯到产品技术要求'),
 ('【待补：抽样方案】','3.1','质量部','抽样数量/批次代表性'),
 ('【待补：样品浓度/单位/储存】','2.3/3.1','检验/研发','copies/μl 或 ng/μl；不要用 IU/ml'),
 ('【待补：RNA 完整性/纯度】','2.3/3.1','检验','RIN/OD260/280/280/230'),
 ('【待补：试剂效期/储存】','2.3','检验','所有组分含 pbmix/水/耗材'),
 ('【待补：NTC/阳性/UNG/no-RT 结果】','3.2/4','研发/检验','含 U/不含 U 模板'),
 ('【待补：内标通道映射】','2.4/5','研发','明确第 4 通道是否为内标'),
 ('【待补：每浓度孔数/重复数】','2.5/4.3','检验','12 孔如何汇总为 16 个 CT 值'),
 ('【待补：原始数据/run ID/阈值】','4.4','检验','仪器导出/曲线/孔位图'),
 ('【待补：操作人/复核人/批准人】','文档信息/6.3','质量部','签字+日期'),
 ('【待补：QA 放行结论】','6.3','QA','批放行/不放行'),
 ('【待补：第二成品批/第三批】','5/6.2','研发/质量','用于批间差，不是对照组分批'),
]
add_table(['占位符','位置','责任','填写说明'], ph_rows, [5.2,3.2,2.2,6.0], font_size=8.5, caption_text='表 7-1 占位符清单')

h('7.2 不能下的结论', 2)
bullet('不能判“两批次无明显差异/完全一致”：没有 n、SD、CI、等效性检验，且第 4 通道缺失。')
bullet('不能判“批放行合格”：记录无依据、无对照、无原始数据、无签字。')
bullet('不能判“UNG 功能有效”：没有 UNG 对照和 NTC。')
bullet('不能判“低浓度检出率≥70%”：4/4 的 95% 置信下限不足，分母也未定义。')
h('7.3 不确定的 3 个点', 2)
bullet('第 4 通道 0.20/−0.05/−0.11 是转录错误、列错位，还是原始数据缺失？现有文件无法判断。')
bullet('第 4 通道是否为内标？文件没有写通道映射；如果不是，内标根本没有测。')
bullet('4/4 的分母是孔数、通道数还是样本数？中间/高浓度只有 2 孔，与 4/4 矛盾。')

# Appendix A CT
page_break()
h('附录 A  CT 原始数据与重算', 1)
ct_rows = [
 ('10⁻⁷','1','35.72','34.95','0.77','0.77','一致'),
 ('10⁻⁷','2','35.57','35.78','−0.20','−0.21','一致（0.01）'),
 ('10⁻⁷','3','34.47','34.82','−0.34','−0.35','一致（0.01）'),
 ('10⁻⁷','4','36.62','36.75','−0.12','−0.13','一致（0.01）'),
 ('10⁻⁶','1','31.76','31.92','−0.17','−0.16','一致（0.01）'),
 ('10⁻⁶','2','32.02','32.22','−0.20','−0.20','一致'),
 ('10⁻⁶','3','31.32','31.48','−0.15','−0.16','一致（0.01）'),
 ('10⁻⁶','4','33.20','【缺失】','0.20','—','不勾稽'),
 ('10⁻⁵','1','28.61','28.61','0.00','0.00','一致'),
 ('10⁻⁵','2','28.84','28.93','−0.09','−0.09','一致'),
 ('10⁻⁵','3','28.03','27.99','0.03','0.04','一致（0.01）'),
 ('10⁻⁵','4','29.82','【缺失】','−0.05','—','不勾稽'),
 ('10⁻⁴','1','25.35','25.32','0.04','0.03','一致（0.01）'),
 ('10⁻⁴','2','25.57','25.58','0.00','−0.01','一致（0.01）'),
 ('10⁻⁴','3','24.81','24.80','0.02','0.01','一致（0.01）'),
 ('10⁻⁴','4','26.46','【缺失】','−0.11','—','不勾稽'),
]
add_table(['浓度','通道','对照 CT','样品 CT','表内 ΔCT','重算 ΔCT','判定'], ct_rows, [1.8,1.2,2.8,2.8,2.8,2.8,2.2], font_size=8.0, center_cols=[0,1,2,3,4,5,6], red_cells={(7,3),(7,4),(11,3),(11,4),(15,3),(15,4)}, caption_text='表 A-1 CT 数据；红色为缺失/不勾稽')

# Appendix B Rn
h('附录 B  Rn 原始数据与重算', 1)
rn_rows = [
 ('10⁻⁷','1','843.70','890.39','+5.5%','+5.53%','一致'),
 ('10⁻⁷','2','369.28','353.39','−4.3%','−4.29%','一致'),
 ('10⁻⁷','3','848.03','860.57','+1.5%','+1.48%','一致'),
 ('10⁻⁷','4','485.86','382.76','−21.2%','−21.22%','一致；低浓度异常'),
 ('10⁻⁶','1','982.67','1028.39','+4.7%','+4.65%','一致'),
 ('10⁻⁶','2','419.65','450.39','+7.3%','+7.32%','一致'),
 ('10⁻⁶','3','930.20','997.41','+7.2%','+7.22%','一致'),
 ('10⁻⁶','4','618.94','663.35','+7.2%','+7.18%','一致'),
 ('10⁻⁵','1','1065.69','1095.92','+2.8%','+2.84%','一致'),
 ('10⁻⁵','2','448.87','465.57','+3.7%','+3.72%','一致'),
 ('10⁻⁵','3','983.47','1023.66','+4.1%','+4.09%','一致'),
 ('10⁻⁵','4','679.66','698.04','+2.7%','+2.70%','一致'),
 ('10⁻⁴','1','1063.23','1115.09','+4.9%','+4.88%','一致'),
 ('10⁻⁴','2','446.93','469.92','+5.1%','+5.14%','一致'),
 ('10⁻⁴','3','991.33','1051.67','+6.1%','+6.09%','一致'),
 ('10⁻⁴','4','708.11','737.59','+4.2%','+4.16%','一致'),
]
add_table(['浓度','通道','对照 Rn','样品 Rn','表内 Rn 差','重算 Rn 差','判定'], rn_rows, [1.8,1.2,2.8,2.8,2.8,2.8,2.2], font_size=8.0, center_cols=[0,1,2,3,4,5,6], red_cells={(3,4)}, caption_text='表 B-1 Rn 数据；红色为低浓度异常值')

# Appendix C standards
h('附录 C  依据文件与说明', 1)
std_rows = [
 ('1','《体外诊断试剂注册与备案管理办法》','市场监管总局令第 48 号','第二十七、二十九、三十条；产品技术要求/非临床研究/统计分析'),
 ('2','《医疗器械产品技术要求编写指导原则》','国家药监局 2022 年第 8 号','性能指标和检验方法'),
 ('3','《定性检测体外诊断试剂分析性能评估注册审查指导原则》','器审中心 2022 年第 36 号','精密度/检出限/特异性；至少三个浓度水平；LoD 多批、多样本、多天'),
 ('4','《医疗器械生产质量管理规范附录体外诊断试剂》','2015 年第 103 号','记录可追溯、批记录/检验记录、PCR 分区/环境'),
]
add_table(['序号','文件','文号','与本报告相关的用途'], std_rows, [1.2,7.2,3.2,5.0], font_size=8.3, center_cols=[0], caption_text='表 C-1 依据文件')
p('说明：指导原则不是强制法规，但可作为质量标准和审评尺度；企业内控可以更严。具体判定以公司产品技术要求、SOP 和注册资料为准。', size=9, color=GREY)

page_break()
h('附录 D  报告签署', 1)
sign_rows = [
 ('复核人','【待补：姓名/岗位】','【待补：日期】'),
 ('审核人','【待补：姓名/岗位】','【待补：日期】'),
 ('批准人','【待补：姓名/岗位】','【待补：日期】'),
 ('QA 结论','【待补：批放行/不放行/待补充】','【待补：日期】'),
]
add_table(['角色','签字','日期'], sign_rows, [3.2,7.0,6.0], font_size=9.5, caption_text='表 D-1 签署')

# core props + update fields
doc.core_properties.title = 'MD2104-003006-DB 成品功能性检验记录复核报告'
doc.core_properties.subject = 'IVD 成品功能性检验记录复核'
doc.core_properties.author = '宝锐生物 营销中心销售管理'
doc.core_properties.comments = '内部技术复核，不替代 QA 批放行结论'
settings = doc.settings.element
uf = OxmlElement('w:updateFields'); uf.set(qn('w:val'),'true'); settings.append(uf)

doc.save(OUT)
print('wrote', OUT, os.path.getsize(OUT))
