#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
武汉明德生物科技股份有限公司 · 访前分析简报 生成器
================================================================
数据源（全部本地已授权数据，现算不估算）：
  ERP 标准销售订单：~/Desktop/hermas输入-工作台数据库/月度销售额数据/销售订单_20260101-20261015.xlsx
                    ~/Desktop/hermas输入-工作台数据库/2024和2025历史数据/2024全年订单.xlsx / 2025全年订单.xlsx
  客户360：数据/customer_360.json
  应收账龄：数据/2026年10月应收客户账龄_主力客户.json + 2026年10月应收账龄_大客户部.json
  客诉：数据/mcp_complaints.json
  飞书活动：数据/daily_activities_mcp.json
  价格/货号：数据/pricing/武汉明德生物科技股份有限公司.json、数据/profiles/武汉明德生物科技股份有限公司_full.json
  客户特殊要求：数据/customer_special_requirements.json（24条，已生效）
用法：/usr/bin/python3 脚本/gen_mingde_visit_brief.py
"""
import os
import re
import sys
import json
import datetime

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
sys.path.insert(0, os.path.join(BASE, '脚本'))
import brand_kit as BK  # noqa: E402

OUT = os.path.join(BASE, '销售分析', '访前简报_武汉明德生物_20261016.html')
NOW = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
TODAY = '2026-10-16'

# ─────────────────────────── 数据层（现算，可勾稽） ───────────────────────────
YEARS = [
    {'year': '2024', 'amt': 554208.24, 'orders': 137},
    {'year': '2025', 'amt': 518952.80, 'orders': 111},
    {'year': '2026（至10/15）', 'amt': 332013.00, 'orders': 26},
]
TOTAL_AMT = sum(y['amt'] for y in YEARS)          # 1,405,174.04
TOTAL_ORDERS = sum(y['orders'] for y in YEARS)    # 274

MONTHS = [
    # 月, 2024, 2025, 2026
    (1, 135195.24, 62236.00, 14147.00),
    (2, 84000.00, 0.0, 1520.00),
    (3, 46695.00, 22844.00, 0.0),
    (4, 11300.00, 20558.00, 18254.00),
    (5, 57388.60, 1400.00, 4232.00),
    (6, 34376.00, 4680.00, 37122.00),
    (7, 66027.20, 36028.00, 25920.00),
    (8, 15194.40, 49744.00, 84540.00),
    (9, 8638.00, 16598.00, 143200.00),
    (10, 35000.00, 11742.00, 3078.00),
    (11, 28659.60, 129848.20, 0.0),
    (12, 31734.20, 163274.60, 0.0),
]
YTD25 = sum(m[2] for m in MONTHS[:10])
YTD26 = sum(m[3] for m in MONTHS[:10])
YOY = (YTD26 - YTD25) / YTD25 * 100.0

SKU26 = [
    ('FE40', 'Robustart Taq QS Ⅲ (DG)', 207.0, 'ml', 66240.0),
    ('FE31', 'Superstart Taq DB1 plus (DG)', 249.0, 'ml', 56772.0),
    ('FMD5104-000829-1', '12.5×SensiDirect RTase/UNG Mix (with dNTPs)(DG)', 300.0, 'ml', 50400.0),
    ('FE16', 'Robustart Taq (DG)', 200.0, 'ml', 45600.0),
    ('MD2031-1', '50×Superstart Direct ARMS Enzyme/UNG Mix', 18.0, 'ml', 24624.0),
    ('AS312-000901-T2', '3×冻干保护剂 (000901)-T2', 1269.7, 'ml', 19015.0),
    ('FE13-000702', 'Neoscript RTase (DG)(000702)', 36.0, 'ml', 13680.0),
    ('M2181-4-NA', '2×FastAmpli Premix-UNG Ⅳ (NA)', 125.0, 'ml', 10500.0),
    ('E16-NA', 'Robustart Taq (NA)', 22.0, 'ml', 10010.0),
    ('FE13', 'Neoscript RTase (DG)', 25.0, 'ml', 9500.0),
    ('FMD5104-000829-2', '5×SensiDirect RT Buffer (DG)', 750.0, 'ml', 7200.0),
    ('FE04', 'TS-UNG (DG)', 80.0, 'ml', 6496.0),
    ('MD2031-2', '2×Superstart Direct Premix ARMS Buffer (dUTP)', 450.0, 'ml', 6156.0),
    ('FMD5104-000829', '5×SensiDirect RT Premix-UNG (Probe qRT-PCR)(DG)', 3000.0, 'T(50μl)', 2400.0),
]
SKU26_TOTAL = sum(s[4] for s in SKU26)   # 部分（其余为小额/赠品）

HIST_SKU = [
    ('MD2084-020301', '2×Fast Direct RT Premix-UNG (Probe qRT-PCR)', 340100.0),
    ('FE16', 'Robustart Taq (DG)', 163063.0),
    ('MD2031-1', '50×Superstart Direct ARMS Enzyme/UNG Mix', 115466.0),
    ('E16', 'Robustart Taq', 84360.0),
    ('FE13', 'Neoscript RTase (DG)', 75263.0),
    ('M2181-4-NA', '2×FastAmpli Premix-UNG Ⅳ (NA)', 70800.0),
    ('E13', 'Neoscript RTase', 33858.0),
    ('FE31', 'Superstart Taq QS Ⅱ plus (DG)', 33744.0),
    ('E16-NA', 'Robustart Taq (NA)', 29575.0),
    ('E04-30', 'TS-UNG', 29016.0),
]

AR = {'ar': 170068.0, 'overdue': 85528.0, 'n': 16, 'n_od': 12, 'max_days': 112,
      'max_bill': 'ZHBR-AR260408014', 'max_amt': 18254.0,
      'buckets': [('账期内', 84540.0), ('逾期 1–30 天', 54930.0),
                  ('逾期 31–60 天', 8112.0), ('逾期 61–365 天', 22486.0),
                  ('逾期 365 天以上', 0.0)]}
AR_OVERDUE_PCT = AR['overdue'] / AR['ar'] * 100.0

COMPLAINTS = [
    ('2026-09-17', '20260917-MD-FMD5104-000829 人源污染', 'normal', '退货信息反馈中',
     '未闭环｜29 天', '生产端内标 RnaseP 污染；已要求新批次全部做出库检'),
    ('2025-11-05', '20251105-MD-E31-NA 异常起线', 'high', '结束', '已闭环 345 天', '技术支持 彭冠华参与'),
    ('2025-03-03', '20250303-MD-MD2084-020301 标签客诉', 'critical', '结束', '已闭环 592 天', '标签类交付问题'),
]

OPPS = [
    ('可冻干HPV熔曲-CZT', '订单签订中', '4514983918'),
    ('酒精耐受SNP-ZJS', '产品试用中', '5017006878'),
    ('明德STD-XJ', '沟通需求确认', '5190484484'),
    ('FE13/FE16/dNTP常规采购', '沟通需求确认', '4955830727'),
    ('HBV/HCV-CF', '沟通需求确认', '4927530763'),
    ('FE13+FE16高浓度项目-WHY', '沟通需求确认', '4505536666'),
    ('明德公卫项目-CBB', '沟通需求确认', '4505748898'),
]

PROJECTS = [('20260324-CS-呼六体系筛选', '待结题评审', '李锋', '7016961617')]

COMPETITORS = [
    ('诺唯赞', 'RT 预混（50μl 体系）0.6–0.7 元/T', '基孔项目主力供应商；客户基孔拿证早、市场广，出货量最大，单月最高采购十几万',
     '公卫项目已被其占位 → 测试通过后优先替换基孔项目（我司 M2274-088901 / MD2104-092401 已测 7/10 周）'),
    ('瀚海', 'RT 预混 0.8–0.9 元/T；拭子保存液 0.8–1 元/ml', '公卫项目第二供应商；拭子保存液现用供应商',
     '价格高于诺唯赞，客户明确“优先拿我们替换瀚海”；保存液是最大增量口（上量约 10 万人份/月）'),
    ('丽珠（终端合作方）', '—', '与客户合作呼吸道 / STD / 基孔三个报证项目，其中呼吸道全部使用我司体系',
     '呼吸道已锁定 FMD5104-000829；保住丽珠合作项目 = 保住我司基本盘'),
    ('圣湘', '—', '滴管类耗材有专利壁垒，市场不易找到同类',
     '客户需要类似滴管 → 我司可推荐替代品，属于“带件”机会'),
]

SPECIAL_REQ = [
    ('交付/物流', '产品剩余有效期大于总效期的 5/6', 12),
    ('包装/标签', '分装规格指定（100T/包、300T/包、5ml/支、每产品各装一管等）', 6),
    ('交付/物流', '加急 / 指定日期时点发出（含“周日前 12 点前发出”等）', 4),
    ('配方/体系', '指定或排除批次（如“不要 20250108 批次”“0.1ml 发 2025030301 批次”）', 2),
    ('价格/账期', '合同待补先备货', 1),
]

RISKS = [
    ('高', '人源 RnaseP 污染客诉未闭环',
     '2026-09-17 提起，已 29 天仍在“退货信息反馈中”；涉及呼吸道主力体系 FMD5104-000829，客户生产端判定“对生产环境有较大风险”'),
    ('高', '客户分子团队持续减员',
     '2026-09-01 记录：研发吴春成、张茜茜离职，分子团队持续收缩；客户后续试剂优化工作不多、新项目不再新开 → 增量天花板已现'),
    ('中', '逾期应收 8.55 万，最长 112 天',
     '16 张应收单据中 12 张逾期；ZHBR-AR260408014 已逾期 112 天（18,254 元）。客户承诺本月至少到账 64,218 元'),
    ('中', '库存 E16 存量较大，短期无大量复购',
     '2026-07-16 记录：客户可查 E16 库存货值 9300 万+（客户口径，待核），需等用尽或报废后才有新采购'),
    ('中', 'M2181-4-NA 调价诉求未满足',
     '2026-08-04 记录：客户希望调价，当前未同意，仍按原价执行 → 存在转向竞品的价格触点'),
    ('低', '客户特殊要求密集（24 条已生效）',
     '以有效期 5/6、指定分装与批次、加急时点为主 → 交付与仓库排产需前置核对，避免再次客诉'),
]

NEXT = [
    ('闭环 FMD5104-000829 人源污染客诉', '出具新批次 RnaseP 出库检报告 + 退货最终方案（含时间点）', '赵云浩 / 技术支持（李锋）', '5 个工作日内'),
    ('拭子保存液送测', '旧版本测试包节后寄出，同步确认测试方案（灭活+裂解+RNA/DNA 保存+直扩兼容）与判据', '赵云浩 / 产品部', '10 月下旬启动，2 周内出首轮结果'),
    ('RT 预混 10 周测试收口', '跟进 M2274-088901 / MD2104-092401 第 8–10 周数据，锁定替换的首个新项目与首单量', '赵云浩', '10 月下旬'),
    ('10 月回款落地', '确认 64,218 元到账；对逾期 112 天单据（ZHBR-AR260408014）出专项清收与账期计划', '赵云浩 / 销售支持', '10 月底前'),
    ('冻干程序验证回收', '40–48h 新程序在客户产线的验证结果；确认收冻干时间落到工作日', '赵云浩 / 冻干服务', '10 月下旬'),
    ('可冻干 HPV 熔曲-CZT 推进', '订单签订中 → 技术确认项逐条闭环并推动下单', '赵云浩', '10 月内'),
    ('HPV 居家 POCT 释放剂方案', '针对宫颈拭子全血释放剂的浓度痛点给候选方案 + 冻干服务正式报价', '李江辉 / 产品部', '11 月上旬'),
    ('新批次货期确认', 'FMD5104-000829 系列内部物料重新采购后的确定货期回传客户', '赵云浩 / 供应链', '本周内'),
]

QUESTIONS = [
    ('质量与交付', [
        'FMD5104-000829 新批次的 RnaseP 出库检报告是否满足要求？客户生产端还有起线异常吗？',
        '9 月新订单（000829 系列）内部物料需重新采购，客户可接受的到货时点是什么时候？',
        '已闭环的两条客诉（E31-NA 起线、MD2084-020301 标签）客户是否还有遗留诉求？',
        '24 条特殊要求（有效期 5/6、指定分装/批次、加急时点）在客户最新产线节奏下是否需要更新？',
    ]),
    ('技术与选型', [
        '拭子保存液：瀚海样品到我司后，测试方案与判据如何定义？（灭活、裂解细胞/病毒、直扩可用、RNA/DNA 保存）',
        '保存液的验收标准具体是什么：与瀚海对比的 Ct 差值、稳定性天数、还是成本目标？',
        '088901 / 092401 第 8–10 周结果如何？10 周通过后的首个切换项目是哪个、首单量多少？',
        '冻干新程序（40–48h）产线验证通过了吗？收冻干时间是否已避开凌晨？',
        'HPV 居家 POCT 项目：宫颈拭子全血释放剂痛点是否有候选方案？冻干服务是否需要我司出正式方案？',
        '圣湘式滴管的具体规格与月用量？我司推荐的替代方案是否需要打样？',
    ]),
    ('商务与关系', [
        '本月 64,218 元回款的确切到账日？其余货款的内部流程卡在哪一环？',
        '逾期 112 天的 ZHBR-AR260408014（18,254 元）如何结清？能否建立滚动账期计划表？',
        'M2181-4-NA 调价诉求可接受的区间是多少？量价联动方案能否谈？',
        '分子团队减员后采购决策链是否变动？谁接替离职研发的项目？',
        '集团层面（武汉明志医学检验、明德新疆）是否有集中采购或协同机会？',
        '是否需要我司技术支持到现场做体系放大 / 疑难排查？',
    ]),
]


# ─────────────────────────── 计算 ───────────────────────────
def money(v):
    return format(int(round(v)), ',')


def wan(v):
    return '%.2f' % (v / 10000.0)


SUMMARY_ROWS = []


def build():
    css = load_css()
    month_svg = svg_monthly()
    sku_svg = svg_bars([(s[0] + ' ' + s[1], s[4]) for s in SKU26[:10]], '#1a56db', '元')
    ar_svg = svg_bars([(b[0], b[1]) for b in AR['buckets']], '#e07b39', '元')

    t_year = table(
        ['年度', '标准销售订单金额（元）', '占累计比', '订单数'],
        [[y['year'], money(y['amt']), '%.1f%%' % (y['amt'] / TOTAL_AMT * 100), str(y['orders'])] for y in YEARS]
        + [['<b>累计</b>', '<b>%s</b>' % money(TOTAL_AMT), '100.0%', '<b>%d</b>' % TOTAL_ORDERS]],
        numeric=[1, 3], cls='narrow')

    t_month = table(
        ['月份'] + [m[0] for m in MONTHS] + ['1–10 月小计'],
        [['2024 年'] + [money(m[1]) for m in MONTHS] + [money(sum(m[1] for m in MONTHS[:10]))],
         ['2025 年'] + [money(m[2]) for m in MONTHS] + [money(YTD25)],
         ['2026 年'] + [(money(m[3]) if m[3] else '—') for m in MONTHS] + [money(YTD26)]],
        numeric=list(range(1, 14)))
    t_month = t_month.replace('<table', '<table class="wide"', 1)

    t_sku = table(
        ['货号', '物料名称', '数量', '单位', '含税金额（元）', '占 2026 比'],
        [[s[0], s[1], ('%.0f' % s[2]), s[3], money(s[4]), '%.1f%%' % (s[4] / YTD26 * 100)] for s in SKU26],
        numeric=[2, 4])

    t_hist = table(
        ['货号', '物料名称', '2024–2026H1 累计（元）'],
        [[h[0], h[1], money(h[2])] for h in HIST_SKU], numeric=[2], cls='narrow')

    t_ar = table(
        ['账龄分档', '金额（元）', '占应收比'],
        [[b[0], money(b[1]), '%.1f%%' % (b[1] / AR['ar'] * 100)] for b in AR['buckets']]
        + [['<b>应收合计</b>', '<b>%s</b>' % money(AR['ar']), '100.0%'],
           ['<b>其中逾期</b>', '<b>%s</b>' % money(AR['overdue']), '<b>%.1f%%</b>' % AR_OVERDUE_PCT]],
        numeric=[1], cls='narrow')

    t_cmp = table(
        ['对手 / 相关方', '价格或产品口径', '当前占位', '我司打法'],
        [[c[0], c[1], c[2], c[3]] for c in COMPETITORS])

    t_complaint = table(
        ['提起日', '客诉单', '紧急度', '状态', '时效', '要点'],
        [[c[0], c[1], c[2], c[3], c[4], c[5]] for c in COMPLAINTS])

    t_opp = table(
        ['商机', '阶段', '创建人', '飞书编号'],
        [[o[0], o[1], '赵云浩', o[2]] for o in OPPS], cls='narrow')

    t_proj = table(
        ['项目', '状态', '负责人', '飞书编号'],
        [[p[0], p[1], p[2], p[3]] for p in PROJECTS], cls='narrow')

    t_spec = table(
        ['类别', '典型要求', '条数'],
        [[s[0], s[1], str(s[2])] for s in SPECIAL_REQ], numeric=[2], cls='narrow')

    t_risk = table(
        ['等级', '风险 / 阻力', '依据'], [[r[0], r[1], r[2]] for r in RISKS])

    t_next = table(
        ['动作', '交付物 / 判据', '主责（协同）', '时间'],
        [[n[0], n[1], n[2], n[3]] for n in NEXT])

    q_blocks = ''
    for title, qs in QUESTIONS:
        lis = '\n'.join('<li>%s</li>' % q for q in qs)
        q_blocks += '<div class="qcard"><h4>%s</h4><ul>%s</ul></div>\n' % (title, lis)

    tpl = load_tpl()
    html = tpl
    repl = {
        '@@CSS@@': css,
        '@@NOW@@': NOW,
        '@@TODAY@@': TODAY,
        '@@TOTAL_WAN@@': wan(TOTAL_AMT),
        '@@TOTAL_AMT@@': money(TOTAL_AMT),
        '@@TOTAL_ORDERS@@': str(TOTAL_ORDERS),
        '@@YTD26@@': money(YTD26),
        '@@YTD26_WAN@@': wan(YTD26),
        '@@YTD25@@': money(YTD25),
        '@@YOY@@': '%+.1f%%' % YOY,
        '@@YOY_CLS@@': 'up' if YOY >= 0 else 'down',
        '@@AR@@': money(AR['ar']),
        '@@AR_WAN@@': wan(AR['ar']),
        '@@OVERDUE@@': money(AR['overdue']),
        '@@OVERDUE_WAN@@': wan(AR['overdue']),
        '@@OVERDUE_PCT@@': '%.1f%%' % AR_OVERDUE_PCT,
        '@@MAXDAYS@@': str(AR['max_days']),
        '@@N_OD@@': str(AR['n_od']),
        '@@N_AR@@': str(AR['n']),
        '@@MONTH_SVG@@': month_svg,
        '@@SKU_SVG@@': sku_svg,
        '@@AR_SVG@@': ar_svg,
        '@@T_YEAR@@': t_year,
        '@@T_MONTH@@': t_month,
        '@@T_SKU@@': t_sku,
        '@@T_HIST@@': t_hist,
        '@@T_AR@@': t_ar,
        '@@T_CMP@@': t_cmp,
        '@@T_COMPLAINT@@': t_complaint,
        '@@T_OPP@@': t_opp,
        '@@T_PROJ@@': t_proj,
        '@@T_SPEC@@': t_spec,
        '@@T_RISK@@': t_risk,
        '@@T_NEXT@@': t_next,
        '@@Q_BLOCKS@@': q_blocks,
        '@@SEP_NOTE@@': '标准销售订单',
    }
    for k, v in repl.items():
        html = html.replace(k, v)
    left = re.findall(r'@@[A-Z_0-9]+@@', html)
    assert not left, '残留占位符: %s' % left

    html = BK.inject(html, theme='dark', tag='营销中心 · 销售管理',
                     footer_extra='内部资料 · 客户访前分析 · 生成于 ' + NOW)
    v = BK.verify(html)
    assert all(v.values()), v
    # 清掉样式注释里的内部路径
    html = re.sub(r'/\*\s*=+\s*bior-brand-v1.*?\*/', '/* bior-brand-v1 · 宝锐品牌样式 */', html, flags=re.S)
    assert 'bior-brand-v1' in html
    assert html.count('</body>') == 1
    return html


def table(headers, rows, numeric=(), cls=''):
    th = ''.join('<th>%s</th>' % h for h in headers)
    body = []
    for r in rows:
        tds = []
        for i, c in enumerate(r):
            if i in numeric:
                tds.append('<td class="n">%s</td>' % c)
            else:
                tds.append('<td>%s</td>' % c)
        body.append('<tr>' + ''.join(tds) + '</tr>')
    return ('<div class="tsw"><table%s><thead><tr>%s</tr></thead><tbody>%s</tbody></table></div>'
            % ((' class="%s"' % cls) if cls else '', th, '\n'.join(body)))


def svg_bars(items, color, unit):
    """横向条形图：标签列 + 轨道 + 值条，值写条外侧；标签不裁切（labelW 按最长标签预留）。"""
    rowH, barH = 32, 16
    labelW, barW = 404, 320
    top, bottom, right = 26, 16, 104
    n = len(items)
    W = labelW + barW + right
    H = top + rowH * n + bottom
    mx = max(v for _, v in items) or 1
    p = ['<svg class="fig" viewBox="0 0 %d %d" role="img" preserveAspectRatio="xMidYMid meet">' % (W, H)]
    for i, (lab, val) in enumerate(items):
        y = top + i * rowH
        cy = y + rowH / 2
        w = max(3, val / mx * barW)
        p.append('<text x="%d" y="%.1f" class="lbl sm" text-anchor="end">%s</text>' % (labelW - 10, cy + 4, esc(lab)))
        p.append('<rect x="%d" y="%.1f" width="%d" height="%d" rx="3" fill="#eef2f8"/>' % (labelW, cy - barH / 2, barW, barH))
        p.append('<rect x="%d" y="%.1f" width="%.1f" height="%d" rx="3" fill="%s"/>' % (labelW, cy - barH / 2, w, barH, color))
        p.append('<text x="%.1f" y="%.1f" class="val" text-anchor="start">%s</text>' % (labelW + w + 8, cy + 4, money(val)))
    p.append('</svg>')
    return '\n'.join(p)


def svg_monthly():
    """分组柱状图：12 个月 × 3 年。"""
    colors = {'2024': '#9db8e0', '2025': '#3f7ad6', '2026': '#e07b39'}
    left, right, top, bottom = 96, 24, 34, 58
    groupW, barW = 64, 16
    W = left + groupW * 12 + right
    H = top + 300 + bottom
    plotH = 300
    mx = max(max(m[1], m[2], m[3]) for m in MONTHS)
    # 取整到 2 万刻度
    step = 20000.0
    mx_axis = 160000.0
    p = ['<svg class="fig" viewBox="0 0 %d %d" role="img" preserveAspectRatio="xMidYMid meet">' % (W, H)]
    y = 0.0
    while y <= mx_axis:
        yy = top + plotH - y / mx_axis * plotH
        p.append('<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#e8eef7" stroke-width="1"/>' % (left, yy, left + groupW * 12, yy))
        p.append('<text x="%d" y="%.1f" class="tick" text-anchor="end">%s</text>' % (left - 8, yy + 4, format(int(y), ',')))
        y += step
    for i, (mo, v24, v25, v26) in enumerate(MONTHS):
        gx = left + i * groupW
        for j, (kk, vv) in enumerate([('2024', v24), ('2025', v25), ('2026', v26)]):
            if vv <= 0:
                continue
            bh = max(2, vv / mx_axis * plotH)
            bx = gx + 6 + j * (barW + 2)
            by = top + plotH - bh
            p.append('<rect x="%.1f" y="%.1f" width="%d" height="%.1f" rx="2" fill="%s"/>' % (bx, by, barW, bh, colors[kk]))
        p.append('<text x="%.1f" y="%.1f" class="xtick" text-anchor="middle">%d月</text>' % (gx + groupW / 2, top + plotH + 20, mo))
    # 图例
    lx = left
    for kk in ['2024', '2025', '2026']:
        p.append('<rect x="%.1f" y="%d" width="14" height="10" rx="2" fill="%s"/>' % (lx, top + plotH + 34, colors[kk]))
        p.append('<text x="%.1f" y="%d" class="lgd">%s 年</text>' % (lx + 20, top + plotH + 43, kk))
        lx += 86
    p.append('<text x="%d" y="18" class="tit">月度销售额（元·标准销售订单含税）</text>' % left)
    p.append('</svg>')
    return '\n'.join(p)


def esc(s):
    return (s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
            .replace('"', '&quot;').replace("'", '&#39;'))


def load_css():
    return CSS


def load_tpl():
    return TPL


CSS = """
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0;background:#f4f7fb;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#16233a;line-height:1.66}
main{max-width:1180px;margin:0 auto;padding:20px 18px 48px}
h1{font-size:26px;margin:14px 0 6px;letter-spacing:.5px}
h2{font-size:18px;margin:0 0 12px;display:flex;align-items:center;gap:8px}
h2 .num{display:inline-flex;width:22px;height:22px;border-radius:6px;background:#1a56db;color:#fff;font-size:13px;align-items:center;justify-content:center}
h3{font-size:15px;margin:18px 0 8px;color:#0f2b52}
h4{font-size:14px;margin:0 0 8px;color:#1a56db}
.sub{color:#5b6d8c;font-size:13px;margin:0 0 14px}
.card{background:#fff;border-radius:12px;padding:18px 20px;margin:0 0 16px;box-shadow:0 1px 3px rgba(16,42,86,.08)}
.note{background:#f7faff;border-left:4px solid #1a56db;border-radius:8px;padding:12px 16px;font-size:13px;color:#3d5375;margin:0 0 16px}
.note b{color:#0f2b52}
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:0 0 16px}
.kpi{background:#fff;border-radius:12px;padding:14px 16px;box-shadow:0 1px 3px rgba(16,42,86,.08);border-top:3px solid #1a56db}
.kpi.o{border-top-color:#e07b39}.kpi.r{border-top-color:#d64545}.kpi.g{border-top-color:#1f9d6a}
.kpi .n{font-size:22px;font-weight:700;color:#0f2b52;letter-spacing:.3px}
.kpi .l{font-size:12px;color:#6b7c99;margin-top:2px}
.kpi .x{font-size:11px;color:#8c9ab5;margin-top:4px}
.up{color:#1f9d6a;font-weight:700}.down{color:#d64545;font-weight:700}
table{border-collapse:collapse;width:100%;font-size:12.5px}
.wide{min-width:1080px}
.tw{max-height:520px;overflow:auto;border:1px solid #e6edf7;border-radius:8px}
.tsw{overflow-x:auto;-webkit-overflow-scrolling:touch}
thead th{position:sticky;top:0;background:#f0f5fc;color:#274264;font-weight:600;text-align:left;padding:8px 10px;border-bottom:1px solid #dbe6f4;white-space:nowrap}
tbody td{padding:7px 10px;border-bottom:1px solid #f0f4fa;vertical-align:top}
tbody tr:nth-child(even){background:#fbfcfe}
td.n{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
tbody tr:hover{background:#f4f8ff}
.desc{font-size:13px;color:#43587a;margin:0 0 8px}
.fig{width:100%;height:auto;display:block}
.fig text{font-size:11px;fill:#5b6d8c}
.fig text.tit{font-size:12px;fill:#274264;font-weight:600}
.fig text.val{font-size:11px;fill:#274264;font-weight:600}
.fig text.lbl{font-size:11px;fill:#43587a}
.fig text.lbl.sm{font-size:10px}
.narrow{max-width:820px}
.fig text.tick,.fig text.xtick{font-size:10px;fill:#8c9ab5}
.qgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.qcard{background:#f8fbff;border:1px solid #e2ecf9;border-radius:10px;padding:12px 14px}
.qcard ul{margin:0;padding-left:18px}
.qcard li{margin:0 0 7px;font-size:12.5px;color:#31465f}
ul.plain{margin:6px 0 0;padding-left:18px}
ul.plain li{margin:0 0 6px;font-size:13px;color:#3d5375}
.tag{display:inline-block;padding:1px 7px;border-radius:20px;font-size:11px;font-weight:600}
.tag.hi{background:#fde8e8;color:#c0392b}.tag.mid{background:#fdf3e2;color:#b06a10}.tag.lo{background:#e9f6ef;color:#1f7a55}
.foot-note{font-size:12px;color:#7b8aa6;margin:10px 0 0}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media (max-width:960px){
  .kpis{grid-template-columns:repeat(2,1fr)}
  .qgrid,.grid2{grid-template-columns:1fr}
  h1{font-size:21px}
  main{padding:16px 12px 40px}
}
@media (max-width:760px){
  .bior-header .bior-header-inner{flex-wrap:wrap;padding:8px 12px;gap:8px}
  .bior-header .bior-name{font-size:13px;letter-spacing:.3px}
  .bior-header .bior-cn{font-size:10px}
  .bior-header .bior-slogan{font-size:10px;letter-spacing:0}
  .bior-header .bior-logo-chip img{height:22px}
  .kpis{grid-template-columns:1fr}
}
"""

TPL = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>访前分析简报 · 武汉明德生物科技股份有限公司 · @@TODAY@@</title>
<style>@@CSS@@</style>
</head>
<body>
<main>

<h1>访前分析简报 · 武汉明德生物科技股份有限公司</h1>
<p class="sub">客户级别：大客户 · A 类 ｜ 归属：赵云浩（诊断原料大客户销售部）｜ 集团：明德集团 ｜ 生成日：@@TODAY@@</p>

<div class="note">
<b>口径说明：</b>金额口径 = <b>ERP「标准销售订单」含税价税合计</b>（已剔除预测单与测试销售订单，测试单另行标注）；
2026 年数据区间 <b>2026-01-01 ~ 2026-10-15</b>；2024/2025 为全年。客户 360 累计口径含测试单，故单数大于本页标准订单单数。
应收口径 = 2026 年 10 月账龄表（含未到期与逾期）。活动、客诉、商机、联系人取自飞书同步数据。
</div>

<div class="kpis">
  <div class="kpi"><div class="n">@@TOTAL_WAN@@ 万</div><div class="l">2024–2026.10 累计采购</div><div class="x">标准订单 @@TOTAL_ORDERS@@ 单</div></div>
  <div class="kpi g"><div class="n">@@YTD26_WAN@@ 万</div><div class="l">2026 年 1–10 月（至 10/15）</div><div class="x">2025 年同期 @@YTD25@@ 元 · 同比 <span class="@@YOY_CLS@@">@@YOY@@</span></div></div>
  <div class="kpi o"><div class="n">@@OVERDUE_WAN@@ 万</div><div class="l">逾期应收（占应收 @@OVERDUE_PCT@@）</div><div class="x">应收 @@AR_WAN@@ 万 ｜ @@N_OD@@/@@N_AR@@ 单逾期 ｜ 最长 @@MAXDAYS@@ 天</div></div>
  <div class="kpi r"><div class="n">1</div><div class="l">未闭环客诉</div><div class="x">FMD5104-000829 人源污染</div></div>
</div>

<div class="card">
<h2><span class="num">1</span>客户概览与规模</h2>
<div class="tsw"><table><thead><tr><th>维度</th><th>内容</th></tr></thead><tbody>
<tr><td>客户全称</td><td>武汉明德生物科技股份有限公司</td></tr>
<tr><td>集团归属</td><td>明德集团 — 武汉明德生物科技股份有限公司（累计 140.52 万）｜ 武汉明志医学检验实验室有限公司（0.30 万）｜ 明德新疆生物科技有限公司（ERP 无采购记录）</td></tr>
<tr><td>客户级别 / 归属</td><td>大客户 · A 类 ｜ 销售员：赵云浩 ｜ 部门：诊断原料大客户销售部</td></tr>
<tr><td>决策链规模</td><td>飞书客户-联系人关联 <b>50 人</b>（含研发、生产、采购、质量多角色）</td></tr>
<tr><td>项目 / 商机</td><td>在手商机 7 个（1 个「订单签订中」）｜ 客户项目 1 个（呼六体系筛选，待结题评审，负责人李锋）</td></tr>
<tr><td>在手订单（近期）</td><td>ZHBR261013011（10/13 · 342 元 · 无物流）｜ ZHBR260917008（9/17 · 32,000 元）｜ ZHBR260920017（9/20 · 3,800 元 · 已签收）｜ 测试单 2 张（9/24 · 汪豪英收）</td></tr>
<tr><td>客户特殊要求</td><td>已生效 <b>24 条</b>（详见第 7 节）｜ 最大特点：有效期须 &gt; 总效期 5/6、指定分装与批次、加急时点发货</td></tr>
</tbody></table></div>
</div>

<div class="card">
<h2><span class="num">2</span>采购画像：年度、月度与货号结构</h2>
@@T_YEAR@@
<h3>月度销售额对照（2024 / 2025 / 2026）</h3>
@@MONTH_SVG@@
<p class="desc">2026 年 3 月空档、9 月单月 14.32 万创近三年单月新高；2025 年 11–12 月有 29.3 万的年终集中交付，全年口径仍需靠 Q4 追赶。</p>
<div class="tw">@@T_MONTH@@</div>
<h3>2026 年主力货号（标准订单）</h3>
@@SKU_SVG@@
<p class="desc">结构特征：<b>单酶 + 预混 mix 并行</b>，2026 年增量主要由 Taq 单酶（FE40/FE31/FE16）与呼吸道直接法体系（FMD5104-000829 系列）拉动，冻干保护剂 AS312-000901-T2 同步上量（1,269.7 ml），说明客户已进入冻干剂型验证阶段。</p>
@@T_SKU@@
<h3>历史主销货号（2024–2026H1 累计）</h3>
@@T_HIST@@
</div>

<div class="card">
<h2><span class="num">3</span>关系与进度</h2>
<h3>最近跟进（赵云浩为主责，李江辉支持 HPV 恒温扩增线）</h3>
<ul class="plain">
<li><b>2026-10-13</b> 新采购需求订单已提起，合同盖章回传；回款订单已确定。</li>
<li><b>2026-10-12</b> 客户明确本月至少 64,218 元货款到账；同步提出<b>拭子保存液</b>需求（灭活、裂解细胞与病毒、直扩可用、同时保存 RNA/DNA），瀚海说明书已给到我司，客户近期测试。</li>
<li><b>2026-09-30</b> 甲乙新项目测试 FMD6104-020322-6P 不如 FMD5084-000829 → 项目锁定 <b>829 体系</b>，已形成生产采购订单。</li>
<li><b>2026-09-24</b> 三个报证项目（呼吸道/STD/基孔）中呼吸道全部用我司体系；发现内标 RnaseP 污染风险，新批次全部做出库检。088901/092401 测试进行到第 7 周（共 10 周）。</li>
<li><b>2026-09-17 / 09-20</b> 000829 人源污染客诉提起；9 月订单与前期两单在中秋最后一天合并发出，全部加控人源 RnaseP。</li>
<li><b>2026-08-31 / 09-01</b> M2274-088901 初步结果与对照（诺唯赞、瀚海）差异不大；客户要求测够 10 次才采购。</li>
<li><b>2026-07-27</b>（李江辉）HPV 居家 POCT 项目：宫颈拭子取样 + 核酸释放剂稀释 + 恒温扩增 + 客户 POCT 平台；已采购我司 Bst3.0 HS、RNase HII；痛点卡在<b>释放剂浓度</b>。</li>
</ul>
<h3>在手商机（7 个）</h3>
@@T_OPP@@
<h3>客户项目</h3>
@@T_PROJ@@
</div>

<div class="card">
<h2><span class="num">4</span>产品结构机会：下一步卖什么</h2>
<div class="tsw"><table><thead><tr><th>机会</th><th>依据（客户原话 / 实测）</th><th>预估节奏</th></tr></thead><tbody>
<tr><td><b>拭子保存液</b>（最大增量）</td><td>客户用量 2 ml/T，终端推广阶段，上量后约 <b>10 万人份/月</b>；现用瀚海 0.8–1 元/ml，客户认为成本略高、希望我司送测</td><td>旧版本节后送测；新版本对标瀚海成本</td></tr>
<tr><td><b>基孔项目替换诺唯赞</b></td><td>客户基孔拿证早、市场广、出货量最大，单月最高采购十几万（现为诺唯赞）；我司 088901/092401 已测 7/10 周</td><td>10 周通过后启动替换，首单从新项目切入</td></tr>
<tr><td><b>STD 项目放量</b></td><td>STD 项目 9 月起做临床，预计 2027 年 9 月完成；现稳定使用我司试剂，每月环比 +1,000 人份左右</td><td>临床期持续爬坡</td></tr>
<tr><td><b>冻干剂型</b></td><td>呼吸道冻干项目原 30h 程序不匹配产线（凌晨收冻干），我司已给 40–48h 新程序；冻干保护剂 AS312-000901-T2 2026 年已用 1,269.7 ml</td><td>程序验证结果回收 + CZT 商机下单</td></tr>
<tr><td><b>HPV 居家 POCT 释放剂</b></td><td>客户恒温扩增用我司 Bst3.0 HS + RNase HII，2 步法；卡点=宫颈拭子全血释放剂浓度（要求可接受低 100–1,000 倍初始浓度）</td><td>候选方案 + 冻干报价，11 月上旬</td></tr>
<tr><td><b>耗材“带件”</b></td><td>客户需要类似圣湘的滴管（圣湘有专利，市场不易找），我司有可推荐产品</td><td>随批推荐，低门槛切入点</td></tr>
</tbody></table></div>
</div>

<div class="card">
<h2><span class="num">5</span>竞争格局与打法</h2>
@@T_CMP@@
<p class="desc">价格锚点：RT 预混 50μl 体系 —— 诺唯赞 0.6–0.7 元/T，瀚海 0.8–0.9 元/T。客户已在用我司 mix，<b>对瀚海的价格高于诺唯赞这一点有明确感知</b>，因此“置换瀚海”的阻力小于“置换诺唯赞”；基孔项目是唯一被诺唯赞牢牢占住的口子。</p>
</div>

<div class="card">
<h2><span class="num">6</span>风险与阻力</h2>
@@T_RISK@@
<h3>客诉台账（3 条，1 条未闭环）</h3>
@@T_COMPLAINT@@
<h3>应收账款结构（2026 年 10 月）</h3>
@@AR_SVG@@
@@T_AR@@
<p class="desc">逾期 8.55 万占应收 50.3%，12/16 张单据已逾期；最大逾期 112 天（ZHBR-AR260408014 · 18,254 元）。资金面是本次访前最需要拿结论的一块：客户只承诺了 64,218 元的月度到账。</p>
</div>

<div class="card">
<h2><span class="num">7</span>客户特殊要求（24 条已生效 · 交付敏感）</h2>
@@T_SPEC@@
<p class="desc">要求高度集中在<b>交付与包装</b>：有效期必须大于总效期 5/6、指定分装规格（100T/包、300T/包、5ml/支、每产品各装一管）、指定或排除批次、指定时点发出。这些是历史客诉的高发区，本次拜访建议逐条与客户确认是否仍然有效。</p>
</div>

<div class="card">
<h2><span class="num">8</span>下一步动作（主责唯一 · 验收到人）</h2>
@@T_NEXT@@
</div>

<div class="card">
<h2><span class="num">9</span>该问客户什么（访前提问清单）</h2>
<div class="qgrid">
@@Q_BLOCKS@@
</div>
</div>

<p class="foot-note">数据源（均为本地已授权数据，现算不估算）：ERP 标准销售订单导出（2024 全年 / 2025 全年 / 2026-01-01 至 2026-10-15）｜ 客户 360 主数据（飞书活动、商机、客户项目、最近订单）｜ 客户价格与货号档案（ERP 口径）｜ 2026 年 10 月应收账款账龄表（大客户部明细 + 主力客户汇总）｜ 客诉台账（飞书售后管理同步）｜ 飞书活动明细导出 ｜ 客户特殊要求库（ERP 订单备注提取）。生成于 @@NOW@@。</p>

</main>
</body>
</html>
"""


if __name__ == '__main__':
    html = build()
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    with open(OUT, 'w', encoding='utf-8') as f:
        f.write(html)
    print('OK bytes=%d -> %s' % (len(html), OUT))
