# ERP 文件上传 → KPI 更新管线

## 触发
- 用户在数据管理Tab上传 ERP/飞书导出 Excel (.xlsx/.xls/.csv)
- 或 agent 调用 `POST /api/upload`, `POST /api/upload/process`

## 三阶段管线

### 1. 质检 (`run_quality_check`)
- 读取文件头（首行），识别列名
- 类型识别：
  - 含"单据编号"+"价税合计" → `erp_order`
  - 含"活动"/"创建者" → `feishu_activity`
  - 其他 → `unknown`
- 关键列校验：`erp_order` 必须有"单据编号"、"价税合计"

### 2. 处理 (`process_uploaded_file`)
- 按扩展名选择解析器：`.csv` → `csv.DictReader`, `.xlsx` → `openpyxl`
- 定位关键列（模糊匹配：'单据编号'、'价税合计'/'税合计'、'销售员'/'下单人'）
- 逐行汇总：`by_sp[销售员] += 价税合计`
- 写入 `kpi_progress.json` → `merge_workbench_data.py` → `cb_sync.sh`

### 3. 归档
- 仅处理成功时归档
- 移至 `数据/上传/_已处理归档/`，加时间戳前缀
- 失败时文件保留在上传目录

## 🔴 合计行过滤（致命陷阱）

ERP 导出末尾有合计行（`合同日期='合计'`, `单据编号=None`）。

**特征**：
| 列 | 数据行 | 合计行 |
|----|--------|--------|
| 合同日期 | `2026-08-01` | `合计` |
| 单据编号 | `ZHBR260801002` | `None` |
| 销售员 | `路要峰` | `None` |
| 价税合计 | `720` | `1,156,903` |

**致命点**：`str(None)` = `'None'`（字面值，非空字符串），`not bn` 不会过滤。不处理会导致：
- "None" 被当成销售员名出现在 KPI 排行
- 合计行金额重复计入总额（翻倍）

**正确过滤**：
```python
bn = str(row.get(bn_col, '') or '').strip()
if not bn or bn == 'None' or '合计' in bn:
    continue  # 跳过合计行
```

## 🔴 openpyxl 陷阱

### `read_only=True` + `iter_rows()` 只返回首列
```python
# ❌ read_only=True 时 iter_rows 返回 ReadOnlyCell，for c in row 只取第一个
wb = openpyxl.load_workbook(fp, data_only=True, read_only=True)
headers = [str(c.value) for c in next(ws.iter_rows(min_row=1, max_row=1))[0]]
# → 只得到 ['合同日期']，不是27列！

# ✅ 去掉 read_only=True，用 values_only=True
wb = openpyxl.load_workbook(fp, data_only=True)
headers = [str(c) for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
# → 得到全部 27 列
```

### `iter_rows` 返回 tuple 不是 row 对象
```python
# ❌ 旧模式（返回 Cell 对象）
headers = [str(c.value) for c in next(ws.iter_rows(min_row=1, max_row=1))[0]]
#                                   需要 [0] 取出行 → 再遍历 Cell → .value

# ✅ 新模式（values_only=True 直接返回值）
headers = [str(c) for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
#                                   直接是可迭代的 tuple，无需 [0]
```

## MCP 订单数据 vs ERP 数据（金额对比）

| 数据源 | 整体准确性 | 测试单 | 预测单(FO) | 多行拆分 | 总计 |
|--------|:---:|:---:|:---:|:---:|:---:|
| **MCP** | ❌ 不可靠 | 100%为0 | 100%为0 | 部分行0 | 少数有值 |
| **ERP** | ✅ 权威源 | 金额为0 | 含金额 | 每行独立 | 完整 |

**铁律**：ERP 是金额唯一可靠源。MCP 订单数据混杂测试单+预测单+多行拆分+未填价，**严禁用于 KPI 计算**。

### MCP 订单类型识别（`单据类型` 字段）
| 单据类型 | label | 价税合计 | 前缀 |
|----------|-------|:---:|------|
| `khm3zkd9a` | 测试销售订单 | 必为0 | ZHBR |
| `7skbo_qss` | 标准销售订单 | 可能0或>0 | ZHBR |
| `dcm48do4j` | 预测订单 | 必为0 | FO |
