# baoyu-dashboard-update — group mapping

## ⚠️ H1-vs-H1 同比方法论陷阱（2026-06-30 新增）

**致命症状**：YOYR_CUSTS中v25=全年2025, v26=H1 2026 → 同比虚高/虚低。

**根因**：旧版YOYR_CUSTS的v25取自2025全年（如丽珠194万），v26取自2026 H1（237万），跨口径比较得出+21.9%。实际H1-vs-H1应为+59.8%（148万→237万）。

**修复方法**：
1. 使用权威ERP文件（2025/2026客户价格.xlsx）分别提取H1数据
2. **按集团聚合**（非按客户）——需建立完整的客户→集团映射
3. 重算 chg/pct/s，替换YOYR_CUSTS数组
4. 重算 YOY_GRADES（基于2026年化预估=v26×2）
5. 更新KPI卡片「同比 +54.3%」→「H1同比 +42.6%」

### 集团映射构建（多源合成）

```python
# 源1：从仪表盘现有YOYR_CUSTS的 g: 字段逆向提取（~386条）
with open('宝锐综合管理仪表盘.html') as f: html = f.read()
yoyr_match = re.search(r'const YOYR_CUSTS=\[(.*?)\];', html, re.DOTALL)
if yoyr_match:
    for e in re.findall(r'\{[^}]+\}', yoyr_match.group(1)):
        cm = re.search(r'c:"([^"]+)"', e)  # 客户名称
        gm = re.search(r'g:"([^"]+)"', e)  # 集团名称
        if cm and gm: cust2grp[cm.group(1)] = gm.group(1)

# 源2：Excel客户清单（42条集团级映射）
cl = pd.read_excel('客户清单.xls', dtype=str)
for _,r in cl.dropna(subset=['集团简称','客户名称']).iterrows():
    cust2grp[str(r['客户名称']).strip()] = str(r['集团简称']).strip()

# 源3：已知子公司关系（手动补充，约50条）
subsidiaries = {
    '丽珠集团': ['珠海丽珠试剂股份有限公司','珠海丽禾医疗诊断产品有限公司'],
    '圣湘集团': ['圣湘生物科技股份有限公司','圣湘（上海）基因科技有限公司',...],
    '天隆集团': ['苏州天隆生物科技有限公司','西安华伟科技有限公司'],
    # ... 约35个集团
}
for grp, custs in subsidiaries.items():
    for c in custs:
        if c not in cust2grp: cust2grp[c] = grp
```

### YOYR_CUSTS生成与替换

```python
# 生成JS数组
entries = []
for grp in all_groups:
    v25 = round(g25.get(grp,0)); v26 = round(g26.get(grp,0))
    if v25 < 5000 and v26 < 5000: continue  # 过滤微小客户
    chg = v26 - v25
    pct = round((v26/v25-1)*100,1) if v25>0 else 0
    
    # 图标：🆕新增 💀流失 🔺大涨 🔻大跌 ↗微涨 ↘微跌
    s = '🆕' if v25==0 else '💀' if v26==0 else '🔺' if chg>100000 else '🔻' if chg<-100000 else '↗' if chg>0 else '↘'
    
    entries.append({'r':region,'c':grp,'g':grp,'v25':v25,'v26':v26,'chg':chg,'pct':pct,'s':s})
entries.sort(key=lambda x: abs(x['chg']), reverse=True)

# 生成JS字符串
yoyr_str = 'const YOYR_CUSTS=[' + ',\n'.join(yoyr_lines) + '];'
grades_str = 'var YOY_GRADES=' + json.dumps(grades, ensure_ascii=False) + ';'

# ⚠️ 用Python regex替换（非patch工具），因为数组太大
import re
with open(dashboard_html, 'r+') as f:
    html = f.read()
    html = re.sub(r'const YOYR_CUSTS=\[.*?\];', yoyr_str, html, flags=re.DOTALL)
    html = re.sub(r'var YOY_GRADES=\{.*?\};', grades_str, html, flags=re.DOTALL)
    f.seek(0); f.write(html); f.truncate()

# KPI卡片同步更新：完成金额、完成率、H1同比%
# 用patch工具更新单行：<div class="kpi-card"><div class="label">已完成...
```


## 客户调研报告年度同比更新（2026-06-30 新增）

将客户调研报告中的单一累计数字替换为年度同比表：

| 年度 | 笔数 | 金额 | 同比 | 趋势 |
|------|------|------|------|------|
| 2023 | 15 | 174.1万 | — | 基准 |
| 2024 | 16 | 131.1万 | -24.7% | 回落 |
| 2025 | 14 | 194.0万 | +48.0% | 反弹 |
| 2026 H1 | 12 | 237.3万 | +21.9% | 超全年 |

KPI卡片改为：累计总金额 + 当期金额 + H1同比 + 年化预估。数据源用首选文件而非ERP快照（避免391万→735万的巨大缺口）。


## 区域同比更新：客户级6月数据映射

当用户提供6月客户级销售明细（字段：客户名称 / 销售员 / 购买总金额），但无区域字段时：

### 数据源准备
1. 从主ERP文件（`erp销售订单_*.xlsx`）建立 `客户名 → 区域` 映射字典
   - ERP Col0=客户, Col22=区域# — 去重取每个客户的首次区域
   - 补充老订单文件（`销售订单_2023～*.xlsx`）扩大映射覆盖率
2. 加载6月客户明细 → 按区域聚合金额
3. 从区域汇总文件（`20260101-*订单.xlsx`，区域级汇总版）获取1-5月子区域基准值

### 大区→子区域分配
ERP的区域#字段是**大区**（华南区/华东区/华北区/华西区/华中区），而YOY表使用**14个子区域**（华南1/华南2/上海1/上海2/浙江/江苏/福建等）。映射方法：
```python
broad_to_sub = {
    "华南区": ["华南1", "华南2"],
    "华东区": ["上海1", "上海2", "浙江", "江苏", "福建"],
    "华北区": ["北京", "华北"],
    "华西区": ["华西"],
    "华中区": ["华中"],
}
# 按各子区域的1-5月基准值加权分配6月大区金额
for sub in subs:
    weight = sub_regions[sub]["v26_base"]  # 1-5月基准
    june_sub[sub] = june_broad * weight / total_weight
```
- 大客户1/大客户2的6月金额需手动匹配（KA客户直接从6月明细中识别）
- 未匹配部分（~13%）按1-5月权重比例分配到所有子区域
- 更新YOYR数组 + 标题 `1-5月 → 1-6月`

---


## Top60集团聚合：多ERP源合并去重 + 仪表盘映射提取 — 2026-06-30 新增

当需要从多个ERP导出文件（不同月份、不同格式）聚合出集团级Top N排名时：

### 数据源合并与去重策略

典型场景：三个文件覆盖不同月份区间——
- `销售订单_2023～20260430.xlsx`（1-4月，含部分5月）
- `销售订单_2026052513593468*.xlsx` Sheet1（5月明细）
- `6月销售数据截止26号*.xlsx`（6月客户级汇总）

**⚠️ 去重是关键**：不同文件有月份重叠。必须用 `单据编号` 作为去重键：

```python
# 1. 主数据源：Full(Jan-Apr) + May Sheet1 + June
# 2. 取 May Sheet1 的 单据编号 集合
may_ids = set(may['单据编号'].dropna().unique())

# 3. Full 的 May 行：只添加不在 may_ids 中的
full_may = full[full['发货日期'].str.contains('2026-05', na=False)]
for _, r in full_may.iterrows():
    if str(r['单据编号']) not in may_ids:
        add_to_totals(r['客户'], float(r['价税合计']))

# 4. 同样检查 ERP16K（Jan-Mar）中是否有不在 Full 中的订单
erp_ids_not_in_full = set(erp16k['单据编号']) - set(full['单据编号'])
# 通常为0（Full更完整），但仍需检查
```

**合并顺序**：Full Jan-Apr → May Sheet1 → Full May extra → June → ERP16K extra

### 从仪表盘HTML逆向提取集团映射

当Excel客户清单只有42条映射（仅集团级），但仪表盘的 `YOYR_CUSTS` 数组嵌入了 **386条** 完整的客户名→集团映射时：

```python
import re

with open('宝锐综合管理仪表盘.html') as f:
    html = f.read()

# 提取 const YOYR_CUSTS=[...];
yoyr_match = re.search(r'const YOYR_CUSTS=\[(.*?)\];', html, re.DOTALL)
cust2grp = {}

if yoyr_match:
    entries = re.findall(r'\{[^}]+\}', yoyr_match.group(1))
    for entry in entries:
        c = re.search(r'c:"([^"]+)"', entry)  # 客户名称
        g = re.search(r'g:"([^"]+)"', entry)  # 集团名称
        if c and g:
            cust2grp[c.group(1)] = g.group(1)

# 补充Excel中42条映射（去重，以仪表盘为准）
# 结果：421条完整映射，覆盖丽珠→珠海丽禾、天隆→西安华伟等子公司归集
```

**效果**：丽珠集团从236万→237万（纳入珠海丽禾+8893），九州通集团从分散→111万（武汉九泽+天津艾维迪），总集团数从428降至396。

### JSON输出供HTML动态加载

```python
result = {
    "period": "2026年1-6月",
    "total": round(grand_total, 0),
    "group_count": len(sorted_groups),
    "top60": [
        {"rank": i+1, "group": g, "amount": round(v, 0),
         "pct": round(v/grand_total*100, 1)}
        for i, (g, v) in enumerate(top60)
    ]
}
# 保存到 /数据/top60_groups.json
# HTML 通过 fetch('/数据/top60_groups.json') 加载 → 渲染表格+份额条
```

### 多Sheet处理

May文件可能有多个Sheet（Sheet1=明细236行, Sheet2=汇总24行），需指定 `sheet_name='Sheet1'`。

June文件可能是客户级汇总（列=客户名称/购买总金额），非逐笔明细——直接用。

### 列名歧义

ERP文件中 `客户` 列可能匹配到 `客户物料规格型号#`。精确匹配：
```python
for c in df.columns:
    if '客户' in str(c) and '物料' not in str(c): cust_col = c
```


## 客户集团v26增量更新（regex解析+匹配+回写）— 2026-06-27 新增

当用户提供1-6月客户级订单明细（含客户名称/金额），需要更新 `customerGroups` 数组中现有集团的v26值时：

```python
import re

# 1. 读取HTML，定位 customerGroups 数组
start = html.find("const customerGroups = [")
end = html.find("];", start) + 2
old_block = html[start:end]

# 2. 正则解析现有条目
entries = re.findall(
    r"\{name:'([^']+)',v24:([\d.]+),v25:([\d.]+),v26:([\d.]+),ann:([\d.]+),grade:'([^']+)'\}", 
    old_block
)

# 3. 从订单数据聚合客户→集团（关键词匹配：丽珠→丽珠集团, 圣湘→圣湘集团, etc.）
# 建立 v26_updates = {"丽珠集团": 236.4, ...}

# 4. 重建数组，更新v26，重算ann（1-6月→年化用 v26×2）
new_lines = []
for name, v24, v25, v26_old, ann_old, grade in entries:
    if name in v26_updates:
        v26_new = v26_updates[name]
        ann_new = round(v26_new * 2, 1)  # 半年度×2
        new_lines.append(f"...v26:{v26_new},ann:{ann_new}...")
    else:
        new_lines.append(f"...")  # 保留原值
new_block = "const customerGroups = [\n" + ",\n".join(new_lines) + "\n];"

# 5. 回写
html = html[:start] + new_block + html[end:]
```

**关键注意**：
- 用 `re.findall()` 而非逐行解析，因为原有数组格式统一（单行条目）
- 年化预估：对于1-6月数据，简化为 `v26 × 2`；1-5月数据使用季节因子法（÷0.338）
- 不要重建整个数组——保留v24/v25/grade等历史数据，仅更新v26和ann
- 更新后验证 div平衡 + JS语法
- 数据源：ERP + `/tmp/customer_mapping.json`
- customer_map 中每个客户的 `group` 字段用于聚合
- 排除 `exclude_customers`（宝锐关联公司+保密客户）
- 按年化预估（v26/0.343）排序，取TOP60
- 每条包含：name / v24 / v25 / v26 / ann / grade

### 客户分级逻辑

**⚠️ 铁律（2026-07-07）：分级以客户清单为唯一权威源。**

客户清单位置：`~/Desktop/Hermes输出-工作类/hermas工作台数据库/客户清单/诊断原料&生命科学客户清单YYYYMMDD.xls`

**KA 定义**：客户清单中 **Sheet 4「大客户销售部」** 列出的所有集团。不以营收阈值判定——即使某集团营收仅10万，只要在 Sheet 4 中即为 KA。当前17个KA集团：万孚/东方基因/中道/丽珠/亚能生物/佰奥莱博/圣湘/复星/天隆/安图/明德/易研/欣中生物/海尔施/爱森/牧原/许昌学院。

**A/B/C 定义**（非KA集团按年化营收）：
- A：年化预估 ≥ 50万
- B：年化预估 10-50万
- C：年化预估 < 10万

> ❌ 废弃定义：~~KA级 = 大客户销售部 + 年化预估进入前70%的集团~~ — 此定义混淆了部门归属和营收分级，已废弃。

### 暗色仪表盘→白底蓝调转换（2026-07-04）

诊断原料作战室为截图到PPT做了白底转换。转换矩阵：

| 元素 | 暗色 | 白底 |
|------|------|------|
| 背景 | `#0a1628→#0f1f38渐变` | `#f0f2f5` |
| 卡片 | `rgba(15,31,56,.85) + blur` | `#fff + box-shadow` |
| 主色 | `#4fc3f7` | `#1a73e8` (宝锐蓝) |
| 文字 | `#e0e8f0` | `#202124` |
| 边框 | `rgba(79,195,247,.15)` | `#e0e0e0` |
| 顶栏 | 暗色毛玻璃52px | 白色48px + Logo |

图表色同步调整：2024=灰`#9e9e9e`，2025=金橙`#f4a236`，2026=蓝`#1a73e8`。表格：白底+浅灰表头+蓝边框。用Python一次性replace全量CSS变量+JS硬编码颜色，改后`node --check`验证。

### 回款高风险 TOP10
- 数据源：`回款/2026年X月逾期应收回款计划-总表-*.xlsx`
- 解析时注意：Col8=逾期天数（可能为'-'表示未逾期），Col9=逾期金额
- 逾期天数>60红底背景、>30黄底、<30白底

---


## 年化预估计算方法论（2026-06-09 修正）

**⚠️ 错误公式（已废弃）**：`t25×0.7 + t24×0.3`（加权历史年份均值）
- 问题：完全没有用到 2026 年实际数据，2026年1-5月=1798万但年化仅1343万——比五个月实际还低

**正确方法一：简单年化法**
- `2026 1-5月 × 12/5` = 线性外推至12个月
- 优点：最透明；缺点：忽略季节性（H2通常大于H1）

**正确方法二：季节因子法（推荐用于仪表盘主数字）**
- 计算历史1-5月/全年比例：
  - 2024: 1-5月=1252万, 全年=3377万, 占比=37.1%
  - 2025: 1-5月=1367万, 全年=4221万, 占比=32.4%
- 加权比例 = 32.4%×0.7 + 37.1%×0.3 = 33.8%
- 2026年化 = 2026 1-5月 ÷ 33.8%
- KPI卡片同时展示两种方法（季节因子为主，简单年化为参考）

**2026年化预估参数（截至5月）**：
| 方法 | 公式 | 结果 | 6-12月预估 |
|------|------|------|-----------|
| 季节因子 | 1798万÷33.8% | 5320万 | 3522万 |
| 简单年化 | 1798万×12/5 | 4316万 | 2518万 |


## 年化同比计算（2026-07-04）

**⚠️ 必须用年化值对比，不能直接半年vs全年**。

正确公式：`同比% = (Y26×2 - Y25全年) / Y25全年 × 100`

错误公式（不可用）：`同比% = (Y26 - Y25) / Y25 × 100`
→ 半年vs全年跨口径比较导致虚低（如某客户25全年=82w, 26H1=19w → 简单同比=-76%，但年化后=-53%更合理）。

用于潜力股筛选的增长型阈值：年化同比>50%或新增（Y25=0且Y26≥5w）。


## 潜力股自动筛选（2026-07-04）

在非重点客户中识别未来可升级为重点的客户。详见 `references/customer-tier-system.md`。

核心筛选逻辑：
```python
potential = {}
for c in non_key_customers:
    if c['amount'] >= 300000:
        potential.add(c)  # 规模型：累计≥30w
    y26a = c['y2026'] * 2  # 年化
    if c['y2026'] >= 50000:
        if c['y2025'] == 0 or (y26a - c['y2025'])/c['y2025']*100 > 50:
            potential.add(c)  # 增长型：年化同比>50%或新增
```

输出 `diag_potential.json`（含 yoy、y26_annual、tags），作战室销售总监视图底部渲染。


## 集团合并工作流（2026-07-04）

当用户要求将多家子公司合并为同一集团时：

1. 搜索 `diag_customers.json` 中含关键字的客户
2. 合并：amount/orders/y2024/y2025/y2026 求和；salespeople 取并集；first_date 取最早；last_date 取最晚；status 取最活跃
3. 删除旧条目，添加合并后集团条目
4. 在 `diag_mappings.json` 的 `cust_to_group` 中加入映射
5. 更新 `diag_summary.json` 的 total_customers
6. **务必重建 LTC 和潜力股**（客户列表变化会影响分级）

已合并案例：默乐集团（江苏+浙江默乐，27w）、格诺集团（江苏格诺，13w，已破产）、生凌集团（4家生凌，250w）。

独立模块，展示三年1-5月逐月同比，放在客户分析仪表盘或综合仪表盘中。

**数据准备**：
```python
# 从三个全年文件按集团×月聚合营收和订单数
yoyRev = [(2024各月), (2025各月), (2026各月)]  # 各5个元素
yoyOrders = [(2024各月), (2025各月), (2026各月)]
# 合计行：sum() 计算三年各自的1-5月合计
# 同比列：(2026合计-2025合计)/2025合计*100, 同理2025 vs 2024
```

**HTML结构**：两个表格上下排列（营收表 + 订单数表），每表 3行×6列数据
**渲染函数**：`renderYoy()` 在 `window.onload` 时调用，DOM操作填充 `<table>` 的 `<tbody>`
**格式**：营收用万元（1位小数），订单用整数，同比正数绿色/负数红色，合计行加粗底色
**JS关键点**：两组变量 `yoyRev` / `yoyOrders`，top-level 声明，用 `onclick` 触发重建


### 订单三分法分类统计（销售/测试/售后/赠品）

基于订单Excel的列标志将订单分为四类，按部门（大客户部/销售拓展部/合计）统计。

**⚠️ 关键：必须先按单据编号去重，再分类！**
- 同一单据编号对应多行产品线（例如：一个订单含3个SKU=3行）
- **去重方法**：以 `单据编号`（Col2）为key聚合，同一单据算1单
- 去重前后差异巨大：2026年正常订单从3532行→1213单（约3倍虚高）
- 所有GTA统计也必须去重：测试6987→1218、赠品664→69、售后103→39（2024年1-5月）

**分类规则（按优先级判断，测试 > 售后 > 赠品 > 正常）**：
| 类型 | 识别列 | 识别逻辑 | 统计方式 |
|------|--------|---------|---------|
| 测试单 | Col1 `单据类型` | 值=`"测试销售订单"` | 仅笔数（金额=0） |
| 售后单 | Col19 `合同编号#` | 包含 `"（售后）"` | 仅笔数（全部金额=0） |
| 赠品单 | Col13 `是否赠品` | 值=`"是"` | 仅笔数（金额=0） |
| 销售单 | - | 以上三种都不匹配 | 金额+笔数 |

**⚠️ GTA分类顺序陷阱（2025-06-09修正）**：
- **必须将测试单放在if/elif链的最前面**，否则会被赠品分支吞掉
- 原因：测试销售订单的 `是否赠品` 列也标记为"是"（测试=免费样品=赠品），如果先判断赠品，所有测试单会被错归为赠品
- 正确顺序：`if '测试' in doc: test` → `elif '售后' in contract: after` → `elif is_gift=='是': gift` → `else: normal`

**⚠️ 售后识别关键陷阱（已在2025-06-09修正）**：
- 原始规则（`amt != 0` 过滤）在 2025/2026 年数据中**完全失效**：
  - 2025年：129行含"售后"合同，仅10行有非零金额（5.7万），其余119行金额=0
  - 2026年：59行含"售后"合同，**全部金额=0**
- **正确做法**：统计**所有**含"售后"的合同行（含零金额），仅统计笔数，不统计金额
- `单据类型`列**不区分**售后（所有售后单仍是"标准销售订单"），只能用合同编号#识别

**部门映射**：
- 大客户部 = {黄明月, 赵云浩, 刘新元, 王昕伟}
- 销售拓展部 = 其余国内销售（冯贤平, 刘虹晶, 路要峰, 宋学伟, 杨天雄, 陈溪子, 黄文才, 林潇, 龙友波, 任鑫, 李江辉, 乔富刚, 刘欣, 李丽佳, 杜建, 王星茗, 黄自鑫, 吴云, 李岚风, 郑鹏飞, 束义彪, 尚猛, 黄晗, 黄启幸, 黄文清）
- 不在以上两组的（王慧洁→生命科学部等）不计入合计

**提取脚本模式**：
```python
import openpyxl
from collections import defaultdict
from datetime import datetime

def classify_orders(filepath):
    """按 销售/测试/售后 三级分类，按月聚合"""
    result = {
        '销售': {'amt': defaultdict(float), 'cnt': defaultdict(int)},
        '测试': {'cnt': defaultdict(int)},  # 金额为0，仅计数
        '售后': {'amt': defaultdict(float), 'cnt': defaultdict(int)}
    }
    wb = openpyxl.load_workbook(filepath, read_only=True, data_only=True)
    ws = wb.active
    for row in ws.iter_rows(min_row=2, values_only=True):
        d = row[0]  # 合同日期（2024/2025全年文件Col0, ERP文件Col1）
        if not isinstance(d, datetime): continue
        m = int(d.strftime('%m'))
        doc = str(row[1]).strip() if row[1] else ''    # 单据类型
        contract = str(row[19]).strip() if row[19] else ''  # 合同编号# (Col19→0-based)
        amt = float(row[11]) if row[11] else 0          # 价税合计
        
        if '售后' in contract:       # 全部含"售后"行不管金额（2025/2026全为零）\n            result['售后']['cnt'][m] += 1
        elif '测试' in doc:
            result['测试']['cnt'][m] += 1
        else:
            result['销售']['amt'][m] += amt
            result['销售']['cnt'][m] += 1
    return result
```

**跨文件列结构差异**：
| 文件 | 日期列 | 单据类型 | 客户 | 区域 | 销售员 | 金额 | 合同编号# | 是否赠品 |
|------|--------|---------|------|------|--------|------|----------|---------|
| 2024/2025全年订单(新版) | Col0 | Col1 | Col3 | Col4 | Col5 | Col12 | Col20 | Col14 |
| 2024/2025全年订单(旧版) | Col0 | Col1 | Col3 | Col23(区域#) | Col4 | Col11 | Col19 | Col13 |
| ERP文件(2026) | Col1 | Col2 | Col0 | - | - | Col3 | Col18 | - |

**⚠️ 用户可能随时重新组织Excel列结构**（新增/移位列），每次加载订单文件时必须先用 `values_only=True` 读取第一行确认列头：
```python
row = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
for i, v in enumerate(row):
    if v: print(f"Col {i}: '{v}'")
```
新版订单文件（~2026年6月起）：Col4='区域'(新E列), Col5='销售员', Col12='价税合计', Col14='是否赠品', Col20='合同编号#'

**HTML注入模式**：
- 在三年同期模块下方新增独立区域，两张全宽表格上下排列
- **测试单表**：9列（月份 | 2024-大客户 | 2024-拓展 | 2024-合计 | 2025-大客户 | 2025-拓展 | 2025-合计 | 2026-大客户 | 2026-拓展 | 2026-合计）
- **售后单表**：同上9列结构，注：仅统计笔数（金额全为零）
- 合计行直接内联求和解算
- JS数据嵌入 `renderYoy()` 函数末尾，数据内联为数组字面量

**JS渲染关键点**：
- 用 `for(var i=0;i<5;i++)` 循环展开5个月，`(i+1)+'月'`
- 每月三组列（大客户/拓展/合计），合计列加 `font-weight:600` 粗体
- 每月三组合计行的 rendering 在后一代码块中计算 `total24=t24KA[i]+t24XS[i]`
- 合计行单独计算 sum 全部5个月的值

**部门映射接入方式**（从工作汇报或综合仪表盘现有数据中获取）：
- 大客户部 = {黄明月, 赵云浩, 刘新元, 王昕伟}
- 销售拓展部 = 全部国内区域销售（华北/华东/华南/华西/华中/南区）加 上海X+Z
- 不在以上两组的销售员（如生命科学部、国际业务部、生物医药部）不计入合计

### 货号聚合陷阱

**⚠️ 物料编码(Col7) ≠ 货号#(Col19)**：
- 产品SKU聚合**必须用Col19(货号#)**，不能用Col7(物料编码)
- 同一货号可能有多个物料编码（不同规格/批次），用物料编码聚合会导致同一产品被拆散，客户数被严重低估
- 例如：E20(Robustart Taq QS Ⅱ)用Col7聚合显示1客户，用Col19正确聚合后显示28客户
- Col19约98%行有值，空值时fallback到Col7
