# -*- coding: utf-8 -*-
"""西安佰奥莱博 近三年经营简报 -> Word（呈总经理）
数据源：数据/_bai_3y_data.json（ERP订单2024-01-01~2026-09-30 + 应收/客诉/商机/特殊要求）
"""
import json, os
from docx import Document
from docx.shared import Pt, Cm, RGBColor, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.section import WD_SECTION
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml, OxmlElement
from docx.oxml.ns import qn

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DATA = json.load(open(os.path.join(BASE, '数据/_bai_3y_data.json')))
LOGO = os.path.expanduser('~/work/公司资料/logo_transparent.png')
OUT = os.path.join(BASE, '内部管理/西安佰奥莱博_三年经营简报_20261021.docx')

BLUE = '1852B6'
DARK = '0D2A52'
LIGHT = 'E8EFFA'
GREY = '5F6368'
RED = 'C62828'
AMBER = 'B26A00'
SLOGAN = '守护生命健康，缔造美好生活'

y = DATA['yearly']
A24, A25, A26 = y['2024']['amount'], y['2025']['amount'], y['2026']['amount']
O24, O25, O26 = y['2024']['orders'], y['2025']['orders'], y['2026']['orders']
YTD25 = y['ytd_2025']['amount']
G = DATA['group']
G24, G25, G26 = G['2024']['amount'], G['2025']['amount'], G['2026']['amount']
T3 = DATA['total3y']
AR = DATA['ar']
OPPS = DATA['opps']
OPP_T = sum(o['target'] for o in OPPS)
GAP_FULL = OPP_T - A26
GAP_Q3 = OPP_T * 3 / 4 - A26

def w(x):   # 元 -> 万元字符串
    return '%.2f' % (x / 10000.0)

def pct(a, b):
    return '%.1f%%' % (a / b * 100.0)

# ---------- 基础工具 ----------
doc = Document()
sec = doc.sections[0]
sec.page_width, sec.page_height = Cm(21.0), Cm(29.7)
sec.top_margin, sec.bottom_margin = Cm(2.0), Cm(1.8)
sec.left_margin, sec.right_margin = Cm(2.0), Cm(2.0)
sec.header_distance, sec.footer_distance = Cm(1.0), Cm(0.9)

st = doc.styles['Normal']
st.font.name = '微软雅黑'
st.font.size = Pt(10.5)
_rf = st.element.rPr.rFonts
_rf.set(qn('w:ascii'), 'Microsoft YaHei')
_rf.set(qn('w:hAnsi'), 'Microsoft YaHei')
_rf.set(qn('w:eastAsia'), '微软雅黑')
st.paragraph_format.line_spacing = 1.45
st.paragraph_format.space_after = Pt(4)


def shade(cell, color):
    cell._tc.get_or_add_tcPr().append(parse_xml(r'<w:shd %s w:val="clear" w:fill="%s"/>' % (nsdecls('w'), color)))


def no_borders(table):
    tblPr = table._tbl.tblPr
    tblPr.append(parse_xml(
        r'<w:tblBorders %s><w:top w:val="none" w:sz="0"/><w:left w:val="none" w:sz="0"/>'
        r'<w:bottom w:val="none" w:sz="0"/><w:right w:val="none" w:sz="0"/>'
        r'<w:insideH w:val="none" w:sz="0"/><w:insideV w:val="none" w:sz="0"/></w:tblBorders>' % nsdecls('w')))


def thin_borders(table, color='C9D8EE'):
    table._tbl.tblPr.append(parse_xml(
        r'<w:tblBorders %s><w:top w:val="single" w:sz="4" w:color="%s"/>'
        r'<w:left w:val="single" w:sz="4" w:color="%s"/>'
        r'<w:bottom w:val="single" w:sz="4" w:color="%s"/>'
        r'<w:right w:val="single" w:sz="4" w:color="%s"/>'
        r'<w:insideH w:val="single" w:sz="4" w:color="%s"/>'
        r'<w:insideV w:val="single" w:sz="4" w:color="%s"/></w:tblBorders>'
        % ((nsdecls('w'),) + (color,) * 6)))


def pborder(par, color=BLUE, sz='10'):
    par._p.get_or_add_pPr().append(parse_xml(
        r'<w:pBdr %s><w:bottom w:val="single" w:sz="%s" w:space="3" w:color="%s"/></w:pBdr>'
        % (nsdecls('w'), sz, color)))


def cell_text(cell, text, bold=False, size=9, color='222222', align='left', font='微软雅黑'):
    cell.text = ''
    p = cell.paragraphs[0]
    p.paragraph_format.space_after = Pt(1.5)
    p.paragraph_format.space_before = Pt(1.5)
    p.paragraph_format.line_spacing = 1.15
    p.alignment = {'left': WD_ALIGN_PARAGRAPH.LEFT, 'center': WD_ALIGN_PARAGRAPH.CENTER,
                   'right': WD_ALIGN_PARAGRAPH.RIGHT}[align]
    r = p.add_run(str(text))
    r.bold = bold
    r.font.size = Pt(size)
    r.font.name = font
    r.font.color.rgb = RGBColor.from_string(color)
    _rrf = r._element.rPr.rFonts
    _rrf.set(qn('w:ascii'), font)
    _rrf.set(qn('w:hAnsi'), font)
    _rrf.set(qn('w:eastAsia'), font)


def h1(text, num=None):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(13)
    p.paragraph_format.space_after = Pt(6)
    p.paragraph_format.keep_with_next = True
    if num:
        r0 = p.add_run(num + '  ')
        r0.bold = True; r0.font.size = Pt(13); r0.font.color.rgb = RGBColor.from_string(BLUE)
    r = p.add_run(text)
    r.bold = True; r.font.size = Pt(13); r.font.color.rgb = RGBColor.from_string(DARK)
    pborder(p)
    return p


def body(text, size=10.5, bold=False, color='222222', space_after=5, indent=0, first_line=0):
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(space_after)
    p.paragraph_format.left_indent = Cm(indent)
    if first_line:
        p.paragraph_format.first_line_indent = Cm(first_line)
    r = p.add_run(text)
    r.bold = bold
    r.font.size = Pt(size)
    r.font.color.rgb = RGBColor.from_string(color)
    return p


def bullet(lead, text, size=10.5, color='222222'):
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Cm(0.55)
    p.paragraph_format.first_line_indent = Cm(-0.55)
    p.paragraph_format.space_after = Pt(4)
    r = p.add_run('▪ ')
    r.font.size = Pt(size); r.font.color.rgb = RGBColor.from_string(BLUE); r.bold = True
    if lead:
        r2 = p.add_run(lead)
        r2.bold = True; r2.font.size = Pt(size); r2.font.color.rgb = RGBColor.from_string(DARK)
    r3 = p.add_run(text)
    r3.font.size = Pt(size); r3.font.color.rgb = RGBColor.from_string(color)
    return p


def bullet10(lead, text):
    return bullet(lead, text, size=10)


def mktable(headers, rows, widths, align=None, size=9, head_size=9):
    t = doc.add_table(rows=1, cols=len(headers))
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    t.autofit = False
    thin_borders(t)
    hdr = t.rows[0].cells
    for i, h in enumerate(headers):
        cell_text(hdr[i], h, bold=True, size=head_size, color='FFFFFF',
                  align='center' if i else 'left')
        shade(hdr[i], BLUE)
    for ri, row in enumerate(rows):
        cells = t.add_row().cells
        for i, v in enumerate(row):
            a = (align[i] if align else ('left' if i == 0 else 'center'))
            bold = str(v).startswith('**')
            vv = str(v).replace('**', '')
            col = '222222'
            if str(v).startswith('!'):
                vv = str(v)[1:]; col = RED; bold = True
            cell_text(cells[i], vv, bold=bold, size=size, color=col, align=a)
            if ri % 2 == 1:
                shade(cells[i], 'F4F7FD')
    t.autofit = False
    t._tbl.tblPr.append(parse_xml(r'<w:tblLayout %s w:type="fixed"/>' % nsdecls('w')))
    t._tbl.tblPr.append(parse_xml(r'<w:tblW %s w:w="%d" w:type="dxa"/>' % (nsdecls('w'), int(sum(widths) * 567))))
    trPr = t.rows[0]._tr.get_or_add_trPr()
    trPr.append(parse_xml(r'<w:tblHeader %s/>' % nsdecls('w')))
    for row in t.rows:
        row._tr.get_or_add_trPr().append(parse_xml(r'<w:cantSplit %s/>' % nsdecls('w')))
    for i, cw in enumerate(widths):
        t.columns[i].width = Cm(cw)
    for row in t.rows:
        for i, cw in enumerate(widths):
            row.cells[i].width = Cm(cw)
    return t


def caption(text):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(7)
    p.paragraph_format.space_after = Pt(3)
    p.paragraph_format.keep_with_next = True
    r = p.add_run(text)
    r.bold = True; r.font.size = Pt(9.5); r.font.color.rgb = RGBColor.from_string(DARK)
    return p


def note(text):
    p = doc.add_paragraph()
    p.paragraph_format.space_before = Pt(3)
    p.paragraph_format.space_after = Pt(6)
    r = p.add_run(text)
    r.font.size = Pt(8); r.font.color.rgb = RGBColor.from_string(GREY)
    return p


# ---------- 页眉 / 页脚 ----------
hdr_p = sec.header.paragraphs[0]
hdr_p.paragraph_format.space_after = Pt(2)
if os.path.exists(LOGO):
    hdr_p.add_run().add_picture(LOGO, height=Cm(0.62))
r = hdr_p.add_run('    宝锐生物 BIORI BIOTECH ｜ 客户经营简报（内部）')
r.font.size = Pt(8.5); r.font.color.rgb = RGBColor.from_string(GREY)
pborder(hdr_p, BLUE, '8')

ftr_p = sec.footer.paragraphs[0]
ftr_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = ftr_p.add_run('BIORI BIOTECH · 珠海宝锐生物科技有限公司 ｜ ' + SLOGAN + ' ｜ 第 ')
r.font.size = Pt(7.5); r.font.color.rgb = RGBColor.from_string(GREY)
fld = OxmlElement('w:fldSimple'); fld.set('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}instr', 'PAGE')
run = OxmlElement('w:r'); rpr = OxmlElement('w:rPr')
sz = OxmlElement('w:sz'); sz.set('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', '15'); rpr.append(sz)
run.append(rpr); fld.append(run); ftr_p._p.append(fld)
r2 = ftr_p.add_run(' 页')
r2.font.size = Pt(7.5); r2.font.color.rgb = RGBColor.from_string(GREY)

# ---------- 标题带 ----------
band = doc.add_table(rows=1, cols=1)
band.autofit = False
no_borders(band)
c = band.rows[0].cells[0]
c.width = Cm(17.0)
shade(c, DARK)
c.text = ''
p0 = c.paragraphs[0]
p0.paragraph_format.space_before = Pt(9)
p0.paragraph_format.space_after = Pt(1)
r = p0.add_run('客户经营简报 ｜ 近三年（2024—2026.9）')
r.font.size = Pt(9); r.font.color.rgb = RGBColor.from_string('9FC0EA'); r.bold = True
p1 = c.add_paragraph()
p1.paragraph_format.space_after = Pt(2)
r = p1.add_run('西安佰奥莱博生物科技有限公司')
r.font.size = Pt(19); r.bold = True; r.font.color.rgb = RGBColor.from_string('FFFFFF')
p2 = c.add_paragraph()
p2.paragraph_format.space_after = Pt(9)
r = p2.add_run('三年累计 %s 万元 ｜ 101 单 ｜ A 类大客户 ｜ 归属：诊断原料大客户销售部 刘子研' % w(T3))
r.font.size = Pt(9.5); r.font.color.rgb = RGBColor.from_string('D6E4F7')

meta = doc.add_table(rows=1, cols=1)
no_borders(meta)
mc = meta.rows[0].cells[0]; mc.width = Cm(17.0)
shade(mc, LIGHT)
mc.text = ''
mp = mc.paragraphs[0]
mp.paragraph_format.space_before = Pt(5); mp.paragraph_format.space_after = Pt(5)
r = mp.add_run('呈：总经理　｜　报送：营销中心 销售管理　刘新元　｜　日期：2026-10-21')
r.font.size = Pt(8.5); r.font.color.rgb = RGBColor.from_string(DARK)
mp2 = mc.add_paragraph()
mp2.paragraph_format.space_before = Pt(0); mp2.paragraph_format.space_after = Pt(5)
r = mp2.add_run('数据口径：ERP 订单 2024-01-01 至 2026-09-30（含税价税合计）　｜　含销售/应收/客诉/商机/特殊要求五源')
r.font.size = Pt(8); r.font.color.rgb = RGBColor.from_string(GREY)

# ---------- 一、核心结论 ----------
h1('核心结论：三个判断、三组数字、三个动作', '一')
body('这家客户三年走完了一条完整的“原料供应商 → 直扩冻干方案供应商”升级路径，2025 年到顶、2026 年掉头向下；'
     '当前问题不在价格、不在关系，而在“订单断档 + 回款逾期 + 一条客诉悬了 5 个月”。建议按第六节三项动作在 11 月中旬前收口，'
     'Q4 仍有机会把全年拉回 80 万元以上。', first_line=0)

caption('三组关键数字')
cards = doc.add_table(rows=1, cols=3)
no_borders(cards)
card_data = [
    (w(T3) + ' 万', '三年累计销售额（单体口径）\n101 单，年均 ' + w(T3 / 3) + ' 万'),
    (w(A25) + ' 万', '2025 年峰值\n同比 +175.6%，直扩原料占 57.1%'),
    (w(AR['overdue']) + ' 万', '当前逾期应收\n占应收 ' + '%.1f%%' % AR['ratio'] + '，最长逾期 62 天'),
]
for i, (big, sub) in enumerate(card_data):
    cc = cards.rows[0].cells[i]
    cc.width = Cm(5.6)
    shade(cc, 'F4F7FD')
    cc.text = ''
    q = cc.paragraphs[0]
    q.alignment = WD_ALIGN_PARAGRAPH.CENTER
    q.paragraph_format.space_before = Pt(7); q.paragraph_format.space_after = Pt(0)
    r = q.add_run(big)
    r.bold = True; r.font.size = Pt(17); r.font.color.rgb = RGBColor.from_string(BLUE)
    q2 = cc.add_paragraph()
    q2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    q2.paragraph_format.space_before = Pt(1); q2.paragraph_format.space_after = Pt(7)
    r = q2.add_run(sub)
    r.font.size = Pt(7.5); r.font.color.rgb = RGBColor.from_string(GREY)
    if i < 2:
        pass

caption('三个判断')
bullet('判断一 · 结构升级成功，但增长极不连续。',
       '销售额 2024 年 %s 万 → 2025 年 %s 万（+175.6%%）→ 2026 年 1—9 月 %s 万（同口径 -51.8%%）。'
       '2025 年的高增长并非需求线性爬坡，而是 4 月、5 月、10 月三次集中采购（25.88 / 17.19 / 15.58 万元）'
       '贡献了全年 53.5%%；2026 年前 7 月月均 6.25 万，低于 2025 年月均 9.14 万，8 月仅 210 元、9 月为零。'
       % (w(A24), w(A25), w(A26)))
bullet('判断二 · 换成“直扩冻干”赛道是这三年最大的成果。',
       '直扩类（FMD 系列）占原料销售额比重由 2024 年 6.8%% 升至 2025 年 57.1%%、2026 年 56.6%%；'
       '主力货号从 FM513（通用 RT）切换为 FMD5084（RNA 直扩）与 FMD2011（DNA 直扩），'
       '2026 年还出现 FM5144-007701（-TP 定制后缀）成交，说明已进入按项目定制的深度绑定阶段。')
bullet('判断三 · 真实风险在“账上”和“闭环”上，不在商务条款上。',
       '应收 %s 万，其中逾期 %s 万（%.1f%%），单笔最长逾期 62 天；'
       '同时 2026-05-18 的 FM5144-007701 漏液客诉至今仍为“大区确认中”，已悬 %d 天未闭环，'
       '而 FM5144 在 12 个月内已发生 3 次同类客诉（沉淀/沉淀/漏液），属重复性问题。'
       % (w(AR['total']), w(AR['overdue']), AR['ratio'], 156))

# ---------- 二、三年经营轨迹 ----------
h1('三年经营轨迹', '二')
caption('表 1　年度经营对比（金额单位：万元）')
rows = [
    ['销售额（单体）', w(A24), w(A25), w(A26)],
    ['销售额（集团口径）', w(G24), w(G25), w(G26)],
    ['订单数（单体）', O24, O25, O26],
    ['订单行数', y['2024']['lines'], y['2025']['lines'], y['2026']['lines']],
    ['平均单笔（元）', '{:,}'.format(int(y['2024']['avg_order'])), '{:,}'.format(int(y['2025']['avg_order'])), '{:,}'.format(int(y['2026']['avg_order']))],
    ['赠品/样品行占比', pct(y['2024']['gift_lines'], y['2024']['lines']),
     pct(y['2025']['gift_lines'], y['2025']['lines']), pct(y['2026']['gift_lines'], y['2026']['lines'])],
    ['直扩类（FMD）占比', '%.1f%%' % y['2024']['fmd_share'], '%.1f%%' % y['2025']['fmd_share'], '%.1f%%' % y['2026']['fmd_share']],
    ['同比（单体）', '—', '+175.6%', '!-51.8%'],
]
mktable(['指标', '2024 年', '2025 年', '2026 年1-9月'], rows, [4.4, 3.8, 3.8, 4.8],
        align=['left', 'center', 'center', 'center'])
note('注：集团口径并入 西安瑞佰生物、基灵（西安）生物、上海基灵（动物医学检验/生物科技）；2026 年为 1—9 月累计，非全年。'
     '集团口径 2024 年 58.42 万、2025 年 111.01 万、2026 年 1—9 月 51.88 万（33 单），同口径同比 -43.0%。')

caption('表 2　月度节奏（万元）')
hdr = ['月份', '2024', '2025', '2026']
mrows = []
for m in range(1, 10):
    k = '2024-%02d' % m
    v24 = y['2024']['monthly'].get(k, 0) / 10000
    v25 = y['2025']['monthly'].get('2025-%02d' % m, 0) / 10000
    _v26 = y['2026']['monthly'].get('2026-%02d' % m, None)
    v26 = None if _v26 is None else _v26 / 10000.0
    mrows.append(['%d 月' % m, '%.2f' % v24, '%.2f' % v25,
                  ('%.2f' % v26) if v26 is not None else '—'])
mrows.append(['1—9 月合计', '%.2f' % (sum(v for k, v in y['2024']['monthly'].items() if k[:4] == '2024' and int(k[5:]) <= 9) / 10000),
              '%.2f' % (YTD25 / 10000), '!%.2f' % (A26 / 10000)])
t2 = mktable(hdr, mrows, [3.4, 4.0, 4.0, 4.0])
# 加粗合计行
for cell in t2.rows[-1].cells:
    for p in cell.paragraphs:
        for r0 in p.runs:
            r0.bold = True
    shade(cell, LIGHT)
note('2026 年 8 月 210 元、9 月为 0，为三年来首次连续两月无实质采购；10 月新下两单（ZHBR261008053、ZHBR261012034），'
     '系统金额尚未结算，未计入本表，其中 ZHBR261012034 为测试单、10-12 已由客户签收。')

# ---------- 三、产品结构 ----------
h1('产品结构：从“通用原料”到“直扩冻干方案”', '三')
caption('表 3　历年销售额 TOP 货号（万元 / 占比）')
SHORT = {
    'FM513': '5×Neoscript RT Premix (Probe qRT-PCR)',
    'FM513-1': '25×Neoscript RTase Mix',
    'FM5144': '5×FastAmpli RT Premix-UNG (Probe qRT-PCR)',
    'FM5144-007701-TP': '5×FastAmpli RT Premix-UNG (007701)',
    'FM5144-007701': '5×FastAmpli RT Premix-UNG (007701)',
    'FM507': '5×Superstart Premix plus (Probe qPCR)',
    'FM223-T1-DB': '2×Robustart Multi RT Premix',
    'FMD5084-000901': '5×Fast Direct RT Premix-UNG (000901)',
    'FMD2011': '2×Superstart Direct Premix-UNG',
}
def short_name(sku, name):
    if sku in SHORT:
        return SHORT[sku]
    n = name.replace(' (DG)', '').replace('(DG)', '').strip()
    return n if len(n) <= 40 else n[:40].rsplit(' ', 1)[0]

def top_rows(year):
    out = []
    amt = y[year]['amount']
    for i, s in enumerate(y[year]['top_sku'][:5], 1):
        out.append(['%d. %s' % (i, s['sku']), short_name(s['sku'], s['name']),
                    '%.2f' % (s['amt'] / 10000), pct(s['amt'], amt)])
    return out
t3 = mktable(['排名 / 货号', '产品名称', '金额（万元）', '占比'],
            top_rows('2024'), [4.2, 7.8, 2.8, 2.2],
            align=['left', 'left', 'center', 'center'], size=8.5)
def add_group_rows(tab, blocks, ncols, widths):
    for label, rows in blocks:
        cells = tab.add_row().cells
        cells[0].merge(cells[ncols - 1])
        c0 = tab.rows[-1].cells[0]
        cell_text(c0, label, bold=True, size=8.5, color=DARK, align='left')
        shade(c0, LIGHT)
        for ri, row in enumerate(rows):
            cs = tab.add_row().cells
            for i, v in enumerate(row):
                cell_text(cs[i], v, bold=False, size=8.5, color='222222',
                          align=('left' if i < 2 else 'center'))
                if ri % 2 == 1:
                    shade(cs[i], 'F4F7FD')
        for rr in tab.rows[-len(rows) - 1:]:
            for i, cw in enumerate(widths):
                rr.cells[i].width = Cm(cw)
add_group_rows(t3, [('2025 年（销售 %s 万元）' % w(A25), top_rows('2025')),
                    ('2026 年 1—9 月（销售 %s 万元）' % w(A26), top_rows('2026'))],
               4, [4.2, 7.8, 2.8, 2.2])
note('排序：2024 → 2025 → 2026 三段，各取前 5。直扩类金额：2024 年 2.72 万、2025 年 62.64 万、2026 年 1—9 月 24.77 万。')

body('结构判断：', bold=True, space_after=2)
bullet('', '2024 年以通用 RT/qPCR 预混液为主（FM513 一款占 42.2%），属“单点试用”阶段；')
bullet('', '2025 年两个直扩大项目（FMD5084 RNA 直扩、FMD2011 DNA 直扩）同时放量，合计 62.64 万、占 57.1%，客户把主力产品压在我方体系上；')
bullet('', '2026 年结构未退化（直扩仍占 56.6%），但总量收缩，说明是“客户出货量下降/换批节奏放缓”，而非被替换——这一点需在 Q4 复盘会上用客户口径确认。')

# ---------- 四、商机漏斗 ----------
h1('商机漏斗：120 万目标只完成三分之一', '四')
caption('表 4　在手存量商机（2026 年目标）')
orows = []
for o in OPPS:
    orows.append([o['name'].replace('西安佰奥莱博生物科技有限公司-', '').replace('佰奥莱博 ', ''), o['type'], '%.1f' % (o['target'] / 10000), '%.1f' % (o['target'] / 4 / 10000)])
orows.append(['**合计', '**5 个', '**%.0f' % (OPP_T / 10000), '**%.1f' % (OPP_T / 4 / 10000)])
mktable(['商机', '类型', '年度目标（万元）', '季度节奏（万元）'], orows, [7.2, 2.4, 3.7, 3.7],
        align=['left', 'center', 'center', 'center'])
note('五个商机全部为“存量”型，季度节奏按 Q1—Q4 均分。')
body('进度：2026 年 1—9 月已实现 %s 万元，占 120 万目标的 %.1f%%。按均分节奏，截至 Q3 应完成 %.0f 万，实际进度缺口 %.2f 万；'
     '距全年目标缺口 %.2f 万。Q4 若不能新增确认订单，五个存量商机将整体欠账。'
     % (w(A26), A26 / OPP_T * 100, OPP_T * 3 / 4 / 10000, GAP_Q3 / 10000, GAP_FULL / 10000),
     bold=False)

# ---------- 五、风险 ----------
h1('四类风险：回款、客诉闭环、技术验证、交付特殊性', '五')
caption('表 5　应收账款明细（2026 年 10 月账龄）')
arows = []
for b in AR['bills']:
    arows.append([b['bill'], b['due'], '%.2f' % (b['amt'] / 10000),
                  (str(b['od_days']) + ' 天') if b['od_days'] else '—',
                  b['bucket'], '未回' if b['od_amt'] > 0 else '—'])
arows.append(['**合计', '', '**%.2f' % (AR['total'] / 10000), '**最长 62 天', '**逾期 %.2f' % (AR['overdue'] / 10000), '**逾期率 %.1f%%' % AR['ratio']])
mktable(['账单号', '到期日', '金额', '已逾期', '账龄区间', '状态'], arows, [5.1, 2.3, 1.8, 2.2, 3.4, 2.2],
        align=['left', 'center', 'center', 'center', 'center', 'center'], size=8)
note('数据源：2026 年 10 月应收账龄表。逾期金额 %s 万中，ZHBR-AR260528025 一笔 7.735 万已逾期 62 天，占逾期额 42.5%%。' % w(AR['overdue']))

_c6 = caption('表 6　客诉记录（近两年 5 条）')
_c6.paragraph_format.page_break_before = True
crows = []
for c0 in DATA['complaints']:
    closed = c0['status'] == '结束'
    crows.append([c0['date'], c0['name'].split('-', 1)[1] if '-' in c0['name'] else c0['name'],
                  '已闭环' if closed else '!' + c0['status']])
mktable(['日期', '客诉事项', '状态'], crows, [2.6, 11.0, 3.4], align=['center', 'left', 'center'], size=8.5)
note('FM5144 同类问题（絮状沉淀 → 沉淀 → 漏液）在 2025-12、2026-03、2026-05 连续三次发生，不属偶发，建议按重复性质量问题立项。')

body('风险清单（按优先级）：', bold=True, space_after=2)
bullet10('P0 · 订单断档：', '8—9 月无实质采购且无在途订单；Q4 备货与客户出货计划未对齐，全年有跌破 50 万的风险。')
bullet10('P0 · 逾期回款：', '应收 %s 万、逾期 %s 万（%.1f%%），逾期集中在 5—7 月三笔，客户回款节奏已明显滞后于账期。' % (w(AR['total']), w(AR['overdue']), AR['ratio']))
bullet10('P1 · 客诉未闭环：', '2026-05-18 FM5144-007701 漏液客诉 5 个月仍在“大区确认中”，而该货号 2026 年仍有 6.80 万成交，属带病供货。')
bullet10('P1 · 技术验证未收口：', '9 月记录显示三个项目仍在测试未通过——PFM507 漏检（加 FE07 酶量、提高镁离子均未解决）、'
       'FMD5084 马 ACTB 个别靶标 Ct 延迟、镁离子翻倍后 Rn 下降，是 2026 年放量的直接前置卡点。')
bullet10('P2 · 交付特殊性：', '沉淀特殊要求 %d 条（包装/标签 %d、交付/物流 %d、其余 4），'
       '“1000T/包分装”“每包一份说明书”“送货单盖章”反复出现，人工核对成本高、易错。'
       % (DATA['n_sr'], DATA['sr_by_cat']['包装/标签'], DATA['sr_by_cat']['交付/物流']))
bullet10('P2 · 关系接力：', '历史订单涉及 4 名销售员（含已离职黄明月），现有 16 位联系人，关键对接人为孙欢、颜露玲、王万怡、颜艳；'
       '客户侧存在人员变动风险，需保持双人对接。')

# ---------- 六、动作 ----------
h1('下一步动作（截至 11 月 30 日）', '六')
caption('表 7　责任到人的收口清单')
arows2 = [
    ['1. 锁定 Q4 需求，阻断断档', '取得客户 Q4 采购计划（货号/数量/到货批次）书面确认，并据此排产备货',
     '刘子研 / 张立娅', '10-31', '客户邮件或会议纪要确认 Q4 需求量 ≥ 30 万'],
    ['2. 清收逾期 18.19 万', '逐笔对账，ZHBR-AR260528025（7.735 万）优先；必要时重审账期授权',
     '刘子研 / 财务部', '10-25', '回款到账或客户书面还款计划'],
    ['3. 闭环 FM5144 漏液客诉', '完成售后报告，推动客诉状态从“大区确认中”改为“结束”',
     '质量保证部 QA / 刘子研', '11-15', '客诉系统状态=结束，售后报告已发客户'],
    ['4. FM5144 重复客诉专项', '3 次同品类客诉（沉淀→沉淀→漏液）做根因分析，出纠正措施',
     '技术支持部 / QA、生产部', '11-30', '根因报告 + 纠正措施 + 批记录核查结论'],
    ['5. 技术卡点收口', 'PFM507 漏检、FMD5084 马 ACTB 的 Ct 延迟与 Rn 下降，给出配方调整并陪跑复测',
     '技术支持部 / 刘子研', '10-31', '客户复测通过（书面数据反馈）'],
    ['6. 缩小 76.25 万商机缺口', '五个存量商机按周更新阶段与金额，推动 Q4 新增确认订单',
     '刘子研 / 大客户部负责人', '12-31', '商机台账周更；Q4 确认订单 ≥ 30 万'],
    ['7. 特殊要求固化', '32 条要求整理为客户档案固化配置（分装/说明书/盖章/标签），下沉到订单备注模板',
     '销售支持部 / 刘子研、仓储', '11-30', '固化配置上线，新订单自动带出备注'],
]
mktable(['事项（含序号）', '具体动作', '主责 / 协同', '时限', '验收标准'], arows2,
        [2.9, 5.2, 3.8, 1.7, 3.4], size=8,
        align=['left', 'left', 'left', 'center', 'left'])
note('时限起算日 2026-10-21；第 1、2、5 项为 10 月内必须收口项，直接决定 Q4 能否回到 30 万以上。')

body('预期：第 1、5 项 10 月底收口，全年约 74—78 万元；叠加第 2、3 项闭环，可为 2027 年 120 万商机打底。', space_after=6)

# ---------- 七、口径 ----------
h1('数据口径与来源', '七')
for line in [
    '数据源：ERP 订单导出（2024-01-01 至 2026-09-30）｜ 2026 年 10 月应收账龄表 ｜ 飞书活动记录（截至 2026-10-16）｜ 客户特殊要求库 ｜ 客诉台账 ｜ 商机台账。',
    '金额口径：含税价税合计，单位万元；平均单笔为“销售额 ÷ 订单数”；赠品/样品行占比 = 赠品行数 ÷ 订单行数。',
    '客户口径：单体 = 西安佰奥莱博生物科技有限公司；集团 = 并入 西安瑞佰生物科技、基灵（西安）生物科技、上海基灵（动物医学检验实验室/生物科技）。',
    '2026 年为 1—9 月累计，非全年，同比对比同口径 1—9 月（2025 年同期 %s 万元）。' % w(YTD25),
    '10 月两笔新订单（ZHBR261008053、ZHBR261012034）系统中金额尚未结算，未计入销售额；ZHBR261012034 为测试单，已于 10-12 由客户签收。',
    '文中“判断/风险”为基于上述数据的分析结论，非客户书面确认，Q4 复盘须以客户口径复核。',
]:
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Cm(0.5)
    p.paragraph_format.first_line_indent = Cm(-0.5)
    p.paragraph_format.space_after = Pt(1)
    r = p.add_run('· ')
    r.font.size = Pt(8.5); r.font.color.rgb = RGBColor.from_string(BLUE)
    r = p.add_run(line)
    r.font.size = Pt(8); r.font.color.rgb = RGBColor.from_string(GREY)

# 落款
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
p.paragraph_format.space_before = Pt(6)
r = p.add_run('营销中心 · 销售管理　刘新元　｜　2026 年 10 月 21 日')
r.font.size = Pt(9); r.font.color.rgb = RGBColor.from_string(DARK)

os.makedirs(os.path.dirname(OUT), exist_ok=True)
doc.save(OUT)
print('SAVED', OUT, os.path.getsize(OUT))
