#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
生成 三部门逐月_2026.json —— 三部门月×部门实际/目标/2025同期 + 客户 + 销售员聚合。
数据全部从 ERP 真实订单现算，单位统一为万元（保留2位小数）。
运行：env -u PYTHONPATH -u VIRTUAL_ENV /usr/bin/python3 gen_dept_monthly_json.py
"""
import os, sys, json, shutil, datetime
import pandas as pd

sys.path.insert(0, '/Users/liuxinyuan/Desktop/Hermes输出-工作类/脚本')
import erp_sales_core as core
import sp_merge as _spm   # 销售员归并映射（唯一权威源，见 脚本/sp_merge.py）
import ls_region_map as _lrm   # 生命科学部 广东/浙江 区域映射（唯一权威源）

BASE = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
DATA = os.path.join(BASE, '数据')
OUT = os.path.join(DATA, '三部门逐月_2026.json')
SRC_2025 = '/Users/liuxinyuan/Desktop/hermas输入-工作台数据库/2024和2025历史数据/2025全年订单.xlsx'
TARGET_PREF = os.path.join(DATA, '月度目标_2026统一.json')
TARGET_FALLBACK = os.path.join(DATA, 'year_targets_2026.json')

DEPTS = ['大客户部', '销售拓展部', '生命科学部']
MONTHS_ALL = [f'2026-{m:02d}' for m in range(1, 13)]   # 全年 12 个月：目标口径（权威源含 10-12 月）


def _detect_months_act():
    """实际月份 = 最新 ERP 已出现的月份（1..最大月），不把未来月份写成 0。"""
    fp = core.find_latest_erp()
    try:
        _df = pd.read_excel(fp, usecols=['合同日期'], dtype=str)
        _ds = pd.to_datetime(_df['合同日期'], errors='coerce')
        _mx = _ds.max()
        if pd.notna(_mx) and getattr(_mx, 'year', 0) == 2026:
            return [f'2026-{m:02d}' for m in range(1, int(_mx.month) + 1)]
    except Exception:
        pass
    return [f'2026-{m:02d}' for m in range(1, 10)]


MONTHS_ACT = _detect_months_act()   # 实际/2025 同期：最新 ERP 月份（当前 1-10 月）
MONTHS = MONTHS_ALL  # 兼容旧引用
W = 1e4  # 元 -> 万元


def norm_dept(d):
    """部门归一：判定顺序 大客户 → 生命科学 → 销售拓展"""
    if not d:
        return None
    d = str(d)
    if '大客户' in d:
        return '大客户部'
    if '生命科学' in d or '科研' in d:
        return '生命科学部'
    if '销售拓展' in d or '拓展' in d:
        return '销售拓展部'
    return None


# ---------------------------------------------------------------- 2026 现算
def extract_2026():
    """按 aggregate 同口径提取 2026 实际月份（当前 1-10）的 月×部门、月×部门×客户、月×部门×销售员。"""
    fp = core.find_latest_erp()
    df = pd.read_excel(fp, dtype=str)
    # 金额口径 = 价税合计（含税）
    df['_amt'] = pd.to_numeric(df['价税合计'], errors='coerce').fillna(0)
    # 销售员
    if '销售员' in df.columns:
        df['_sp'] = df['销售员'].fillna('').astype(str)
    else:
        om = core.load_order_sp_map(); sm = core.load_sp_map()
        df['_oid'] = df['单据编号'].astype(str).str.strip()
        df['_sp'] = df['_oid'].map(om).fillna('')
        miss = df['_sp'] == ''
        df.loc[miss, '_sp'] = df.loc[miss, '客户'].astype(str).str.strip().map(sm).fillna('')
    pt_col = '客户采购类型#' if '客户采购类型#' in df.columns else ('产品类型#' if '产品类型#' in df.columns else None)
    df['_date'] = df['合同日期'].apply(core.ser2date)
    df['_month'] = df['_date'].str[:7]

    std = df[(df['单据类型'] == '标准销售订单') & (df['单据状态'] == '已审核') & (df['是否赠品'] != '是')]
    std = std[~std['客户'].astype(str).str.contains(core.BAORUI_PATTERN, na=False)].copy()
    std['_amt'] = std['_amt'].astype(float)

    person_dept, lzy = core.load_mappings()
    md, mdc, mds = {}, {}, {}          # dept / dept-cust / dept-sp
    mdr, sp_region = {}, {}            # 月×区域 原始额（生命科学部）；销售员→区域
    unmapped_ls = set()                # 生命科学部里区域映射缺失的销售员（必须在门禁里为空）
    sp_custs = {}                      # (m,dept,sp) -> set(cust)
    for _, r in std.iterrows():
        m = str(r['_month'])
        if m not in MONTHS_ACT:      # 只看最新 ERP 已出现的月份（当前 1-10 月）
            continue
        sp = str(r['_sp']).strip()
        cust = str(r['客户']).strip()
        amt = float(r['_amt'])
        pt = str(r[pt_col]) if pt_col else ''
        kpi_person, dept = core.map_person(sp, cust, person_dept, lzy, pt)
        if dept is None:
            continue
        eff_sp = kpi_person or sp
        # 销售员归并：① 黄明月/张立娅/刘新元 → 刘子研（大客户1岗）
        #             ② 离职人员 → 区域经理 黄建佳/黄自鑫/段玉/梁莹莹/肖立→韩远怀；
        #                赵以诺/李岚风/曾琪→吴云；龙友波→黄文才（见 脚本/sp_merge.py）
        eff_sp = _spm.merge_sp(eff_sp)
        md.setdefault(m, {}); md[m][dept] = md[m].get(dept, 0) + amt
        mdc.setdefault(m, {}).setdefault(dept, {})
        mdc[m][dept][cust] = mdc[m][dept].get(cust, 0) + amt
        mds.setdefault(m, {}).setdefault(dept, {})
        mds[m][dept][eff_sp] = mds[m][dept].get(eff_sp, 0) + amt
        sp_custs.setdefault((m, dept, eff_sp), set()).add(cust)
        if dept == '生命科学部':     # 广东 / 浙江 子维度（归并后按归属人所属区域计）
            _rg = _lrm.region_of(eff_sp)
            if _rg:
                mdr.setdefault(m, {}); mdr[m][_rg] = mdr[m].get(_rg, 0) + amt
                sp_region[eff_sp] = _rg
            else:
                unmapped_ls.add(eff_sp)
    if unmapped_ls:
        raise RuntimeError('生命科学部存在未映射区域的销售员（请补 脚本/ls_region_map.py）：'
                           + ', '.join(sorted(unmapped_ls)))
    return fp, md, mdc, mds, sp_custs, mdr, sp_region


# ---------------------------------------------------------------- 2025 同期
def extract_2025():
    """2025 同期（与 2026 实际月份对齐，当前 1-10）。该文件无「单据状态」列，采用 标准销售订单 + 非赠品 + 价税合计，
    部门经「销售部门区域对应」表归一（科研-* → 生命科学部）。锚点 1-9 合计 2749.27 万可复现。"""
    if not os.path.exists(SRC_2025):
        return None
    last25 = f'2025-{len(MONTHS_ACT):02d}'
    df = pd.read_excel(SRC_2025, sheet_name='Sheet1', dtype=str)
    df['_amt'] = pd.to_numeric(df['价税合计'], errors='coerce').fillna(0)
    df['_month'] = df['合同月份'].astype(str).str[:7]
    m = pd.read_excel(SRC_2025, sheet_name='销售部门区域对应', dtype=str)
    sp2dept = {str(r['销售员']).strip(): str(r['部门']).strip()
               for _, r in m.iterrows() if pd.notna(r['销售员'])}
    person_dept, lzy = core.load_mappings()

    std = df[(df['单据类型'] == '标准销售订单') & (df['是否赠品'] != '是')].copy()
    std = std[~std['客户'].astype(str).str.contains(core.BAORUI_PATTERN, na=False)]
    std['_amt'] = std['_amt'].astype(float)

    md, mdc = {}, {}
    for _, r in std.iterrows():
        m5 = str(r['_month'])
        if not ('2025-01' <= m5 <= last25):
            continue
        sp = str(r['销售员']).strip()
        cust = str(r['客户']).strip()
        amt = float(r['_amt'])
        dept = norm_dept(sp2dept.get(sp))
        if dept is None:
            _, dept = core.map_person(sp, cust, person_dept, lzy, str(r.get('产品类型#', '')))
        if dept is None:
            continue
        md.setdefault(m5, {}); md[m5][dept] = md[m5].get(dept, 0) + amt
        mdc.setdefault(m5, {}).setdefault(dept, {})
        mdc[m5][dept][cust] = mdc[m5][dept].get(cust, 0) + amt
    return md, mdc


# ---------------------------------------------------------------- 目标
def load_targets():
    """读**唯一权威目标源**，返回 12 个月（1-12）的部门月度目标（元）。

    顺序：① 数据/目标基础数据_2026.json（唯一权威源，管理台导出落盘）
          ② 数据/月度目标_2026统一.json（旧源，已 deprecation，仅回退）
    新源缺失/损坏 → 回退旧源并打印告警（绝不静默）。
    """
    import target_source as _ts
    _sd = _ts.load()
    if _sd:
        def _annual_int(x):
            x = float(x)
            return int(x) if x.is_integer() else x
        depts = {}
        for d in DEPTS:
            tm = _ts.dept_months(_sd, d)          # {月: 万元}
            months = [round(float(tm[_ts.MONTHS[i]]) * W, 2) for i in range(12)]   # → 元
            depts[d] = {'annual': _annual_int(_ts.dept_annual(_sd, d)), 'months': months}
        total_months = [round(sum(depts[d]['months'][i] for d in DEPTS), 2) for i in range(12)]
        return ('目标基础数据_2026.json（唯一权威源）', depts, total_months,
                _annual_int(_ts.total_annual(_sd)))
    print('⚠️ 未找到权威源 数据/目标基础数据_2026.json → 回退旧源 月度目标_2026统一.json（已 deprecation）')
    if os.path.exists(TARGET_PREF):
        with open(TARGET_PREF, encoding='utf-8') as f:
            t = json.load(f)
        depts = {}
        for d in DEPTS:
            months = list(t['depts'][d]['months'])
            assert len(months) == 12, f'{d} 目标月份数 != 12'
            depts[d] = {'annual': t['depts'][d]['annual'], 'months': months}
        mt = t['monthly_total']
        total_months = [mt[str(i)] for i in range(1, 13)]
        return os.path.basename(TARGET_PREF), depts, total_months, t['annual_total']
    # 回退
    with open(TARGET_FALLBACK, encoding='utf-8') as f:
        t = json.load(f)
    depts = {}
    for d in DEPTS:
        months = list(t['depts'][d]['months'])
        assert len(months) == 12, f'{d} 目标月份数 != 12'
        depts[d] = {'annual': t['depts'][d]['annual'], 'months': months}
    total_months = list(t['monthly'])
    return os.path.basename(TARGET_FALLBACK), depts, total_months, t['annual']


def r2(x):
    return round(float(x) / W, 2)


def main():
    fp26, md26, mdc26, mds26, sp_custs, mdr26, sp_region26 = extract_2026()
    src25, mdc25 = (None, None)
    res25 = extract_2025()
    md25 = mdc25 = None
    if res25:
        md25, mdc25 = res25
        src25 = os.path.basename(SRC_2025)
    tgt_name, tdepts, tmonths_total, tannual_total = load_targets()

    months_all = MONTHS_ALL      # 12 个月（目标）
    months_act = MONTHS_ACT      # 最新 ERP 已出现的月份（当前 1-10 月）
    months25key = {f'2026-{m:02d}': f'2025-{m:02d}' for m in range(1, len(months_act) + 1)}

    depts_out = {}
    for d in DEPTS:
        # 目标：12 个月全给
        tm = {months_all[i]: r2(tdepts[d]['months'][i]) for i in range(12)}
        # 实际 / 2025 同期：跟随最新 ERP 月份（当前 1-10 月；未来月份不写键 → 空白，绝不写 0）
        am = {months_act[i]: r2(md26.get(months_act[i], {}).get(d, 0)) for i in range(len(months_act))}
        a25 = {}
        for i in range(len(months_act)):
            k26, k25 = months_act[i], months25key[months_act[i]]
            if md25 is None:
                a25[k26] = None
            else:
                a25[k26] = r2(md25.get(k25, {}).get(d, 0))
        depts_out[d] = {'target_annual': tdepts[d]['annual'],
                        'target_monthly': tm, 'actual_monthly': am, 'actual_2025': a25}

    # ── 生命科学部 · 广东 / 浙江 子维度（用户 2026-10-06 拍板；前端可展开、可下钻）───
    #  目标：广东 3 + 浙江 5 人有年度目标（共 8 人）→ 区域当月目标 = 部门当月 / 2（2 位小数）；各自 12 月合计精确 = 300.0。
    #  实际：从原始金额按「销售员→区域」聚合现算（归并后按归属人所属区域），逐月与部门月值勾稽
    #       （浙江吸收四舍五入差），9 月合计精确 = 部门实际 39.86（不重算金额、不写 0）。
    #  区域经理：缺口按区域摊（浙江 110→吴云 190.0；广东 65→韩远怀 165.0，含刘斌离职释放的 30）；浙江含新增 张晋菲 15.0。
    _gd_am = {m: round(mdr26.get(m, {}).get('广东', 0.0) / W, 2) for m in months_act}
    # 逐人实际（归并后归属人，原始金额）与客户数（供区域下钻行显示）
    _psp_amt, _psp_custs = {}, {}
    for m in months_act:
        for sp, amt in mds26.get(m, {}).get('生命科学部', {}).items():
            _psp_amt[sp] = _psp_amt.get(sp, 0.0) + amt
        for sp in (mds26.get(m, {}).get('生命科学部', {}) or {}):
            _psp_custs.setdefault(sp, set()).update((m, c) for c in sp_custs.get((m, '生命科学部', sp), set()))
    regions_out = {}
    for _rg in _lrm.REGIONS:
        _mgr = _lrm.REGION_MANAGER[_rg]
        _rtm = {m: round(depts_out['生命科学部']['target_monthly'][m] / 2.0, 2) for m in months_all}
        _ram = {}
        for m in months_act:
            if _rg == "广东":
                _ram[m] = _gd_am[m]
            else:
                _ram[m] = round(depts_out['生命科学部']['actual_monthly'][m] - _gd_am[m], 2)
        regions_out[_rg] = {
            'target_annual': 300.0,
            'target_monthly': _rtm,
            'actual_monthly': _ram,
            'manager': _mgr,
            'manager_target_annual': _spm.MGR_TARGET_OVERRIDE.get(_mgr),
            'manager_shortfall': (_spm.MGR_SHORTFALL.get(_mgr) or {}).get('shortfall'),
            # 区域逐人年度目标（含新增人员；页面按此渲染区域下的销售员行）
            'people': [{'name': _nm, 'target_annual': _tg,
                        'is_new': _nm in _spm.LS_NEW_HIRES,
                        'actual_ytd': round(_psp_amt.get(_nm, 0.0) / W, 2),
                        'custs': len(_psp_custs.get(_nm, set()))}
                       for _nm, _tg in _spm.LS_PERSON_TARGETS[_rg].items()],
        }
    depts_out['生命科学部']['regions'] = regions_out
    # 门禁：两区逐人年度目标各自闭合 300 / 部门闭合 600（不符即中止）
    _p_sum = {_rg: round(sum(v['target_annual'] for v in _r['people']), 2) for _rg, _r in regions_out.items()}
    assert all(abs(v - 300.0) < 1e-9 for v in _p_sum.values()), f'区域逐人目标合计 != 300: {_p_sum}'
    assert abs(sum(_p_sum.values()) - 600.0) < 1e-9, f'逐人目标部门合计 != 600: {_p_sum}'

    total_out = {
        'target_annual': tannual_total,
        # 合计 = 三部门之和（保证 部门合计 == total，避免独立四舍五入产生 0.01 差）
        'target_monthly': {m: round(sum(depts_out[d]['target_monthly'][m] for d in DEPTS), 2) for m in months_all},
        'actual_monthly': {m: round(sum(depts_out[d]['actual_monthly'][m] for d in DEPTS), 2) for m in months_act},
    }
    if md25 is None:
        total_out['actual_2025'] = {m: None for m in months_act}
    else:
        total_out['actual_2025'] = {
            m: round(sum(depts_out[d]['actual_2025'][m] for d in DEPTS), 2) for m in months_act}

    # ── Q4（2026-10 ~ 12）目标与年度缺口 ───────────────────────────────────────
    # 口径：部门月度目标（target_monthly，2位小数）先按 1 位小数取整再加总，
    #       与 kpi_scheme.json meta.h2_targets 的展示口径一致（200.7/340.9/134.5…）。
    # annual_shortfall_at_full_q4 = 年度目标 − 实际月份累计 − Q4目标
    #   （即使 Q4 完全达标，全年仍差这么多；当前实际月份已含 10 月。）
    _r1 = lambda x: round(float(x), 1)
    q4_months = ['2026-10', '2026-11', '2026-12']
    q4_by_dept = {d: _r1(sum(_r1(depts_out[d]['target_monthly'][m]) for m in q4_months)) for d in DEPTS}
    q4_target = _r1(sum(q4_by_dept.values()))
    ytd_actual_1dp = _r1(sum(total_out['actual_monthly'].values()))
    shortfall = _r1(tannual_total - ytd_actual_1dp - q4_target)
    q4_actual_months = [m for m in q4_months if m in total_out['actual_monthly']]
    q4 = {
        'months': q4_months,
        'target': q4_target,
        'target_by_dept': q4_by_dept,
        'actual_to_date': {m: total_out['actual_monthly'][m] for m in q4_actual_months},
        'actual_by_dept_to_date': {d: {m: depts_out[d]['actual_monthly'][m] for m in q4_actual_months}
                                   for d in DEPTS},
        'actual_total_to_date': round(sum(total_out['actual_monthly'][m] for m in q4_actual_months), 2),
        'annual_shortfall_at_full_q4': shortfall,
    }

    # customers / salespersons（2026 1-9，aml>0，金额降序）
    customers = []
    for m in months_act:
        for d in DEPTS:
            for cust, amt in mdc26.get(m, {}).get(d, {}).items():
                v = r2(amt)
                if v <= 0:
                    continue
                k25 = months25key[m]
                a25 = r2(mdc25.get(k25, {}).get(d, {}).get(cust, 0)) if mdc25 else 0.0
                customers.append({'month': m, 'dept': d, 'cust': cust,
                                  'amt': v, 'amt_2025': a25})
    # [FIX 2026-10-05] 补 2025-only 客户（2026 同期为 0）。
    # 原实现只遍历 2026，导致「纯 2025 有、2026 无」的客户根本不进数组 →
    # 驱动层无法算出「流失客户」（恒为 0），且瀑布图基线被低估成 1,662.4 万
    # （正确应为 2,749.3 万）。补入后 Σamt_2025 覆盖全量 2025，瀑布图可勾稽。
    _seen26 = {(c['month'], c['dept'], c['cust']) for c in customers}
    _added25 = 0
    for m in months_act:
        k25 = months25key[m]
        for d in DEPTS:
            for cust, amt in (mdc25 or {}).get(k25, {}).get(d, {}).items():
                v = r2(amt)
                if v <= 0 or (m, d, cust) in _seen26:
                    continue
                customers.append({'month': m, 'dept': d, 'cust': cust,
                                  'amt': 0.0, 'amt_2025': v, 'only2025': True})
                _added25 += 1
    print(f'   [FIX] 补入 2025-only 客户 {_added25} 条（用于流失口径）')
    customers.sort(key=lambda x: (x['month'], x['dept'], -x['amt']))

    salespersons = []
    for m in months_act:
        for d in DEPTS:
            for sp, amt in mds26.get(m, {}).get(d, {}).items():
                v = r2(amt)
                if v <= 0:
                    continue
                salespersons.append({'month': m, 'dept': d, 'sp': sp,
                                     'amt': v, 'custs': len(sp_custs.get((m, d, sp), set())),
                                     'region': (sp_region26.get(sp) if d == '生命科学部' else None)})
    salespersons.sort(key=lambda x: (x['month'], x['dept'], -x['amt']))

    note = (
        '口径：ERP 标准销售订单 + 单据状态=已审核 + 非赠品(是否赠品≠是) + 剔除宝锐/宝泰/横琴自体系客户；'
        '金额=价税合计（含税，元→万元）。部门归一判定顺序：大客户→生命科学→销售拓展。'
        f'2026 源=ERP现用件 {os.path.basename(fp26)}。'
        f'2025 同期源={src25}（该历史文件无「单据状态」字段，故沿用 标准销售订单+非赠品 口径，'
        '2025部门由「销售部门区域对应」表归一，科研-* 计入生命科学部；2025 年 1-9 月合计 2749.27 万，与已知锚点一致）。'
        f'目标源={tgt_name}（唯一权威源 数据/目标基础数据_2026.json：部门/区域/个人年度目标 + 月度目标；'
        '月度按 monthly_rule 自动摊派，合计精确=年度；旧源 kpi_scheme/月度目标_2026统一 已 deprecation 仅回退。全年 12 个月均有目标；'
        '年度 大客户2650/拓展3400/生命科学600=合计6650 万。'
        '【2026-10-06 用户拍板修正】生命科学部 900→600 万（广东 300 / 浙江 300），其 12 个月度目标=原值×2/3'
        '（1 位小数，舍入差由 12 月吸收，合计精确 600.0）；大客户/拓展目标不动。'
        '生命科学部另附 regions（广东/浙江）子维度，区域月目标 = 部门当月/2，各区 12 月合计 300.0。）'
        'actual_2025 键沿用 2026 月标签（同比对齐）；'
        f'target_monthly 含 12 个月（1-12）；actual_monthly / actual_2025 覆盖最新 ERP 月份（当前 1-{len(months_act)} 月，'
        '未到月份不写键、保持空白，绝不写 0）；'
        f'q4 = 10-12 月目标、Q4 已实现（当前 10 月）与年度缺口。'
        f'customers/salespersons 为 2026 1-{len(months_act)} 月，按金额降序，仅保留金额>0（四舍五入到万元后仍 >0）；'
        'salespersons 带 region（生命科学部为 广东/浙江，其余为 null）。'
    )

    out = {
        'meta': {
            'generated': datetime.datetime.now().astimezone().isoformat(timespec='seconds'),
            'source_erp': os.path.basename(fp26),
            'source_target': tgt_name,
            'note': note,
            'q4_caliber': ('Q4 目标 = 部门 10-12 月月度目标（target_monthly，2位小数）先按 1 位小数取整再加总，'
                           '与 kpi_scheme.json meta.h2_targets 展示口径一致；'
                           'annual_shortfall_at_full_q4 = 年度目标(6650) − 实际月份累计 − Q4目标。'),
            'mgr_merge': _spm.mgr_merge_meta_block(),
            'ls_region': _lrm.meta_block(),
        },
        'months': months_all,
        'depts': depts_out,
        'total': total_out,
        'q4': q4,
        'customers': customers,
        'salespersons': salespersons,
    }

    if os.path.exists(OUT):
        ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        shutil.copy2(OUT, OUT + f'.bak_{ts}')
        print('备份原文件 ->', OUT + f'.bak_{ts}')
    with open(OUT, 'w', encoding='utf-8') as f:
        json.dump(out, f, ensure_ascii=False, indent=2)
    print('已写出:', OUT)

    # ------------------------------------------------ 自检门禁
    print('\n================ 自检门禁 ================')
    a_tot = {m: sum(x for x in [depts_out[d]['actual_monthly'][m] for d in DEPTS]) for m in months_act}
    ytd = sum(a_tot.values())
    ytd_19 = sum(total_out['actual_monthly'][m] for m in MONTHS_ALL[:9])
    a_tot_19 = {m: a_tot[m] for m in MONTHS_ALL[:9] if m in a_tot}
    print('\n月份     大客户部   销售拓展部  生命科学部 |  实际合计   该月目标   达成率')
    for m in months_act:
        row = [depts_out[d]['actual_monthly'][m] for d in DEPTS]
        tgt = total_out['target_monthly'][m]
        rate = a_tot[m] / tgt * 100 if tgt else 0
        print(f'{m}  {row[0]:9.2f} {row[1]:10.2f} {row[2]:9.2f} | {a_tot[m]:9.2f} {tgt:9.2f}  {rate:6.2f}%')
    ytd_tgt = sum(total_out['target_monthly'][m] for m in months_act)
    print(f'YTD合计  {sum(depts_out["大客户部"]["actual_monthly"].values()):9.2f} '
          f'{sum(depts_out["销售拓展部"]["actual_monthly"].values()):10.2f} '
          f'{sum(depts_out["生命科学部"]["actual_monthly"].values()):9.2f} | '
          f'{ytd:9.2f} {ytd_tgt:9.2f}  {ytd/ytd_tgt*100:6.2f}%')

    # ① 2026 1-9 合计锚点（历史区间，不随 10 月新增而变）
    g1 = abs(ytd_19 - 3443.0) <= 10
    print(f'\n① 2026 1-9 合计 = {ytd_19:.2f} 万（锚点 3443.0 ±10）-> {"PASS" if g1 else "FAIL"}'
          f'；1-{len(months_act)} 月累计 = {ytd:.2f} 万')
    # ② 三部门 YTD 之和 == total YTD
    s_dept = sum(sum(depts_out[d]['actual_monthly'].values()) for d in DEPTS)
    g2 = abs(s_dept - ytd) < 0.01
    print(f'② 三部门YTD之和 = {s_dept:.2f} vs total {ytd:.2f} -> {"PASS" if g2 else "FAIL"}')
    # ③ 每月三部门之和 == 该月 total
    g3 = all(abs(a_tot[m] - total_out['actual_monthly'][m]) < 0.01 for m in months_act)
    print(f'③ 每月三部门之和==total该月 -> {"PASS" if g3 else "FAIL"}')
    # ④ 达成率（历史 1-9 门禁用固定锚点；1-10 全量只打印，不参与固定值断言）
    rates = {d: sum(depts_out[d]['actual_monthly'].values()) / depts_out[d]['target_annual'] * 100 for d in DEPTS}
    tot_rate = ytd / total_out['target_annual'] * 100
    rates_19 = {d: sum(depts_out[d]['actual_monthly'][m] for m in MONTHS_ALL[:9]) / depts_out[d]['target_annual'] * 100
                for d in DEPTS}
    tot_rate_19 = ytd_19 / total_out['target_annual'] * 100
    exp = {'大客户部': 41.77, '销售拓展部': 67.53, '生命科学部': 6.64}
    okd = all(abs(rates_19[d] - exp[d]) <= 1 for d in DEPTS) and abs(tot_rate_19 - 51.77) <= 1
    print('④ 达成率(实际/年度目标): ' + '  '.join(f'{d}={rates[d]:.2f}%' for d in DEPTS) + f'  合计={tot_rate:.2f}%')
    print(f'   1-9 门禁 大客户{rates_19["大客户部"]:.2f}/拓展{rates_19["销售拓展部"]:.2f}/'
          f'生命科学{rates_19["生命科学部"]:.2f}/合计{tot_rate_19:.2f}（期望 41.77/67.53/6.64/51.77 ±1）'
          f' -> {"PASS" if okd else "FAIL"}')
    # ⑤ 目标 12 个月且全年合计 == 6650；未到月份实际必须无键/无 0
    g5 = (len(out['months']) == 12
          and len(total_out['target_monthly']) == 12
          and all(len(depts_out[d]['target_monthly']) == 12 for d in DEPTS)
          and abs(sum(total_out['target_monthly'].values()) - 6650) <= 0.5)
    no_zero_future = all(
        (m not in depts_out[d]['actual_monthly']) or depts_out[d]['actual_monthly'][m] is None
        for d in DEPTS for m in months_all if m not in months_act)
    g5 = g5 and no_zero_future and (len(depts_out['大客户部']['actual_monthly']) == len(months_act))
    print(f'⑤ 目标月份={len(out["months"])} 全年目标合计={sum(total_out["target_monthly"].values()):.2f} 万(锚 6650±0.5) '
          f'/ 未到月份实际无 0 -> {"PASS" if g5 else "FAIL"}')
    # ⑥ Q4 目标与年度缺口（缺口随 10 月实际变化，用公式复核，不写死）
    expected_shortfall = round(6650 - ytd - q4['target'], 1)
    g6 = (abs(q4['target'] - 1913.6) < 0.05
          and abs(q4['annual_shortfall_at_full_q4'] - expected_shortfall) < 0.05)
    print(f'⑥ Q4目标={q4["target"]} Q4分部门={q4["target_by_dept"]} '
          f'Q4已实现(10月)={q4.get("actual_total_to_date")} 年度缺口={q4["annual_shortfall_at_full_q4"]} '
          f'(公式={expected_shortfall}) -> {"PASS" if g6 else "FAIL"}')

    # ⑦ 生命科学部新目标：年度 600、月目标 12 项合计精确 600.0、形状 = 原值×2/3（用户拍板）
    _base = [5.4, 5.5, 20.5, 41.2, 63.1, 99.2, 65.8, 74.8, 106.8, 134.5, 141.1, 142.0]
    _exp_m = [round(b * 2.0 / 3.0, 1) for b in _base[:11]]
    _exp_m.append(round(600.0 - sum(_exp_m), 1))
    _ls = depts_out['生命科学部']
    _ls_ok = (abs(_ls['target_annual'] - 600.0) < 1e-9
              and abs(sum(_ls['target_monthly'].values()) - 600.0) < 1e-9
              and all(abs(_ls['target_monthly'][months_all[i]] - _exp_m[i]) < 1e-9 for i in range(12)))
    print(f'⑦ 生命科学部 年度目标={_ls["target_annual"]} 12月合计={sum(_ls["target_monthly"].values()):.2f} '
          f'形状(×2/3)一致={_ls_ok} -> {"PASS" if _ls_ok else "FAIL"}')
    # ⑧ 广东 / 浙江 子维度：年度各 300、月目标 12 项各合计精确 300.0、月目标之和 == 部门、实际之和 == 部门
    _regs = _ls.get('regions') or {}
    _reg_ok = (set(_regs.keys()) == {'广东', '浙江'})
    for _rg in ('广东', '浙江'):
        _r = _regs.get(_rg) or {}
        _reg_ok = _reg_ok and abs(_r.get('target_annual', 0) - 300.0) < 1e-9
        _reg_ok = _reg_ok and abs(sum((_r.get('target_monthly') or {}).values()) - 300.0) < 1e-9
    for m in months_all:
        _reg_ok = _reg_ok and abs(_regs['广东']['target_monthly'][m] + _regs['浙江']['target_monthly'][m]
                                  - _ls['target_monthly'][m]) < 1e-9
    _gsum = {m: round(_regs['广东']['actual_monthly'][m] + _regs['浙江']['actual_monthly'][m], 2) for m in months_act}
    _reg_ok = _reg_ok and all(abs(_gsum[m] - _ls['actual_monthly'][m]) < 1e-9 for m in months_act)
    _reg_ok = _reg_ok and abs(sum(_regs['广东']['actual_monthly'].values())
                              + sum(_regs['浙江']['actual_monthly'].values())
                              - sum(_ls['actual_monthly'].values())) < 1e-9
    print(f'⑧ 区域 广东/浙江 年度={[_regs.get(r,{}).get("target_annual") for r in ("广东","浙江")]} '
          f'月目标合计={[round(sum((_regs.get(r,{}).get("target_monthly") or {}).values()),2) for r in ("广东","浙江")]} '
          f'实际合计={[round(sum((_regs.get(r,{}).get("actual_monthly") or {}).values()),2) for r in ("广东","浙江")]} '
          f'(部门={round(sum(_ls["actual_monthly"].values()),2)}) -> {"PASS" if _reg_ok else "FAIL"}')

    # ⑨ 离职者（11 人归区域经理：吴云 5 + 韩远怀 6）不再在 salespersons 单列；区域逐人目标闭合（广东300/浙江300/部门600）
    _merged_names = set(_spm.MGR_MERGE.keys())
    _left = sorted({r['sp'] for r in salespersons if r['sp'] in _merged_names})
    _gd_p = _spm.LS_PERSON_TARGETS['广东']
    _zj_p = _spm.LS_PERSON_TARGETS['浙江']
    _gd_sum = round(sum(_gd_p.values()), 1)
    _zj_sum = round(sum(_zj_p.values()), 1)
    _mg_ok = ((not _left) and abs(_gd_sum - 300.0) < 1e-9 and abs(_zj_sum - 300.0) < 1e-9
              and abs(_gd_sum + _zj_sum - 600.0) < 1e-9
              and _spm.LS_PERSON_TARGETS['浙江'].get('张晋菲') == 15.0)
    print(f'⑨ 离职人员残留 salespersons={_left or "无"} ｜ 广东{len(_gd_p)}人={_gd_sum} ｜ '
          f'浙江{len(_zj_p)}人={_zj_sum} ｜ 合计={_gd_sum + _zj_sum} -> {"PASS" if _mg_ok else "FAIL"}')

    print(f'\ncustomers={len(customers)}  salespersons={len(salespersons)}')
    if md25:
        y25 = sum(sum(md25.get(f'2025-{m:02d}', {}).values()) for m in range(1, 10))
        print(f'2025 1-9 同期合计 = {y25/W:.2f} 万（锚点 2749.3 ±10）-> {"PASS" if abs(y25/W-2749.3)<=10 else "FAIL"}')
        y25_all = sum(sum(md25.get(months25key[m], {}).values()) for m in months_act)
        print(f'2025 1-{len(months_act)} 同期合计 = {y25_all/W:.2f} 万（同比展示用）')
    print('\nALL GATES:', 'PASS' if (g1 and g2 and g3 and okd and g5 and g6 and _ls_ok and _reg_ok and _mg_ok) else 'CHECK')


if __name__ == '__main__':
    main()
