# -*- coding: utf-8 -*-
"""生成《西安佰奥莱博生物科技有限公司 近三年经营情况简报》Word（呈总经理）
数据源：数据/_bai_3y_data.json（由 脚本/bai_3y_data.py 从 ERP 导出与本地 JSON 现算）
"""
import json, os
from decimal import Decimal, ROUND_HALF_UP

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_TAB_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor

W = os.path.expanduser('~/Desktop/Hermes输出-工作类')
D = os.path.join(W, '数据')
IMG = os.path.join(W, '销售分析/佰奥莱博_三年简报_图')
OUT = os.path.join(W, '销售分析/西安佰奥莱博_近三年经营简报_呈总经理_20261021.docx')
LOGO = os.path.expanduser('~/work/公司资料/logo_transparent.png')

BLUE = RGBColor(0x0D, 0x47, 0xA1)
BLUE2 = RGBColor(0x15, 0x65, 0xC0)
GRAY = RGBColor(0x5F, 0x6B, 0x7A)
DARK = RGBColor(0x22, 0x2A, 0x35)
RED = RGBColor(0xC0, 0x39, 0x2B)
SLOGAN = '守护生命健康，缔造美好生活'

data = json.load(open(os.path.join(D, '_bai_3y_data.json')))
Y = data['yearly']
G = data['group']


def n(v, nd=2):
    q = Decimal(1).scaleb(-nd)
    return Decimal(repr(float(v))).quantize(q, rounding=ROUND_HALF_UP)


def wan(v, nd=2):
    """元 → 万元字符串"""
    return str(n(float(v) / 10000.0, nd))


def pct(v, nd=1):
    return str(n(v, nd)) + '%'


def ea(run, name='微软雅黑'):
    run.font.name = name
    run._element.rPr.rFonts.set(qn('w:eastAsia'), name)


doc = Document()
sec = doc.sections[0]
sec.top_margin = Cm(2.2)
sec.bottom_margin = Cm(2.0)
sec.left_margin = Cm(2.4)
sec.right_margin = Cm(2.4)

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

# ---------------- 页眉：Logo + 文档标识 ----------------
hdr = sec.header
hp = hdr.paragraphs[0]
hp.paragraph_format.tab_stops.add_tab_stop(Cm(16.0), WD_TAB_ALIGNMENT.RIGHT)
r = hp.add_run()
r.add_picture(LOGO, height=Cm(0.72))
r2 = hp.add_run('\t宝锐生物 BIORI BIOTECH　｜　客户经营简报')
r2.font.size = Pt(8.5)
r2.font.color.rgb = GRAY
ea(r2)
# 页眉下框线
pPr = hp._p.get_or_add_pPr()
pbdr = OxmlElement('w:pBdr')
bottom = OxmlElement('w:bottom')
bottom.set(qn('w:val'), 'single'); bottom.set(qn('w:sz'), '6')
bottom.set(qn('w:space'), '4'); bottom.set(qn('w:color'), 'BFCBDD')
pbdr.append(bottom); pPr.append(pbdr)

# ---------------- 页脚：slogan + 页码 ----------------
ftr = sec.footer
fp = ftr.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
fr = fp.add_run(SLOGAN + '　｜　BIORI BIOTECH · 宝锐生物　｜　第 ')
fr.font.size = Pt(8); fr.font.color.rgb = GRAY; ea(fr)
for instr in ('PAGE', ' / '):
    if instr == ' / ':
        rr = fp.add_run(' / '); rr.font.size = Pt(8); rr.font.color.rgb = GRAY
        continue
    rr = fp.add_run(); rr.font.size = Pt(8); rr.font.color.rgb = GRAY
    f1 = OxmlElement('w:fldChar'); f1.set(qn('w:fldCharType'), 'begin')
    it = OxmlElement('w:instrText'); it.set(qn('xml:space'), 'preserve'); it.text = instr
    f2 = OxmlElement('w:fldChar'); f2.set(qn('w:fldCharType'), 'end')
    rr._r.append(f1); rr._r.append(it); rr._r.append(f2)
rr = fp.add_run(' 页'); rr.font.size = Pt(8); rr.font.color.rgb = GRAY


def H(text, level=1):
    h = doc.add_heading(text, level=level)
    h.paragraph_format.space_before = Pt(14 if level == 1 else 10)
    h.paragraph_format.space_after = Pt(6)
    for r in h.runs:
        ea(r)
        r.font.color.rgb = BLUE if level == 1 else BLUE2
        r.font.size = Pt(15 if level == 1 else 12)
        r.bold = True
    return h


def P(text, bold=False, size=10.5, color=None, align=None, space_after=4, indent=0):
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(space_after)
    if indent:
        p.paragraph_format.first_line_indent = Cm(indent)
    r = p.add_run(text)
    r.bold = bold
    r.font.size = Pt(size)
    ea(r)
    if color:
        r.font.color.rgb = color
    if align:
        p.alignment = align
    return p


def lead(num, title, body):
    """结论条目：编号+加粗判断，后接依据"""
    p = doc.add_paragraph()
    p.paragraph_format.space_after = Pt(7)
    p.paragraph_format.left_indent = Cm(0.6)
    p.paragraph_format.first_line_indent = Cm(-0.6)
    r1 = p.add_run(num + '　')
    r1.bold = True; r1.font.size = Pt(10.5); r1.font.color.rgb = BLUE
    ea(r1)
    r2 = p.add_run(title)
    r2.bold = True; r2.font.size = Pt(10.5); r2.font.color.rgb = DARK
    ea(r2)
    r3 = p.add_run(body)
    r3.font.size = Pt(10.5); r3.font.color.rgb = DARK
    ea(r3)
    return p


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


def fixed_layout(table, widths_cm):
    """强制固定列宽（python-docx 仅设 cell.width 在部分渲染器无效）"""
    tbl = table._tbl
    tblPr = tbl.tblPr
    layout = OxmlElement('w:tblLayout'); layout.set(qn('w:type'), 'fixed'); tblPr.append(layout)
    tblW = OxmlElement('w:tblW')
    tblW.set(qn('w:type'), 'dxa'); tblW.set(qn('w:w'), str(int(sum(widths_cm) * 567)))
    tblPr.append(tblW)
    grid = tbl.find(qn('w:tblGrid'))
    if grid is not None:
        for gc, wd in zip(grid.findall(qn('w:gridCol')), widths_cm):
            gc.set(qn('w:w'), str(int(wd * 567)))
    for row in table.rows:
        for i, wd in enumerate(widths_cm):
            if i < len(row.cells):
                row.cells[i].width = Cm(wd)


def T(headers, rows, widths=None, fs=9, aligns=None, highlight_rows=()):
    t = doc.add_table(rows=1, cols=len(headers))
    t.style = 'Table Grid'
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    for i, h in enumerate(headers):
        c = t.rows[0].cells[i]
        c.text = ''
        p = c.paragraphs[0]
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        r = p.add_run(h)
        r.bold = True; r.font.size = Pt(fs); r.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
        ea(r)
        shade(c, '0D47A1')
    for ri, row in enumerate(rows):
        cells = t.add_row().cells
        for i, v in enumerate(row):
            c = cells[i]
            c.text = ''
            p = c.paragraphs[0]
            if aligns and i < len(aligns) and aligns[i] == 'r':
                p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
            elif aligns and i < len(aligns) and aligns[i] == 'c':
                p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            r = p.add_run('' if v is None else str(v))
            r.font.size = Pt(fs)
            ea(r)
            if ri in highlight_rows:
                r.bold = True
                shade(c, 'FFF3E0')
            elif ri % 2 == 1:
                shade(c, 'F5F8FC')
    trPr = t.rows[0]._tr.get_or_add_trPr()
    th = OxmlElement('w:tblHeader'); th.set(qn('w:val'), 'true'); trPr.append(th)
    if widths:
        t.autofit = False
        fixed_layout(t, widths)
    doc.add_paragraph().paragraph_format.space_after = Pt(2)
    return t


def note(text):
    P(text, size=8.5, color=GRAY, space_after=8)


def figure(path, width=15.6, caption=''):
    doc.add_picture(path, width=Cm(width))
    doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
    if caption:
        P(caption, size=8.5, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=8)


# ================= 封面 =================
for _ in range(2):
    doc.add_paragraph()
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run().add_picture(LOGO, height=Cm(1.5))
for _ in range(2):
    doc.add_paragraph()
P('西安佰奥莱博生物科技有限公司', bold=True, size=24, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=2)
P('近三年经营情况简报', bold=True, size=24, color=BLUE, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=10)
P('2024 — 2026 年 9 月　｜　呈总经理', size=12, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER, space_after=2)
P('数据基准日：2026 年 10 月 21 日', size=10.5, color=GRAY, align=WD_ALIGN_PARAGRAPH.CENTER)
for _ in range(3):
    doc.add_paragraph()

meta = [
    ['客户主体', '西安佰奥莱博生物科技有限公司（集团口径含瑞佰、基灵西安、上海基灵 2 家）'],
    ['客户等级', '大客户 · %s' % data['tier']],
    ['归属销售', '%s（%s）' % (data['sales'], data['dept'])],
    ['数据区间', '2024-01-01 至 2026-09-28（ERP 订单明细，价税合计）'],
    ['编 制', '营销中心 · 销售管理　刘新元'],
]
t = doc.add_table(rows=0, cols=2)
t.alignment = WD_TABLE_ALIGNMENT.CENTER
t.autofit = False
for k, v in meta:
    cells = t.add_row().cells
    cells[0].width = Cm(3.4); cells[1].width = Cm(11.8)
    p0 = cells[0].paragraphs[0]
    r = p0.add_run(k); r.bold = True; r.font.size = Pt(10.5); r.font.color.rgb = BLUE; ea(r)
    p1 = cells[1].paragraphs[0]
    r = p1.add_run(v); r.font.size = Pt(10.5); ea(r)
    shade(cells[0], 'EAF1FA')
fixed_layout(t, [3.4, 11.8])
doc.add_page_break()

# ================= 一、核心结论 =================
H('一、五条结论（先看这一段）', 1)

lead('结论一｜', '规模先翻倍、后腰斩，2026 是关键失速年。',
     '销售额 2024 年 %s 万元 → 2025 年 %s 万元（同比 +%s）→ 2026 年 1–9 月 %s 万元（同比 −%s）。'
     '2026 年 8 月仅 210 元、9 月为 0，Q4 若无大单，全年将在 50 万元量级，相当于 2025 年的四成。'
     % (wan(Y['2024']['amount']), wan(Y['2025']['amount']), pct(Y['yoy_2025']),
        wan(Y['2026']['amount']), pct(abs(Y['yoy_2026']))))

lead('结论二｜', '客户性质已变：从“买通用 RT 试剂”变成“直扩冻干联合开发”。',
     'FMD 直扩系列占客户销售额 2024 年 %s → 2025 年 %s → 2026 年 1–9 月 %s，'
     '主销货号由 FM513（常规 RT 预混）切换为 FMD5084、FMD2011。'
     '这意味着客户把配方与放大交给我们，替代成本上升，但也意味着我方要为项目节奏和性能负责。'
     % (pct(Y['2024']['fmd_share']), pct(Y['2025']['fmd_share']), pct(Y['2026']['fmd_share'])))

lead('结论三｜', '下滑主因是项目节奏 + 质量摩擦，不是被竞争对手整体替换。',
     '2026 年 8–9 月订单近乎归零，但同期沟通并未中断（9 月 23 日集中跟进宠物真菌、PFM507、马 ACTB 三个项目；10 月 16 日线上跟进）。'
     '年内 3 起客诉集中在 FM5144 系列与包装环节，属“信任被磨损”，可用质量闭环与交付纪律修复。')

lead('结论四｜', '回款已经亮红灯，必须在 10 月底前处置。',
     '当前应收 %s 万元，其中逾期 %s 万元，占 %s；最长逾期 62 天（账单 ZHBR-AR260528025，%s 万元）；10 月至今零回款。'
     '该客户 2026 年回款风险等级应上调为“重点盯防”。'
     % (wan(data['ar']['total']), wan(data['ar']['overdue']), pct(data['ar']['ratio']),
        wan(data['ar']['bills'][0]['amt']) if data['ar']['bills'] else '—'))

lead('结论五｜', '履约复杂度是隐形雷：32 条特殊要求靠人传，已直接造成客诉。',
     '在生效特殊要求 %d 条，其中包装/标签 %d 条、交付/物流 %d 条；'
     '2026 年“说明书装错”“漏液”两起客诉均发生在包装与单据执行环节。'
     '把要求做成随单执行卡，是本客户最划算的一项改善。'
     % (data['n_sr'], data['sr_by_cat'].get('包装/标签', 0), data['sr_by_cat'].get('交付/物流', 0)))

P('一句话给总经理：这是一个“值得保、也保得住”的 A 类客户——技术绑定在加深，'
  '问题出在交付纪律与回款，不在产品被替换；2026 年的下滑首先是项目节奏问题，其次是质量问题与账款互信相互拖累。',
  size=10.5, color=BLUE, space_after=6)

# ================= 二、三年经营总览 =================
H('二、三年经营总览', 1)
rows = [
    ['销售额（万元）', wan(Y['2024']['amount']), wan(Y['2025']['amount']), wan(Y['2026']['amount']) + '（1–9月）'],
    ['同比', '—', '+%s' % pct(Y['yoy_2025']), pct(Y['yoy_2026'])],
    ['订单数（笔）', Y['2024']['orders'], Y['2025']['orders'], Y['2026']['orders']],
    ['单均金额（万元）', wan(Y['2024']['avg_order']), wan(Y['2025']['avg_order']), wan(Y['2026']['avg_order'])],
    ['订单行（条）', Y['2024']['lines'], Y['2025']['lines'], Y['2026']['lines']],
    ['赠品行（条）', Y['2024']['gift_lines'], Y['2025']['gift_lines'], Y['2026']['gift_lines']],
    ['FMD 直扩占比', pct(Y['2024']['fmd_share']), pct(Y['2025']['fmd_share']), pct(Y['2026']['fmd_share'])],
]
T(['指标', '2024 年', '2025 年', '2026 年'], rows, widths=[4.6, 3.7, 3.7, 4.0],
  aligns=['l', 'r', 'r', 'r'])
note('注：金额为 ERP 订单“价税合计”，剔除作废单据；2026 年取数至 9 月 28 日。'
     '客户 360 口径（含 10 月已开票）2026 年为 %s 万元。' % wan(463340))

figure(os.path.join(IMG, 'chart_year.png'), 15.4, '图 1　近三年销售额：2025 年放量，2026 年失速')

# 月度表
months = ['%d 月' % m for m in range(1, 13)]
m24 = [wan(Y['2024']['monthly'].get('2024-%02d' % m, 0), 1) for m in range(1, 13)]
m25 = [wan(Y['2025']['monthly'].get('2025-%02d' % m, 0), 1) for m in range(1, 13)]
m26 = [wan(Y['2026']['monthly'].get('2026-%02d' % m, 0), 1) if m <= 9 else '—' for m in range(1, 13)]
T(['月份'] + months, [
    ['2024'] + m24,
    ['2025'] + m25,
    ['2026'] + m26,
], widths=[1.5] + [1.13] * 12, fs=7.5, aligns=['c'] + ['r'] * 12)
note('单位：万元。2024 年 8–9 月为全年低点（0.08 / 0.25 万元），2025 年 4 月单月 '
     '%s 万元为三年最高；2026 年 7 月后快速下滑。' % wan(Y['2025']['monthly'].get('2025-04', 0), 1))

figure(os.path.join(IMG, 'chart_month.png'), 15.4, '图 2　月度销售额对比：2026 年 8–9 月归零')

# ================= 三、产品结构 =================
H('三、产品结构：从通用预混到直扩冻干', 1)
rows = []
for i in range(5):
    rows.append([
        str(i + 1),
        Y['2024']['top_sku'][i]['sku'] if i < len(Y['2024']['top_sku']) else '—',
        wan(Y['2024']['top_sku'][i]['amt']) if i < len(Y['2024']['top_sku']) else '—',
        Y['2025']['top_sku'][i]['sku'] if i < len(Y['2025']['top_sku']) else '—',
        wan(Y['2025']['top_sku'][i]['amt']) if i < len(Y['2025']['top_sku']) else '—',
        Y['2026']['top_sku'][i]['sku'] if i < len(Y['2026']['top_sku']) else '—',
        wan(Y['2026']['top_sku'][i]['amt']) if i < len(Y['2026']['top_sku']) else '—',
    ])
T(['#', '2024 货号', '金额', '2025 货号', '金额', '2026 货号', '金额'], rows,
  widths=[0.7, 3.2, 1.7, 3.2, 1.7, 3.4, 1.7], fs=8,
  aligns=['c', 'l', 'r', 'l', 'r', 'l', 'r'])
note('单位：万元，按货号聚合、取各年前五。2025 年起 FMD5084-000901（直扩 RT）与 FMD2011（直扩 DNA）'
     '接替 FM513 成为主力；2026 年 FM5144-007701-TP 冻干形态首次进入前三。')

figure(os.path.join(IMG, 'chart_mix.png'), 15.4, '图 3　FMD 直扩系列占比：三年完成结构性迁移')

P('结构判断：', bold=True, color=BLUE)
P('1）客户已把 RNA 直扩（FMD5084）、DNA 直扩（FMD2011）、宠物 RNA/DNA 冻干（FM513/FM507）四条线放到我方体系上，'
  '2025 年这四条线合计贡献接近全部增量；2）常规液体预混（FM513/FM5144/FM507）仍在采购，但已退居配套地位；'
  '3）货号变更（FMD5084-000901 → FMD5084-007703-DB，2026 年 7 月完成）说明客户在我方体系上做过版本迭代，'
  '粘性强于单纯比价客户。', size=10, indent=0.6)

# ================= 四、集团口径 =================
H('四、集团口径：今年增量其实来自基灵（西安）', 1)
gc = G['2026']['by_cust']
g25 = G['2025']['by_cust']
g24 = G['2024']['by_cust']
names = set(list(gc) + list(g25) + list(g24))
rows = []
for nm in ['西安佰奥莱博生物科技有限公司', '基灵（西安）生物科技有限公司', '上海基灵生物科技有限公司',
           '上海基灵动物医学检验实验室有限公司', '西安瑞佰生物科技有限公司']:
    rows.append([nm, wan(g24.get(nm, 0)), wan(g25.get(nm, 0)), wan(gc.get(nm, 0))])
rows.append(['集团合计', wan(G['2024']['amount']), wan(G['2025']['amount']), wan(G['2026']['amount'])])
T(['主体', '2024 年', '2025 年', '2026 年 1–9 月'], rows,
  widths=[8.0, 2.6, 2.5, 2.6], aligns=['l', 'r', 'r', 'r'], highlight_rows=(5,))
note('集团合计同比：2026 年 1–9 月 %s 万元 vs 2025 年同期 %s 万元，−%s。'
     '集团口径跌幅（−%s）小于单体（−%s），原因是基灵（西安）今年已做 %s 万元。'
     % (wan(G['2026']['amount']), wan(G['ytd_2025']['amount']), pct(abs(G['yoy_2026'])),
        pct(abs(G['yoy_2026'])), pct(abs(Y['yoy_2026'])), wan(gc.get('基灵（西安）生物科技有限公司', 0))))

# ================= 五、风险台账 =================
H('五、风险台账（三项，按紧急度排序）', 1)

H('5.1 回款：逾期占七成，账龄已进入 61–365 天区间', 2)
rows = []
for b in data['ar']['bills']:
    od = b.get('od_days')
    rows.append([b.get('bill'), b.get('due'), wan(b['amt']), (str(od) + ' 天') if od else '—', b.get('bucket')])
rows.append(['合计', '—', wan(data['ar']['total']), '逾期 %s' % wan(data['ar']['overdue']), pct(data['ar']['ratio']) + ' 逾期'])
T(['账单号', '到期日', '金额（万元）', '逾期天数', '账龄区间'], rows,
  widths=[4.6, 2.6, 2.6, 2.4, 3.6], aligns=['l', 'c', 'r', 'c', 'l'], highlight_rows=(len(rows) - 1,))
note('数据源：2026 年 10 月应收账龄（大客户部）明细，基准日 2026-10-21。10 月回款 0；'
     '其中 7.74 万元已超 60 天，需按公司逾期制度上报跟进。')

H('5.2 客诉：5 起，1 起未闭环，质量问题集中在 FM5144 系列', 2)
rows = []
for c in data['complaints']:
    rows.append([c['date'], c['name'], c['status'], '关注' if c['status'] != '结束' else '已闭环'])
T(['日期', '客诉事项', '处理状态', '判定'], rows,
  widths=[2.2, 8.6, 3.0, 1.8], aligns=['c', 'l', 'c', 'c'])
note('2026-05-18 FM5144-007701 漏液客诉仍处“大区确认中”，是本客户唯一未闭环质量问题，'
     '建议 10 月底前给出结案结论（见第七节行动建议）。')

H('5.3 履约：32 条特殊要求集中在包装与单据，靠人工传递', 2)
rows = [['包装/标签', data['sr_by_cat'].get('包装/标签', 0), '1000T/包分装、赠送与尾数单独分装、1 份说明书/包、控 NA'],
        ['交付/物流', data['sr_by_cat'].get('交付/物流', 0), '送货单加盖出库章/仓库章/销售业务章、指定收件人备注'],
        ['质检/文件', data['sr_by_cat'].get('质检/文件', 0), '随货 COA、批号指定'],
        ['价格/账期', data['sr_by_cat'].get('价格/账期', 0), '发票金额不超 1 万需拆票'],
        ['其他', data['sr_by_cat'].get('其他', 0), '采购单送货单盖章要求']]
T(['类别', '条数', '典型要求（摘录）'], rows, widths=[3.0, 1.8, 10.8], aligns=['l', 'c', 'l'])
note('要求条目来源：ERP 订单备注结构化提取（32 条全部为“已生效”，归属人 张立娅）。'
     '该客户 16 位联系人、多项目并行，要求无法靠记忆传递。')

# ================= 六、机会盘点 =================
H('六、机会盘点：在手商机 120 万元目标，兑现不足四成', 1)
opps = data['opps']
rows = []
tot = 0
for o in opps:
    tot += float(o['target'])
    rows.append([o['name'], o['type'], wan(o['target']), o['sales']])
rows.append(['合计', '—', wan(tot), '—'])
T(['商机', '类型', '年目标（万元）', '客户经理'], rows,
  widths=[8.8, 1.8, 3.0, 2.0], aligns=['l', 'c', 'r', 'c'], highlight_rows=(len(rows) - 1,))
note('另有“委托批量生产冻干球”商机（需求确认阶段，未设年度目标）。'
     '以 2026 年实际 %s 万元对照 120 万元目标，兑现率约 %s；'
     '缺口主要来自 FMD5084（40 万）与 FMD2011（30 万）两个存量项目的采购节奏放缓。'
     % (wan(463340), pct(463340 / tot * 100)))

P('2027 目标建议（待拍板）：', bold=True, color=BLUE)
P('以 2025 年峰值 %s 万元为基准，设 2027 年保底 %s 万元（峰值的 80%%）、冲刺 %s 万元（持平 2025 年）。'
  '支撑条件是四个存量项目恢复季度采购节奏 + 回款转正常。此为建议值，需与部门年度目标（诊断原料 6650 万元）对齐后落定。'
  % (wan(Y['2025']['amount']), wan(Y['2025']['amount'] * 0.8), wan(Y['2025']['amount'])), size=10, indent=0.6)

# ================= 七、行动建议 =================
H('七、行动建议（主责唯一，可按此考核）', 1)
rows = [
    ['1', '逾期回款专项：10 月底前收回 7.74 万（62 天）+4.25 万（35 天）两笔，合计 11.99 万；'
          '11 月底前清理剩余逾期', '刘子研', '财务部、张立娅（对账）', '10/31、11/30',
     '银行到账凭证 + AR 台账对应两笔清零'],
    ['2', 'FM5144-007701 漏液客诉结案：出复测结论、责任判定与整改措施，客户书面确认', '技术支持部（指定对接人）',
     '质量保证部、刘子研', '10/31', '客诉状态改为“结束”，客户确认邮件归档'],
    ['3', '建“佰奥莱博订单执行卡”随单流转：32 条要求固化为勾选项（1000T/包、盖章类型、说明书/包、控 NA、拆票）',
     '张立娅', '仓库、质量保证部', '11/15', '连续 3 单零差错，客诉 0 起'],
    ['4', '四个存量项目逐季对表：FMD5084（40 万）、FMD2011（30 万）、宠物 RNA FM513（24 万）、猫传腹（18 万）',
     '刘子研', '技术支持部', '每季度末', '季度采购额与商机目标偏差 ≤20%'],
    ['5', '集团客户合并管理：基灵（西安）纳入同一客户经理口径与同一执行卡，统一账期与报价', '刘子研',
     '销售管理（刘新元）', '12/31', '集团口径台账合并、单一联系人视图上线'],
    ['6', '客户风险评估升级：把该客户回款等级调为“重点盯防”，月度复盘进销售管理例会', '刘新元', '刘子研',
     '10/31 起月度', '例会纪要留痕，逾期率月度下降'],
]
T(['#', '行动', '主责', '协同', '时限', '验收标准'], rows,
  widths=[0.7, 5.8, 2.1, 2.3, 1.7, 3.2], fs=8,
  aligns=['c', 'l', 'c', 'l', 'c', 'l'])

# ================= 八、口径 =================
H('八、数据口径与出处', 1)
srcs = [
    '1）销售额与订单：ERP 销售订单导出——2024 全年订单、2025 全年订单（Sheet1）、销售订单_20260101-20260928；'
    '金额口径为“价税合计”，按客户名称精确匹配“西安佰奥莱博生物科技有限公司”，剔除作废与关闭单据。',
    '2）2026 年 1–9 月为 9 月 28 日止；客户 360 与应收账龄口径（含 10 月已开票）2026 年为 46.33 万元，'
    '两口径并列，不用近似值替代精确值。',
    '3）应收账龄：数据/2026年10月应收账龄_大客户部.json 明细（基准日 2026-10-21），5 张账单逐笔可核。',
    '4）客诉、商机、活动：客户 360（customer_360.json）与飞书活动导出，客诉状态为系统实时状态。',
    '5）特殊要求：customer_special_requirements.json，32 条“已生效”，来源为 ERP 订单备注结构化提取。',
    '6）集团口径成员：西安佰奥莱博、西安瑞佰、基灵（西安）、上海基灵生物、上海基灵动物医学检验实验室，'
    '取自 workbench_data 集团归属表。',
    '7）本简报不含毛利、库存与在途数据（未在本地数据源中），如需请另行取数。',
]
for s in srcs:
    P(s, size=9, color=GRAY, space_after=3)

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