# ERP 增量入库标准流程（提交方）

## 何时用

用户说「更新 ERP 数据 / 更新工作台全套数据」并传来一个 `销售订单_<起>-<止>.xlsx`。**不要退回要求重传全年文件**（用户会反复传增量），直接按下面五步合并入库。

前置认知：ERP 导出永远是「到某个日期为止」的快照，所以「数据到今天」永远是伪命题 —— 汇报时必须说明实际覆盖到哪个日期。

---

## ① 体检：先看清现状，别急着动

```bash
/usr/bin/python3 脚本/erp_preflight.py
```

输出：`数据/每日上传/` 与 `月度销售额数据/` 的 xlsx（按 mtime）、现用文件的行数与合同日期范围、8 个派生 JSON 的 mtime 与关键值、收件箱、8765 健康。

判读重点：
- `find_latest_erp()` 实际选中哪个文件（**不是**名字最新，是 mtime 最新）
- 覆盖截止日 vs 今天差几天 —— 这就是「缺多少数据」
- 若 `erp_amount.json` 的最新日比现用文件的截止日还晚，说明此前管道用过另一个文件；以 `find_latest_erp()` 现选中的为准

## ② 拍基线快照（回归校验的锚点）

```bash
/usr/bin/python3 脚本/erp_baseline.py      # 落盘 数据/_erp_baseline_snapshot.json
```

输出逐月总额 / 部门×月 / 个人×月（仅 7 月起）。每次更新前把上一版另存为 `数据/_erp_baseline_<旧截止日>.json`，便于事后回看。

## ③ 核验增量文件结构

```bash
/usr/bin/python3 脚本/erp_inspect_increment.py
```

必看：行数、列数、**列结构是否与全年文件一致**（`assert list(old.columns)==list(new.columns)`，不一致先对齐再拼）、合同日期 min/max 与按日分布、单据类型/单据状态分布、与旧文件的重叠行数。

`合同日期='合计'` 的汇总行按日期区间过滤会被自然剔除，无需单独处理。

## ④ 合并 + 就地回归校验

```bash
/usr/bin/python3 脚本/erp_merge_increment.py     # 末尾必须打印 MERGE_OK=1
```

规则：
- **切分点 = 增量文件的最小日期**；旧件保留 `< 切分点` 的行，增量件提供 `[切分点, 最大日期]` 的**全部**行（含对重叠区行状态/金额的修订）
- 合并件存 `月度销售额数据/销售订单_20260101-YYYYMMDD.xlsx`（全年命名），写后 `os.utime` 保证 mtime 最新
- 原始增量归档 `月度销售额数据/_增量归档/`（子目录，非递归 listdir 扫不到）
- 断言 `find_latest_erp() == 合并件`，否则中止
- **回归校验**：逐月对比基线，历史月（< 最新月）差异必须 < 0.05万；只有最新月允许变化

`MERGE_OK=0` = 历史月被改动 → 停下排查切分点/列对齐/日期解析，**不准继续部署**。

## ⑤ 跑管道 → 部署 → 三层验证

```bash
/usr/bin/python3 脚本/refresh_erp.py            # kpi_progress 当月
cd 数据 && /usr/bin/python3 mcp_kpi_pipeline.py # kpi_dashboard(H1+H2) + three_year_monthly
cd .. && /usr/bin/python3 脚本/refresh_erp_amount.py   # erp_amount 按天/按客户
/usr/bin/python3 脚本/merge_workbench_data.py  # workbench_data.json（前端合并包）
# 部署 5 个：kpi_progress / kpi_dashboard / three_year_monthly / erp_amount / workbench_data
```

三层验证，缺一不可：

1. **派生 JSON 层**：`kpi_progress.month/total`、`kpi_dashboard.source`（应为当天日期）、三部门 `h2_actual_months[2]`（9月）、`three_year_monthly['2026']['9']`、`erp_amount.daily` 最新日 —— 五处数字必须互相自洽。
   **键名**：`kpi_dashboard` 用 `total_h1` / `total_h2_actual` / `total_h1_h2` / `depts[].target` / `depts[].h2_actual_months`（**没有** `total_h2`）；这些 JSON 的金额单位是**元**，打印 `/1e4` 后再贴「万」，别把元值标成万。
2. **本地 == CDN**：`curl -H 'Cache-Control: no-cache'`；**中文路径必须 `urllib.parse.quote('数据')`**（urllib 直接吃非 ASCII 路径会抛 `UnicodeEncodeError: 'ascii' codec can't encode`）
3. **前端实读**：本地 8765 打开工作台 → 看板「当月进展/累计达成」、订单管理「本月/本季 税合计」对得上。

   绕过登录实测的完整姿势（少一步看板就是空白）：`loginOverlay.style.display='none'` + `app.style.display='block'` + **`state.user = {name:'刘新元', role:'admin', dept:'销售管理', line:'da'}`** + `showApp()` + `loadData()`，等约 10s 再 `switchTab('dashboard'); render()`。**不设 `state.user` 时 `#main` 永远为空**（渲染分支依赖登录态），别误判成数据坏了。读结果用 `document.getElementById('main').innerText`，断言出现「当月进展 <数字>万」「累计达成」等字样。

### ⚠️ 前端全 0 的误判

绕过登录做实测时（`loginOverlay.style.display='none'` + `app.style.display='block'`）**必须手动调一次 `loadData()`** —— 它是在登录流程里被调用的，不调则 `state.data` 为空，看板所有数字显示 0。这**不是**数据坏了，别急着回滚。

### ⚠️ 部门卡分母口径

看板部门卡「204.1万/388.8万」的分母是**该部门当月目标**，不是上月实际（数值巧合很常见）。校验：三部门分母之和 == 当月总目标，且 == 顶部「目标 909.3万」。

## 顺带检查（收工前）

- 硬编码文件名：`grep -rnE "销售订单_20260101-[0-9]{8}" 脚本/*.py 数据/*.py | grep -v YYYYMMDD` → **只看带具体日期的那几行**。（裸 `销售订单_20260101` 会命中几百条 JSON 的 `source` 字段和合并件的输出名，那些是正常的，不要动。）命中即改：`sys.path.insert(0,'脚本'); import erp_sales_core; ERP_F = erp_sales_core.find_latest_erp()`，改完 `python3 -m py_compile` + 重跑该脚本确认产出正常。
- **`h2_months` 污染检查只认「7 月目标 ≈ 7 月实际」这一个信号，不要用权重比例判。** `h2_months` 是逐月累进舍入算出来的，`round(T×权重)`（T 由 `h2_months[1]/0.15` 反推）对三个部门**全部**不成立 → 一次跑出 3 个假阳性（实测）。正确做法：拿该部门/该人 7 月的 **ERP 实际**（基线快照 / `my_kpis.json`）与 `h2_months[0]` 比，**精确吻合**才是被实际值污染。
- 同理别用 `Σh2_months == target - h1_total` 判自洽：大客户部天然差一个「岗位离职后无人承接」的缺口（百万元级），差额大 ≠ 数据坏了。
- KPI cron / `cb_sync.sh` 每 2h 会重跑管道；确认它们读的是合并后的新文件（`source` 字段应是当天）
- **扫页面文本找模板残留标记**：前端实读时 `main.innerText` 里不得出现 `/* … */` / `__XXX__` 这类标记 —— 写进 HTML 字符串的注释会变成用户可见的文字（实测趋势卡标题旁裸露一个版本标记）。修法：删掉字符串里的注释 → 对提取的内联 JS 跑 `node --check` → 部署（`工具/index.html` → CDN `/workbench/index.html`）→ md5 比对本地与线上一致
