#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""2026年10月 应收账款账龄统计（合并版：大客户部 + 销售拓展部）→ 自包含 HTML

输入：数据/2026年10月应收账龄_大客户部.json、数据/2026年10月应收账龄_销售拓展部.json
输出：销售分析/2026年10月应收账款账龄统计_诊断原料全部门.html
"""
import html as H
import io
import json
import os
import re
import sys

sys.path.insert(0, '/Users/liuxinyuan/Desktop/Hermes输出-工作类/脚本')
import brand_kit as BK  # noqa: E402

W = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
D1 = json.load(io.open(W + '/数据/2026年10月应收账龄_大客户部.json', encoding='utf-8'))
D2 = json.load(io.open(W + '/数据/2026年10月应收账龄_销售拓展部.json', encoding='utf-8'))
B = D1['buckets']
COLORS = {'账期内': '#3b82f6', '逾期1-30天': '#22c55e', '逾期31-60天': '#f59e0b',
          '逾期61-365天': '#ef4444', '逾期365天以上（一年以上）': '#7f1d1d'}
SHORT = {'账期内': '账期内', '逾期1-30天': '逾期1-30天', '逾期31-60天': '逾期31-60天',
         '逾期61-365天': '逾期61-365天', '逾期365天以上（一年以上）': '逾期365天以上（一年以上）'}
money = lambda x, d=2: format(x, ',.%df' % d)
pct = lambda x: '%.1f%%' % x
wan = lambda x: x / 10000.0

DEPTS = [('诊断原料大客户销售部', '大客户部', D1), ('诊断原料销售拓展部', '销售拓展部', D2)]


def merge(a, b, key):
    return round(a[key] + b[key], 2)


TOT = merge(D1, D2, 'total')
OV = merge(D1, D2, 'overdue_total')
ND = merge(D1, D2, 'notdue_total')
N = D1['rows'] + D2['rows']
overall = {}
for k in B:
    overall[k] = {'单数': D1['overall'][k]['单数'] + D2['overall'][k]['单数'],
                  '金额': round(D1['overall'][k]['金额'] + D2['overall'][k]['金额'], 2),
                  '客户数': len({r['cust'] for r in D1['detail'] + D2['detail'] if r['bucket'] == k})}
o60v = round(sum(overall[k]['金额'] for k in B[3:]), 2)
o60n = sum(overall[k]['单数'] for k in B[3:])
y1 = overall['逾期365天以上（一年以上）']
LED = json.load(io.open(W + '/数据/2026年10月应收客户账龄_主力客户.json', encoding='utf-8'))
CU = LED['customers']
tot_rev = LED['meta']['tot_rev']

# ── 七、逾期主力客户（按部门）──
def cust_rows(items, dept_ov, n=8):
    out = []
    for o in items[:n]:
        o60 = sum(o['buckets'][b] for b in B[3:])
        rev = o['rev2026']
        exp = ('%.1f%%' % o['exposure']) if o['exposure'] is not None else '零营收'
        cls = 'high' if (o['exposure'] is not None and o['exposure'] >= 30) or rev <= 0 else ''
        out.append('<tr%s><td class="b">%s</td><td>%s</td><td class="n">%s</td><td class="n">%s</td>'
                   '<td class="n">%s</td><td class="n">%d</td><td class="n">%s</td><td class="n %s">%s</td></tr>'
                   % ((' class="high"' if cls else ''), H.escape(o['cust']), H.escape(o['sp']),
                      money(wan(o['overdue']), 1), pct(o['overdue'] / dept_ov * 100 if dept_ov else 0),
                      money(wan(o60), 1) if o60 else '—', o['max_days'],
                      money(wan(rev), 1) if rev else '0', cls, exp))
    return ''.join(out)

sec7 = []
for fname, short, D in DEPTS:
    items = sorted([o for o in CU if o['dept'] == short], key=lambda o: -o['overdue'])[:8]
    dov = D['overdue_total']
    sec7.append('<h3>%s ｜ 逾期合计 <b style="color:#b91c1c">%s 万</b>（%d 家）</h3>'
                '<table><thead><tr><th>客户</th><th>销售员</th><th style="text-align:right">逾期(万)</th>'
                '<th style="text-align:right">占部门逾期</th><th style="text-align:right">其中&gt;60天(万)</th>'
                '<th style="text-align:right">最长天数</th><th style="text-align:right">本年营收(万)</th>'
                '<th style="text-align:right">逾期/营收</th></tr></thead><tbody>%s</tbody></table>'
                % (H.escape(short), money(wan(dov), 1), sum(1 for o in CU if o['dept'] == short and o['overdue'] > 0),
                   cust_rows(items, dov, 99)))
sec7 = ''.join(sec7)

# ── 八、逾期主力客户（按销售员）──
sec8 = []
for sp, v in sorted(LED['by_sp'].items(), key=lambda kv: -kv[1]['overdue']):
    tops = sorted([o for o in CU if o['sp'] == sp and o['overdue'] > 0], key=lambda o: -o['overdue'])[:3]
    lst = '；'.join('%s <b>%s万</b>' % (H.escape(o['cust']), money(wan(o['overdue']), 1)) for o in tops) or '—'
    sec8.append('<tr><td class="b">%s</td><td>%s</td><td class="n">%s</td><td class="n">%s</td><td>%s</td></tr>'
                % (H.escape(sp), H.escape(v['dept']), money(wan(v['overdue']), 1), money(wan(v['rev']), 1), lst))
sec8 = ''.join(sec8)

# ── 九、营收主力客户（按部门）──
def rev_rows(items, dept_rev, n=8):
    out = []
    for o in items[:n]:
        exp = ('%.1f%%' % o['exposure']) if o['exposure'] is not None else '—'
        cls = 'high' if (o['exposure'] is not None and o['exposure'] >= 30) else ''
        out.append('<tr%s><td class="b">%s</td><td>%s</td><td class="n">%s</td><td class="n">%s</td>'
                   '<td class="n">%s</td><td class="n">%s</td><td class="n %s">%s</td></tr>'
                   % ((' class="high"' if cls else ''), H.escape(o['cust']), H.escape(o['sp']),
                      money(wan(o['rev2026']), 1), pct(o['rev2026'] / dept_rev * 100 if dept_rev else 0),
                      money(wan(o['ar']), 1), money(wan(o['overdue']), 1) if o['overdue'] else '—', cls, exp))
    return ''.join(out)

sec9 = []
for fname, short, D in DEPTS:
    items = sorted([o for o in CU if o['dept'] == short], key=lambda o: -o['rev2026'])[:8]
    drev = sum(o['rev2026'] for o in CU if o['dept'] == short)
    sec9.append('<h3>%s ｜ 本年营收合计 <b>%s 万</b>（%d 家）</h3>'
                '<table><thead><tr><th>客户</th><th>销售员</th><th style="text-align:right">2026营收(万)</th>'
                '<th style="text-align:right">占部门营收</th><th style="text-align:right">应收(万)</th>'
                '<th style="text-align:right">逾期(万)</th><th style="text-align:right">逾期/营收</th></tr></thead>'
                '<tbody>%s</tbody></table>'
                % (H.escape(short), money(wan(drev), 1), sum(1 for o in CU if o['dept'] == short and o['rev2026'] > 0),
                   rev_rows(items, drev, 99)))
sec9 = ''.join(sec9)

# ── 十、营收主力客户（按销售员）──
sec10 = []
for sp, v in sorted(LED['by_sp'].items(), key=lambda kv: -kv[1]['rev']):
    tops = sorted([o for o in CU if o['sp'] == sp and o['rev2026'] > 0], key=lambda o: -o['rev2026'])[:3]
    lst = '；'.join('%s <b>%s万</b>' % (H.escape(o['cust']), money(wan(o['rev2026']), 1)) for o in tops) or '—'
    exp = ('%.1f%%' % v['exposure']) if v['exposure'] is not None else '—'
    cls = 'high' if (v['exposure'] is not None and v['exposure'] >= 30) else ''
    sec10.append('<tr><td class="b">%s</td><td>%s</td><td class="n">%s</td><td class="n">%s</td>'
                 '<td class="n">%s</td><td class="n %s">%s</td><td>%s</td></tr>'
                 % (H.escape(sp), H.escape(v['dept']), money(wan(v['rev']), 1), money(wan(v['ar']), 1),
                    money(wan(v['overdue']), 1), cls, exp, lst))
sec10 = ''.join(sec10)

# ── 十一、风险客户 ──
zero = sorted([o for o in CU if not o['matched'] and o['overdue'] > 0], key=lambda o: -o['overdue'])
hi = sorted([o for o in CU if o['exposure'] is not None and o['exposure'] >= 30], key=lambda o: -o['overdue'])
risk_rows = ''.join('<tr class="high"><td class="b">%s</td><td>%s</td><td>%s</td><td class="n">%s</td>'
                    '<td class="n">%d</td><td>%s</td></tr>'
                    % (H.escape(o['cust']), H.escape(o['dept']), H.escape(o['sp']), money(wan(o['overdue']), 1),
                       o['max_days'], '本年营收 0（无订单）') for o in zero)
risk_rows += ''.join('<tr class="high"><td class="b">%s</td><td>%s</td><td>%s</td><td class="n">%s</td>'
                     '<td class="n">%d</td><td>逾期/营收 <b>%.1f%%</b></td></tr>'
                     % (H.escape(o['cust']), H.escape(o['dept']), H.escape(o['sp']), money(wan(o['overdue']), 1),
                        o['max_days'], o['exposure']) for o in hi[:8])
sec11 = risk_rows

ALL = D1['detail'] + D2['detail']
od_rows = [r for r in ALL if r['overdue']]
mx = max(od_rows, key=lambda r: r['od_days'] or 0)

# ── 堆叠条 ──
WID, HEI = 1100, 82
x = 0.0
segs = []
for k in B:
    w = overall[k]['金额'] / TOT * WID
    segs.append('<rect x="%.2f" y="18" width="%.2f" height="36" fill="%s"></rect>' % (x, w, COLORS[k]))
    if w > 62:
        segs.append('<text x="%.2f" y="41" font-size="12.5" fill="#fff" text-anchor="middle" font-weight="700">%s</text>'
                    % (x + w / 2, pct(overall[k]['金额'] / TOT * 100)))
        segs.append('<text x="%.2f" y="68" font-size="11" fill="#5f6368" text-anchor="middle">%s · %s万</text>'
                    % (x + w / 2, H.escape(k.replace('（一年以上）', '>1年').replace('逾期', '逾')), money(wan(overall[k]['金额']), 1)))
    x += w
svg = '<svg viewBox="0 0 %d %d" width="100%%" height="%d" role="img" aria-label="账龄结构">%s</svg>' % (WID, HEI, HEI, ''.join(segs))

legend = ''.join('<div style="display:flex;align-items:center;gap:7px;font-size:12.5px">'
                 '<span style="width:12px;height:12px;border-radius:3px;background:%s;display:inline-block"></span>'
                 '<b>%s</b><span style="color:#6b7280">%s 元</span><span style="color:#9aa3b2">%s ｜ %d 单</span></div>'
                 % (COLORS[k], H.escape(k), money(overall[k]['金额']), pct(overall[k]['金额'] / TOT * 100), overall[k]['单数']) for k in B)

# ── 表1：分账龄（合计）──
t1 = []
for k in B:
    v = overall[k]
    bar = ('<div style="background:#eef2f7;border-radius:3px;height:13px"><div style="width:%.1f%%;background:%s;height:13px;border-radius:3px"></div></div>'
           % (v['金额'] / TOT * 100, COLORS[k]))
    t1.append('<tr><td class="b">%s</td><td class="n">%d</td><td class="n">%s</td><td class="n">%s</td>'
              '<td class="n">%s</td><td style="min-width:110px">%s</td></tr>'
              % (H.escape(k), v['单数'], money(v['金额']), pct(v['金额'] / TOT * 100),
                 (pct(v['金额'] / OV * 100) if k != '账期内' else '—'), bar))
t1.append('<tr class="sub"><td class="b">〔逾期60天以上 小计〕</td><td class="n">%d</td><td class="n">%s</td>'
          '<td class="n">%s</td><td class="n">%s</td><td></td></tr>'
          % (o60n, money(o60v), pct(o60v / TOT * 100), pct(o60v / OV * 100)))
t1.append('<tr class="tot"><td class="b">合计（两部门）</td><td class="n">%d</td><td class="n">%s</td>'
          '<td class="n">100.0%%</td><td class="n">—</td><td></td></tr>' % (N, money(TOT)))

# ── 表2：分部门 × 账龄 ──
t2 = []
for fname, short, D in DEPTS:
    tt = D['total']
    cells = ''
    for k in B:
        cells += '<td class="n">%s<br><small>%s ｜ %d 单</small></td>' % (
            money(wan(D['overall'][k]['金额']), 1), pct(D['overall'][k]['金额'] / tt * 100), D['overall'][k]['单数'])
    t2.append('<tr><td class="b">%s<br><small>%s</small></td>%s<td class="n b">%s<br><small>100%%</small></td>'
              '<td class="n" style="color:#b91c1c">%s</td><td class="n">%s</td></tr>'
              % (H.escape(short), H.escape(fname), cells, money(wan(tt), 1),
                 money(wan(D['over60_subtotal']['金额']), 1), money(wan(D['overdue_total']), 1)))
# 合计行
cells = ''.join('<td class="n b">%s</td>' % money(wan(overall[k]['金额']), 1) for k in B)
t2.append('<tr class="tot"><td class="b">两部门合计</td>%s<td class="n b">%s</td><td class="n b" style="color:#b91c1c">%s</td>'
          '<td class="n b">%s</td></tr>' % (cells, money(wan(TOT), 1), money(wan(o60v), 1), money(wan(OV), 1)))

# ── 表3：分销售员 × 账龄（两部门）──
t3 = []
for fname, short, D in DEPTS:
    rows = sorted(D['by_sp'].items(), key=lambda kv: -kv[1]['逾期合计'])
    for sp, v in rows:
        bb = v['buckets']
        cells = ''.join('<td class="n">%s</td>' % (money(wan(bb[k]['金额']), 1) if bb[k]['金额'] else '—') for k in B)
        rate = v['逾期合计'] / v['合计'] * 100 if v['合计'] else 0
        t3.append('<tr><td class="b">%s</td><td>%s</td><td class="n">%d</td>%s<td class="n b" style="color:#b91c1c">%s</td>'
                  '<td class="n">%s</td><td class="n">%s</td></tr>'
                  % (H.escape(sp), H.escape(short), v['单数'], cells, money(wan(v['逾期合计']), 1),
                     money(wan(v['合计']), 1), pct(rate)))
t3.append('<tr class="tot"><td class="b">合计</td><td>两部门</td><td class="n">%d</td>%s'
          '<td class="n b" style="color:#b91c1c">%s</td><td class="n b">%s</td><td class="n b">%s</td></tr>'
          % (N, ''.join('<td class="n b">%s</td>' % money(wan(overall[k]['金额']), 1) for k in B),
             money(wan(OV), 1), money(wan(TOT), 1), pct(OV / TOT * 100)))

# ── 勾稽块 ──
cc = []
for fname, short, D in DEPTS:
    for c in sorted(D['crosscheck'], key=lambda c: -c['逾期(本表)']):
        cc.append('<tr><td class="b">%s</td><td>%s</td><td class="n">%s</td><td class="n">%s</td><td class="n ok">%s</td>'
                  '<td class="n">%s</td><td class="n">%s</td><td class="n ok">%s</td></tr>'
                  % (H.escape(c['销售员']), H.escape(short), money(c['逾期(本表)']), money(c['逾期(进度表)']), money(c['差']),
                     money(c['逾期60天以上(本表)']), money(c['逾期60天以上(进度表)']), money(c['差2'])))
cc = ''.join(cc)

# 一年以上 / 0 天 / 最长
y1rows = sorted([r for r in od_rows if (r['od_days'] or 0) > 365], key=lambda r: -(r['od_amt']))
y1tbl = ''.join('<tr><td class="b">%s</td><td class="n">%d</td><td class="n">%s</td><td>%s</td><td>%s</td></tr>'
                % (H.escape(r['cust']), r['od_days'], money(r['od_amt']), H.escape(r['sp']), r['due']) for r in y1rows)
z = [r for r in od_rows if (r['od_days'] or 0) == 0]
zero_txt = ('源表共 <b>%d 单</b>到期日当天（逾期天数=0）但挂逾期金额，合计 <b>%s 元</b>（%s）'
            % (len(z), money(sum(r['od_amt'] for r in z)),
               '、'.join('%s %s' % (r['sp'], money(r['od_amt'], 0)) for r in z))) if z else '本期源表无「逾期天数=0 但挂逾期金额」的行'

HTML = """<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{period}}应收账款账龄统计 · 诊断原料（大客户部 + 销售拓展部）</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#f4f6fa;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#20242c}
.wrap{max-width:1260px;margin:0 auto;padding:18px 20px 44px}
h1{font-size:24px;margin:14px 0 6px}
.meta{color:#6b7280;font-size:12.5px;line-height:1.85;margin-bottom:14px}
.kpis{display:grid;grid-template-columns:repeat(6,1fr);gap:11px;margin:14px 0 18px}
.kpi{background:#fff;border:1px solid #e6eaf2;border-radius:10px;padding:12px 13px}
.kpi .l{font-size:12px;color:#6b7280}.kpi .v{font-size:20px;font-weight:800;margin-top:5px}
.kpi .s{font-size:11px;color:#9aa3b2;margin-top:3px}
.card{background:#fff;border:1px solid #e6eaf2;border-radius:10px;padding:16px 18px;margin-bottom:16px}
.card h2{font-size:15.5px;margin:0 0 12px;padding-left:9px;border-left:3px solid #1a5276}
table{width:100%;border-collapse:collapse;font-size:12.5px}
th{background:#1a5276;color:#fff;font-weight:600;padding:8px 9px;text-align:left;white-space:nowrap}
td{padding:8px 9px;border-bottom:1px solid #eef1f6}
td.n{text-align:right;font-variant-numeric:tabular-nums}
td.b{font-weight:700}
tr.high td{background:#fef2f2}tr.high td.b{color:#b91c1c}
h3{font-size:13.5px;margin:12px 0 7px;color:#1a5276}
tr.sub td{background:#fdf6e3}tr.tot td{background:#eef4fa;font-weight:800}
small{color:#9aa3b2;font-size:10.5px}
.ok{color:#15803d;font-weight:700}
.note{background:#fbfcfe;border:1px dashed #cbd5e1;border-radius:8px;padding:11px 13px;font-size:12px;color:#4b5563;line-height:1.9}
.note b{color:#1a5276}
@media(max-width:1150px){.kpis{grid-template-columns:repeat(3,1fr)}}
@media(max-width:760px){.kpis{grid-template-columns:repeat(2,1fr)}table{font-size:11.5px}th,td{padding:6px}}
</style></head><body><div class="wrap">
<h1>{{period}}应收账款账龄统计 · 诊断原料两大部门</h1>
<div class="meta">
截止 <b>2026-09-30</b> ｜ 范围：<b>诊断原料大客户销售部 ＋ 诊断原料销售拓展部</b>（各一份 {{period}} 报送文件）<br>
数据源：<b>2026年10月逾期应收回款计划-总表（回款进度）</b> → Sheet「附表_逾期&amp;未逾期（汇总）」｜ 账龄判据 = <b>逾期金额 &gt; 0</b>，按逾期天数分档；营收取自 <b>ERP 2026 年 1-10 月销售订单（剔除赠品/测试）</b> ｜ 生成 {{gen}}
</div>
<div class="kpis">
  <div class="kpi"><div class="l">应收总额</div><div class="v">{{tot_wan}}<span style="font-size:13px"> 万</span></div><div class="s">{{n}} 单</div></div>
  <div class="kpi"><div class="l">逾期合计</div><div class="v" style="color:#b91c1c">{{ov_wan}}<span style="font-size:13px"> 万</span></div><div class="s">占应收 {{ov_pct}}</div></div>
  <div class="kpi"><div class="l">账期内</div><div class="v" style="color:#1d4ed8">{{nd_wan}}<span style="font-size:13px"> 万</span></div><div class="s">{{nd_pct}}</div></div>
  <div class="kpi"><div class="l">逾期60天以上</div><div class="v" style="color:#b91c1c">{{o60_wan}}<span style="font-size:13px"> 万</span></div><div class="s">{{o60_pct}} ｜ {{o60_n}} 单</div></div>
  <div class="kpi"><div class="l">逾期一年以上</div><div class="v" style="color:#7f1d1d">{{y1_wan}}<span style="font-size:13px"> 万</span></div><div class="s">{{y1_n}} 单</div></div>
  <div class="kpi"><div class="l">最长逾期</div><div class="v" style="color:#7f1d1d">{{max_days}}<span style="font-size:13px"> 天</span></div><div class="s">{{max_cust}}</div></div>
</div>
<div class="card"><h2>一、账龄结构（两部门合计）</h2>{{svg}}
<div style="display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;padding-top:11px;border-top:1px solid #eef1f6">{{legend}}</div></div>
<div class="card"><h2>二、分账龄统计（两部门合计）</h2>
<table><thead><tr><th>账龄</th><th style="text-align:right">单数</th><th style="text-align:right">金额(元)</th>
<th style="text-align:right">占应收</th><th style="text-align:right">占逾期</th><th>占比</th></tr></thead><tbody>{{t1}}</tbody></table></div>
<div class="card"><h2>三、分部门 × 账龄（万元）</h2>
<table><thead><tr><th>部门</th>{{hdr_short}}<th style="text-align:right">应收合计</th>
<th style="text-align:right">逾期60天以上</th><th style="text-align:right">逾期合计</th></tr></thead><tbody>{{t2}}</tbody></table></div>
<div class="card"><h2>四、分销售员 × 账龄（万元）</h2>
<table><thead><tr><th>销售员</th><th>部门</th><th style="text-align:right">单数</th>{{hdr_short}}
<th style="text-align:right">逾期合计</th><th style="text-align:right">应收合计</th><th style="text-align:right">逾期占比</th></tr></thead>
<tbody>{{t3}}</tbody></table></div>
<div class="card"><h2>五、逾期一年以上明细（{{y1_n}} 单 · {{y1_wan}} 万元）</h2>
<table><thead><tr><th>客户</th><th style="text-align:right">逾期天数</th><th style="text-align:right">金额(元)</th>
<th>销售员</th><th>到期日</th></tr></thead><tbody>{{y1tbl}}</tbody></table></div>
<div class="card"><h2>六、勾稽校验（本表 vs 源文件《逾期应收回款_进度》）</h2>
<table><thead><tr><th>销售员</th><th>部门</th><th style="text-align:right">逾期(本表)</th><th style="text-align:right">逾期(进度表)</th>
<th style="text-align:right">差</th><th style="text-align:right">60天以上(本表)</th><th style="text-align:right">60天以上(进度表)</th>
<th style="text-align:right">差</th></tr></thead><tbody>{{cc}}</tbody></table>
<div class="note" style="margin-top:10px">
<b>口径说明</b><br>
① <b>逾期判据 = 逾期金额 &gt; 0</b>（不是逾期天数 &gt; 0）—— {{zero_txt}}；按天数判据会把这类行误归「账期内」并丢金额。<br>
② 分档互斥、合计 = 应收总额：账期内 / 逾期1-30天 / 逾期31-60天 / 逾期61-365天 / 逾期365天以上（一年以上）。<br>
③ 「逾期60天以上」= 61-365 天 ＋ 365 天以上 = <b>{{o60_exact}} 元</b>，与源表《进度表》「60天以上」列一致（大客户 {{dk60}} 元 + 拓展 {{tz60}} 元）。<br>
④ 逐人两列勾稽差均为 <b>0.00</b>（{{cc_n}} 人，两大部门各自与源文件《进度表》对齐）。<br>
⑤ 你要的口径对照：<b>账期内</b>→账期内；<b>逾期30天</b>→逾期1-30天；<b>逾期60天</b>→逾期31-60天；<b>逾期60天以上</b>→逾期61-365天＋逾期一年以上（见〔逾期60天以上 小计〕）；<b>逾期一年以上</b>→逾期365天以上。<br>
⑥ 口径统一说明：两大部门均为 <b>{{period}}</b> 报送数据、截止 2026-09-30，可直接相加；生命科学部回款表未在报送范围内，不含其数据。
</div></div>
<div class="card"><h2>七、逾期主力客户 · 按部门</h2>{{sec7}}</div>
<div class="card"><h2>八、逾期主力客户 · 按销售员（每人 Top3）</h2>
<table><thead><tr><th>销售员</th><th>部门</th><th style="text-align:right">逾期合计(万)</th>
<th style="text-align:right">本年营收(万)</th><th>逾期主力客户（金额）</th></tr></thead><tbody>{{sec8}}</tbody></table></div>
<div class="card"><h2>九、营收主力客户 · 按部门</h2>{{sec9}}
<div class="note" style="margin-top:10px">营收口径：ERP 销售订单 {{erp_period}}，<b>剔除赠品/测试单</b>，按客户归集；与应收侧按客户名精确匹配（153 家中 4 家本年无订单，见第十一节）。</div></div>
<div class="card"><h2>十、营收主力客户 · 按销售员（每人 Top3）</h2>
<table><thead><tr><th>销售员</th><th>部门</th><th style="text-align:right">本年营收(万)</th>
<th style="text-align:right">应收(万)</th><th style="text-align:right">逾期(万)</th><th style="text-align:right">逾期/营收</th>
<th>营收主力客户（金额）</th></tr></thead><tbody>{{sec10}}</tbody></table></div>
<div class="card"><h2>十一、风险客户（长期挂账·本年零营收 ／ 逾期超营收 30%）</h2>
<table><thead><tr><th>客户</th><th>部门</th><th>销售员</th><th style="text-align:right">逾期(万)</th>
<th style="text-align:right">最长天数</th><th>风险类型</th></tr></thead><tbody>{{sec11}}</tbody></table>
<div class="note" style="margin-top:10px">判定：① <b>本年零营收但仍挂逾期</b>（4 家）—— 已无业务往来，属回收/核销评估对象；② <b>逾期/营收 ≥ 30%</b>（8 家）—— 单客户回款风险高度集中，建议停货或预付。</div></div>
</div><script>/* 静态自包含，无外部依赖 */</script></body></html>"""

VALS = {
    'period': '2026年10月',
    'gen': D1['meta']['generated'],
    'tot_wan': '%.2f' % wan(TOT), 'n': str(N),
    'ov_wan': '%.2f' % wan(OV), 'ov_pct': pct(OV / TOT * 100),
    'nd_wan': '%.2f' % wan(ND), 'nd_pct': pct(ND / TOT * 100),
    'o60_wan': '%.2f' % wan(o60v), 'o60_pct': pct(o60v / TOT * 100), 'o60_n': str(o60n),
    'y1_wan': '%.2f' % wan(y1['金额']), 'y1_n': str(y1['单数']),
    'max_days': str(mx['od_days']), 'max_cust': H.escape(mx['cust']),
    'svg': svg, 'legend': legend, 't1': ''.join(t1), 't2': ''.join(t2), 't3': ''.join(t3),
    'y1tbl': y1tbl, 'cc': cc,
    'hdr_short': ''.join('<th style="text-align:right">%s</th>' % H.escape(k) for k in B),
    'zero_txt': zero_txt, 'o60_exact': money(o60v),
    'dk60': money(D1['over60_subtotal']['金额']), 'tz60': money(D2['over60_subtotal']['金额']),
    'cc_n': str(len(D1['crosscheck']) + len(D2['crosscheck'])),
    'sec7': sec7, 'sec8': sec8, 'sec9': sec9, 'sec10': sec10, 'sec11': sec11,
    'erp_period': '2026-01-01 ~ 2026-10-15',
}
page = HTML
for k, v in VALS.items():
    page = page.replace('{{%s}}' % k, str(v))
assert not re.findall(r'\{\{\w+\}\}', page), '未替换占位符: %s' % set(re.findall(r'\{\{\w+\}\}', page))
page = BK.inject(page, theme='light', tag='营销中心 · 销售管理', footer_extra='数据源：2026年10月逾期应收回款计划-总表（回款进度）· 大客户 / 销售拓展')
import re as _re
_t = _re.sub(r'<[^>]+>', '', page)
for _bad in ('万 万', '万万元', '万元万', '万万'):
    assert _bad not in _t, '❌ 单位重复: %s' % _bad
v = BK.verify(page)
bad = [k for k, ok in v.items() if not ok]
assert not bad, '品牌体检未过: %s' % bad
out = W + '/销售分析/2026年10月应收账款账龄统计_诊断原料全部门.html'
io.open(out, 'w', encoding='utf-8').write(page)
print('✅ HTML:', out, '%.0f KB' % (os.path.getsize(out) / 1024))
print('   两部门合计：应收 %s 元｜逾期 %s 元（%s）｜账期内 %s 元｜60天以上 %s 元｜一年以上 %s 元'
      % (money(TOT), money(OV), pct(OV / TOT * 100), money(ND), money(o60v), money(y1['金额'])))
print('   品牌体检:', v)
