# 看板中文化改版与台账管理 CRUD 交付说明

时间：2026-06-12
访问：`https://wwyl.yipeng.online/goods-ledger/dashboard/`（口令同前）

## 1. 本次三项需求与落地

### 1.1 表头默认中文，悬停显示英文

- 新增 [frontend/src/fields.ts](../frontend/src/fields.ts)：约 120 个字段的中文字典 + 枚举值中文映射。
- 所有表格表头默认中文（虚线下划线提示可悬停），鼠标移上去 Tooltip 显示英文原始字段名。
- 枚举值（入库/出库、错误/警告、待处理/已解决、上下游方向、流水类型等）渲染为带色 Tag 中文。
- 总览饼图图例、图表轴标签同步中文化；统一搜索分组名中文化。

### 1.2 排版优化（按 huashu-design 思路，基于现有 Semi Design 体系改进）

- 顶栏 96px → 56px，侧栏 248px → 200px，卡距 24 → 16/12，指标卡与卡片内边距收紧。
- 图表不再独占整行：每页改为「图表 + 高优先表格」并排的两栏布局，纵向利用率明显提升。
- 去掉标题里的「（ECharts）」等技术词；数量/金额列右对齐 + 等宽数字；空值统一弱化显示。
- 品牌区改为中文「货品台账」。

### 1.3 台账管理（增删改查）

新增侧栏「台账管理」分区，12 个实体全开 CRUD：

```text
产品 / 组织对象 / 项目IP / 采购需求单 / 订单明细 / 渠道分配 /
库存SKU / 库存流水 / 签收单 / 签收明细 / 校验问题 / 库存账户
```

- 每个实体：搜索 + 分页 + 新增（弹窗双列表单）+ 编辑 + 删除（二次确认）。
- 表单按字段类型自动生成：文本/数字/日期/开关/枚举下拉（中文）/外键下拉（可搜索，显示业务名称而非 UUID）。
- 外键列在表格里显示引用对象名称（如订单明细显示所属订单号）。

## 2. 后端受控 CRUD 层

新增 [backend/app/admin_service.py](../backend/app/admin_service.py)：

- **白名单实体 + 白名单列**：只有注册表声明的表/字段可读写，不开放裸 SQL。
- 接口：`GET /api/admin/meta`（实体配置）、`GET /api/admin/{entity}`（列表+搜索+分页）、
  `GET /api/admin/refs/{entity}`（外键下拉）、`POST/PATCH/DELETE /api/admin/{entity}[/{id}]`。
- **全程审计**（沿用 api_write_requests + agent_write_audit 双表）：
  - update 记录逐字段 old→new diff；
  - delete 记录被删整行原值；
  - 操作人记录为 `dashboard:<登录用户名>`（nginx 从 Basic Auth 注入）。
- 删除遇外键引用返回 409 中文提示（如「该记录被订单明细引用」），不静默级联。
- 库存流水按用户决策：允许改/删，靠审计追溯（未做冲正制）。

## 3. 鉴权与部署

- 新增独立 `LEDGER_UI_API_KEY`（云端 .env，与机器人 key 分离、可独立轮换）。
- nginx `/goods-ledger/` location 注入 `X-Ledger-API-Key`（UI key）和 `X-Ledger-Actor: dashboard:$remote_user`——运营登录看板即可直接写入，无需手动配 key。后端 `_require_write_key` 接受两把钥匙。
- snippet 权限 640；配置留档同步至 [deploy/nginx-goods-ledger-locations.conf](../deploy/nginx-goods-ledger-locations.conf)。

## 4. 验证记录

- 后端：import ok（12 实体）、/health 200、/api/admin/meta 200。
- CRUD 闭环（经公网+口令，证明 nginx 注入生效）：
  - 新增组织对象 → 200 返回 id；
  - PATCH 改名 → `changed_fields: [display_name, short_name]`；
  - 列表搜索命中；DELETE → `deleted: true`；删后搜索 total=0。
- 审计：三次操作均落 `agent_write_audit`，actor=`dashboard:ledger`，update diff 完整。
- 前端构建：tsc + vite 通过；Playwright 实测三页截图，**控制台报错 0**。
- 旧版前端已备份：云端 `frontend/dist.bak-20260612_153854`。

## 5. 已知边界

- 删除是物理删除（带审计），不是软删除；库存流水改删后当前库存即时重算（视图实时计算）。
- 渠道分配改数后系统不会自动校正订单明细的渠道合计，差异会出现在「渠道分配差异」表中——这是有意设计（不静默修正）。
- 创建采购订单/签收单时建议仍优先走机器人受控接口（会自动建明细+渠道分配）；台账管理更适合单条修正与补录。

## 6. 第二轮重排（2026-06-12 下午，按用户反馈）

用户反馈：排版大大小小、内容为了有而存在（表行数/文件条数对人无意义）。按 huashu-design「每个元素必须挣到位置」原则整页重排：

### 信息架构改为按功能组织

- 「总览」→「**工作台**」：只回答"今天要处理什么"——缺口产品、待处理问题、零/负库存、未签收发货单（四个可点击 KPI，点击跳转对应页面）+ 缺口/正差额排名 + 问题清单 + 采购规模。
- **删除的 data slop**：表行数饼图、源文件列表、写入审计计数、单值饼图（签收状态只有1种值）、双柱条形图（只有2根柱子）。
- 新后端接口 `GET /api/dashboard/workbench`：按工作台口径直接算好（缺口排名、正差额排名、问题Top、库存健康、未签收数、上下游金额）。
- 缺口为 0 时工作台自动改显「正差额排名（自留库存候选）」——对账的另一半，不留空区块。

### 视觉系统收口（解决"大大小小"）

- 字号全站 5 档：12（标签）/ 13（正文表格）/ 15（区块标题）/ 18（页标题）/ 24（指标值）。
- 单主色 `#2563eb` + 三个语义色（红=缺口异常、橙=待办、绿=正常），语义色只用在有含义的数值上；废除五彩指标卡、渐变背景光斑、渐变品牌块。
- 统一区块组件 Section（同边框/圆角/标题栏）与 Stat 指标卡（等高网格）。
- 倾斜轴柱状图全部换成「横向排名条 RankBar」（名称+条+数值+副注，中文场景更易读）；ECharts 仅保留渠道页的分组对比一处。
- 表格只留决策相关列（如订单列表：订单号/方向/买卖方/日期/到货/状态）。

### 验证

- 全部 6 个看板页 Playwright 截图肉眼自检，控制台报错 0。

## 7. 第三轮：操作式修改与细节修正（2026-06-12 晚，按用户反馈）

用户反馈：单元格文字溢出、编码不应可改且新增应自动生成、渠道分配应"从A调拨到B"而非裸改表、内容未占满屏幕。

### 操作式修改（核心变化：用算法承接修改意图）

- **渠道分配**不再提供原始行编辑：改为按订单明细分组的总览（明细数量/未分配/渠道分布 chips），操作只有两个——
  - 「调拨」：选从渠道（显示现量）→ 调到渠道（可选可新输）→ 数量（上限=现量）→ 必填原因。后端 `POST /api/admin/ops/channel-transfer` 在一个事务里同时调整两行，总量天然守恒。
  - 「分配」：把未分配余量分给某渠道，上限=余量，`POST /api/admin/ops/channel-allocate`。
  - 后端直接拒绝渠道分配的裸增/裸改（409 提示走操作）。
- **库存流水**：
  - 新增改为「登记入库/出库」弹窗：选SKU即显示当前库存，类型按方向过滤，复用 `/api/write/stock-movements`（库存不足自动拒绝并записать校验问题）。
  - 新增「冲正」操作：`POST /api/admin/ops/stock-reverse` 生成等量反向流水（related_movement_id 关联，重复冲正被拒），原流水保留可追溯。
  - 关键列（SKU/方向/类型/数量）登记后锁定（🔒），编辑只能改物流/备注等元数据；数量错了用冲正。
- **编码列**（SKU编码/对象编码/订单编码/流水编码等8个）：新增时自动生成（忽略传入值），编辑时后端剥离不可改，表单中不再出现。

### 细节修正

- 表格全局 `table-layout: fixed` + 单元格省略号，悬停显示完整内容（原生 title）；名称/说明类列固定更宽。
- `content-area` 去掉 max-width，内容占满屏幕宽度。
- 发现并处理 `channel_allocations.adjusted_quantity` 是**生成列**（=original+adjustment），调拨/分配 SQL 不写该列由库自动计算。

### 验证（全部经公网真实执行）

- 渠道调拨闭环：馆内100→90 + 新渠道10，合计恒为170；回转恢复100；归零临时行删除；超量调拨被拒（"当前数量100，不足以调出99999"）。
- 库存闭环：登记入库1件→冲正→库存回到75；重复冲正被拒；超量出库被拒并记录校验问题（测试问题已标忽略）。
- 编码保护：新增不传码自动生成 SKU-*；PATCH sku_code 返回"没有可更新的字段"。
- 500 故障安全：生成列问题导致的失败全程事务回滚，数据零损坏。
- UI：1680 宽截图验收，内容占满；控制台报错 0。

## 8. 客户维度筛选 + 渠道新增（2026-06-12 晚）

### 客户筛选

- 顶栏新增「客户」下拉：全部客户 / 国博 / 良渚（数据源=projects 表，以后新增博物馆客户在台账管理「项目/IP」里建即可自动出现）。
- 全部 6 个看板接口（workbench/inventory/orders/channels/shipments/reconciliation）支持 `project_id` 参数；过滤路径：订单/签收/对账视图直接 project_id，库存经 stock_items→inventory_accounts，渠道经 order_lines→purchase_orders。
- 验证：全部在库 1900 = 国博 1450 + 良渚 450；西泠未签收单归良渚；上游金额 374,148 全在国博；良渚无订单显示 0（如实）。
- 已知边界：校验问题表无项目字段，「待处理问题」数暂不随客户过滤（全局值）。

### 渠道新增

- 新接口 `GET /api/admin/ops/channels`：历史分配渠道 ∪ 组织对象中类型=渠道/平台的名称。
- 调拨/分配弹窗的渠道下拉用该接口 + 支持直接输入新渠道名（allowCreate，首次使用即创建）；也可在台账管理「组织/对象」新建类型=渠道的对象进入选项。

## 9. 台账管理分组重构（2026-06-12 晚）

按业务域把 12 个实体拆成 5 组，两级导航（组 → 总览/实体子菜单）：

| 组 | 实体 | 关键动作 |
|---|---|---|
| 基础资料 | 产品 / 组织对象 / 项目IP | 新增产品 / 新增组织对象 / 新增项目客户 |
| 采购 | 采购需求单 / 订单明细 / 渠道分配 | 新增采购单 / 新增明细 / 渠道调拨分配 |
| 库存 | 库存账户 / 库存SKU / 库存流水 | 登记入库出库 / 新建SKU |
| 发货签收 | 签收单 / 签收明细 | 新增签收单 / 新增明细 |
| 对账与问题 | 校验问题 | 新增校验问题（**对账单管理逻辑预留，待用户补充**） |

- 每组默认落在「总览」：只放可行动的数字（如基础资料显示"25 个产品缺编码"、采购显示"渠道分配差异 20 → 用调拨处理"、库存显示负库存、签收显示异常明细），下方一排关键动作按钮直达新增/登记/调拨。
- 新接口 `GET /api/admin/ops/group-overview` 一次返回 5 组统计；任何写操作后总览与表格同步刷新。

## 10. 采购→签收闭环与超时管理（2026-06-12 深夜）

### 闭环关联

- DDL：`shipment_signoffs.purchase_order_id`（FK→purchase_orders + 索引），迁移 `schema/migration_20260612_signoff_po_link.sql`。
- 新建签收单**必填「对应采购单」**（下拉选订单号），缺失被拒：`缺少必填字段: 对应采购单`。
- 历史西泠签收单未关联（建系统前的记录），可在台账管理补填。

### 到货日期规范化

- 采购单表单新增「预计到货(起/止)」**真日期**字段（日期选择器），原模糊文本降级为"文本备注"。
- 回填脚本 `scripts/backfill_arrival_dates.py`：把"5月中旬/6月下旬"等解析成日期区间（上旬1-10/中旬11-20/下旬21-月末，跨年自动+1），只填空+审计。已回填 4 单 + 19 明细行。

### 未签收追踪与超时强调

- 口径：状态≠已取消、履约状态≠已完成、**没有任何已签收状态的关联签收单** → 进入追踪；`预计到货(止) < 今天` → 超时（天数=差值）。
- 订单页新增「未签收订单追踪」表（超时红色 Tag 置顶排序）；工作台 KPI 卡改为「未签收采购订单」（超时则红色+"其中 N 单已超过预计到货"），点击跳订单页。支持客户筛选。
- 验证：13 单未签收、2 单超时（2月中旬→112天、5月中旬→23天，按 2026-06-12 计算）。
- 运营消单方式：给订单建已签收的签收单，或把订单「履约状态」填 fulfilled/completed/已完成，或状态改已取消。

### 其他

- 所有实体表格的「状态」列固定右侧（紧贴操作列），采购单首当其冲。
- 部署脚本修正：构建失败时不再误部署旧产物（以 dist/index.html 存在为门禁）。

## 11. 签收确认自动入库（2026-06-12 深夜，补全关联性）

用户指出：原表结构按 Excel 导入反推，依赖关系有缺失；做系统要全面关联。签收后应自动增加库存并对应所在位置。

### 补全的依赖链

```text
采购单 ←(purchase_order_id)── 签收单 ──(inventory_account_id)→ 库存账户(所在位置)
                                │
                          签收明细 ──(stock_movement_id, v1已设计本次启用)→ 入库流水 → 当前库存
                                └──(product_id)→ 产品 →(自动建)→ 库存SKU
```

- DDL：`shipment_signoffs.inventory_account_id`（迁移 migration_20260612_signoff_auto_stock_in.sql）。
- 新操作 `POST /api/admin/ops/signoff-confirm`：签收单行上的绿色✓按钮 →填签收人/日期→
  按明细 signed_quantity 自动生成 purchase_in 入库流水到指定账户；该账户没有对应产品的库存SKU时**按产品自动创建**；
  明细行回写 stock_movement_id（**幂等**，重复确认自动跳过已入库行）；签收单置 signed。
- 防错校验：未关联采购单/未指定入库账户/明细未绑定标准产品 → 中文报错且整体回滚；
  仅支持「下游供应商采购」到货签收（向客户发货走出库登记，避免重复记账）。

### 验证（真实闭环+清理复原）

```text
缺入库账户确认 → 409 提示补填
确认 → movements_created=1，库存 380→381
重复确认 → created=0 skipped=1（幂等）
清理删除后 → 库存复原 380，审计链 6 条完整
```

## 12. 地址簿 / 主子表展开 / 顺丰追踪（2026-06-12 深夜二）

### 收货地址簿

- 新表 `delivery_addresses`（migration_20260612_delivery_addresses.sql）：名称标签/收货人/电话(脱敏)/地址/常用物流/关联对象/默认。
- 台账管理「基础资料」组新增「收货地址簿」实体（总览含计数 + 新增动作）。
- **采购单 / 签收单的新增与编辑弹窗顶部出现「常用收货地址」选择器**：选中自动填入收货人、收货地址、物流公司，不再重复手填。

### 主子表展开（明细不再平级）

- 采购/需求单、签收单表格行首有展开箭头：**点开直接看该单的明细行**（嵌套表格，行号排序，可在展开区内添加/编辑/删除明细，新明细自动挂到该主单且所属单字段锁定）。
- 「订单明细」「签收明细」从子菜单移除；后端列表接口支持受控父子过滤（`ref_col/ref_id` 仅允许注册表声明的 ref 列，非法列 400）。

### 顺丰物流追踪

- 所有表格（台账+看板）中物流单号有值时显示「追踪」按钮：**自动复制运单号 + 新标签页打开顺丰官网查询页**，粘贴即查。
- 已核实（WebSearch）：顺丰官网查询页无公开的带单号直链参数，且查询需收/寄件人手机后四位验证（隐私门槛在顺丰侧），故采用"复制+跳转"为最可行方案。来源：[顺丰查快递](https://www.sf-express.com/chn/sc/waybill/list)。

## 13. 列宽修正 + 拖拽调宽（2026-06-12 深夜三）

- 排查所有编码/单号类字段实际长度：自动编码=前缀+20位时间戳（23-25字符），签收单编码19字符，物流单号14字符+追踪按钮。
- 统一列宽规则（台账+看板两处同步）：`*code` 列 230px、`*_no` 列 170-180px、物流单号 200px（含按钮）、69码 140px——这类定长内容不再省略号截断。
- 全部数据表（主表/展开子表/渠道操作表/看板表/追踪表）开启 Semi Table `resizable`：**列宽可手动拖拽调整**。
- 验证：最长编码 MV-20260612092353040569 完整显示，拖拽手柄正常。

## 14. 级联删除事故与删除护栏（2026-06-12 晚）

### 事故（如实记录）

排查用户反馈「删除报错不弹出」时发现根因：**v1 schema 的子表外键是 ON DELETE CASCADE**——删除主记录不会报错，而是静默连带删除子数据。这正是"看不到报错"的原因。排查过程中的两条真实删除测试造成损失：PO-0009（元浪 2026-01-13 单）+5 明细 +15 渠道分配；SKU 国宝萌趣（随机）+1 入库流水。

### 恢复（经用户批准执行）

- 全部 23 行从 `data/normalized_v1` 原始导出 CSV 按原 UUID 插回（排除生成列），重放 settlement/到货日期回填，写恢复审计。
- 行数核验复原：13/42/80/13/27 ✓。

### 根治：删除前引用护栏

- `delete_row` 删除前反查引用（注册表 ref 反查 + EXTRA_REFS 补充注册表外的已知 FK：签收明细入库关联、冲正关联、产品别名、对账明细、渠道对象、项目主体），有引用即 409，**应用层挡住级联**：
  `无法删除：该产品正被以下数据引用——订单明细 2 条、库存SKU 1 条、对账明细 2 条。请先删除或改挂这些关联记录`
- 前端删除失败改用 **Modal.error 弹窗**展示引用明细（不再是一闪而过的 Toast）。
- 附带修复：parties.party_type 标必填（与 DB NOT NULL 对齐）；NOT NULL/CHECK 违反转中文 409 不再 500。
- 验证：删有明细采购单/有流水SKU/有引用产品均 409 列明引用；无引用记录正常删除 200。

### 教训

- 删除路径测试必须用专门构造的临时数据，不碰真实业务行。
- 后续建议：把核心子表 FK 从 CASCADE 改为 RESTRICT（需 DDL+迁移评估），数据库层也挡住。

## 15. 发货单字段（2026-06-12 晚，参考真实《天猫补货发货单》）

样例归档：`data/source_samples/天猫补货发货单_钱袋子杜邦纸斜挎包66.xlsx`

发货单与签收单是同一业务单的两个阶段（开单发货→对方签收），统一在「发货/签收单」实体（已改名），不另建平行表。字段映射：

| 单据字段 | 处理 |
|---|---|
| 收货方/供货商/物流单号/发货日期/产品/数量/对应采购单 | 已有闭环字段 |
| 国博编码(必填)/69码(必填) | 从产品主数据带出，不重复存——**强化了产品编码补录的优先级** |
| 结算方式(代销/经销) | 新增闭环字段（下拉） |
| 联系人姓名/电话、对方采购负责人、产品经理、单据抬头 | 新增**预留字段**（版式需要，暂不参与计算） |
| 发货人/收货人签章 | 纸面动作，留给后续打印导出模板 |

迁移：`schema/migration_20260612_dispatch_note_fields.sql`。已验证按真实单据值（代销/朱木雨/雅楠/文创集团）建单读回一致。

## 16. 独立发货表与交叉核对（2026-06-12 深夜四，按用户澄清重构）

用户澄清：发货单要独立成表，发货签收核对通过两表交叉对比；发货单从采购单快捷创建并导出 Excel。

- 新表 `dispatch_notes` + `dispatch_note_lines`（migration_20260612_dispatch_notes.sql），**外键 ON DELETE RESTRICT**（吸取级联事故教训）；签收单加 `dispatch_note_id` 关联发货单。
- **从采购单一键生成**：采购单行内「生成发货单」按钮（POST ops/dispatch-from-order）——单头自动带出供货商/收货方/结算方式/地址，明细复制订单明细（含来源行追溯），生成后自动跳到发货单页。
- **导出 Excel**：发货单行内下载按钮（GET ops/dispatch-export/{id}，openpyxl 按真实《补货发货单》版式：抬头/收货方/物流/供货商/联系人/采购负责人/结算+日期/明细表/合计/签章区），国博编码/69码从产品主数据带出。
- **交叉核对**：发货签收组总览内置「发货 vs 签收 核对表」（GET ops/dispatch-check）：按采购单对比发货单数/发货量 vs 签收单数/签收量，差异非零橙色高亮。
- 上一轮加在签收单上的发货侧字段（结算/联系人等）已移到发货单表单；签收单恢复精简并增加「关联发货单」。
- E2E 验证：YL2026042801 一键生成 6 行（4810件）→ 导出 Excel 版式与原单一致 → 核对表显示 发货4810 vs 签收0 差异4810 → 测试单清理后核对表回空。

## 17. 对账单两层结算状态机（2026-06-12 深夜五，按用户口述逻辑）

### 业务规则（用户定义）

上游公司主动发来对账单（实际销售的产品/数量/渠道/结算日期）→ 我们入库记录并标记上游是否已结算；
我们主动发起下游结算 → 系统筛选「上游已结算且下游未结算」弹窗汇总 → 确认生成下游总对账单并**冻结**涉及的上游单（防反复筛选）→ 下游结算完成 → 同步上游单**结清**。

### 实现

- 新表（migration_20260612_settlement_statements.sql，FK RESTRICT）：
  `upstream_statements`（状态机 pending→settled→frozen→cleared，frozen 由 downstream_statement_id 挂接实现）
  + `upstream_statement_lines`（产品/渠道/数量/结算单价/金额）+ `downstream_statements`（draft→settled）。
- 操作接口：`ops/settle-preview`（筛选+汇总）、`ops/settle-create`（生成+冻结，事务）、
  `ops/settle-complete`（完成+同步结清）、`ops/settle-cancel`（取消未结算单+解冻）。全程审计。
- UI（对账与问题组）：总览五状态卡 + 上游/下游对账单实体（上游主子表展开明细；下游展开**只读**显示包含的上游单）；
  「发起下游结算」弹窗：汇总三卡+明细列表+可选供应商/备注+确认即冻结；下游单行内「完成结算」「取消(解冻)」。

### E2E 验证（临时数据，验后清理）

```text
未标结算→预览0张 ✓ → 标settled→预览2张/150件/1290元 ✓ → 生成DS冻结2张 ✓
冻结后预览回0、重复生成被拒(列明单号) ✓ → 取消→解冻2张可重新生成 ✓
完成结算→上游2张同步cleared ✓ → 已结算下游单不可取消 ✓
```

## 18. 结算方式统一与金额自动计算（2026-06-12 深夜六，按真实《稀克5月对账》对齐）

样例归档：`data/source_samples/【稀克】发圈发卡5月对账.xlsx`（品名/单位/单价/分渠道数量/汇总/金额，金额=单价×汇总数量）

- **结算方式统一字典** `SETTLEMENT_METHODS`（代销/经销）：采购单（原文本改下拉）、发货单、上/下游对账单共用同一组值；新增方式只改一处。
- **计算规则集中** `settle_line_amount()`：当前所有结算方式 = 单价×数量；**税率/提点等后续方式在此函数扩展，不动表结构**。
- **状态统一落库**：对账明细不填金额自动算出并保存；量/价修改自动重算；明细增/改/删后**单头总数量/总金额自动同步**（SYNC_PARENT_TOTALS），不靠手填。
- 发起下游结算弹窗增加结算方式下拉，透传到下游对账单。
- 上下游对账单均加 settlement_method 列（DDL）。
- **测试数据组（按用户要求保留在系统）**：上游对账单两张——
  5月单（按稀克真实数字：5产品×线下/天猫 10行，状态=上游已结算）自动汇总 **1134件/10645元 与 Excel 总计完全一致**；
  6月单（40件/320元，待上游结算）。备注均标"测试数据可删"。用户可直接点「发起下游结算」体验整个状态机。

## 19. 下游结算按采购合同定价（2026-06-12 深夜七）

用户规则：下游结算单的单价/总金额必须按「我们与该供应商签的下游采购单合同价」计算（不是上游对账价）；所选供应商合同未覆盖的货品需提醒。

- 定价函数 `_price_statement_lines`：上游对账明细按产品聚合数量 → 匹配该供应商下游采购合同行（product_id 优先、名称兜底、**最新订单日期的合同价优先**）→ 金额=合同价×数量。
- 新表 `downstream_statement_lines`（migration_20260612_downstream_statement_lines.sql）：结算明细落库，含合同单价、金额、**来源采购单号/来源合同行**（完整追溯）。下游单展开即见。
- `ops/settle-pricing` 预览接口：弹窗选供应商后实时显示定价表（品名/数量/合同单价/金额/来源采购单）+ 上下游金额对比；**未覆盖货品红色提醒、确认按钮禁用**。
- `ops/settle-create`：供应商必选；未覆盖直接 409 并列出货品清单；取消结算时连同明细删除并解冻。
- 验证：错误供应商（博航）→ 5 货品全部提醒未覆盖、强行生成被拒；正确供应商（四川极深奇乐）→ 全部按 PO-0003 合同价定价（8/8/11/8/11.5），明细 5 行落库含来源；取消后演示数据恢复可结算状态。
- 注：演示数据的合同价恰与上游价相同（演示单就是按稀克下游对账造的）；真实场景上游价≠合同价，系统取的是合同价。

## 20. 对账单自动核销库存与负库存策略（2026-06-12 深夜八）

用户需求：上游对账单下来后自动按渠道扣减库存；明确负库存如何处理。

### 设计决策（负库存策略）

**照扣 + 红色警报，不静默不阻断**：对账单是已发生的销售事实，照实扣减；扣成负数说明此前库存记录有误——自动生成 **error 级校验问题**（写明SKU/扣减来源/当前负数/处置建议：补录漏录入库或冲正流水），工作台「零/负库存」红卡与问题中心同步亮起。与系统一贯"不静默修正"原则一致。

### 实现

- `ops/statement-writeoff`：按明细生成 out/shipment_out 流水，**渠道映射到流水去向**（天猫/馆内等渠道对象），用途记"对账核销 渠道:X"；明细回写 stock_movement_id（**幂等**）；无SKU时在项目账户**自动建SKU**；项目无法唯一定位时明确报错要求对账单补项目。
- **自动触发**：上游对账单状态改「上游已结算」时自动核销（失败不阻断状态变更，转为提示）；行内另有手动「核销库存」按钮（结果弹窗，负库存逐项列出）。
- 删除护栏补充：核销流水被对账明细引用，不可直接删（先解除或删行）。
- 迁移：migration_20260612_statement_writeoff.sql。

### 验证（6月演示单）

```text
无SKU且双账户无法定位 → 明确报错"请在对账单指定项目" ✓（护栏）
补项目后核销 → 自动建SKU(国博账户)，out 40件，去向=天猫 ✓
负库存 -40 → error问题生成（含处置建议与冲正流水号）✓ 工作台KPI亮起 ✓
重复核销 → 0新增1跳过（幂等）✓
```
演示状态：6月单已核销（负库存-40 故意保留以演示策略）；5月单未核销，可在行内点「核销库存」体验10行批量核销。

## 21. 跨供应商批次结算（2026-06-12 深夜九）

用户指出：上游对账覆盖的货品可能跨多个下游供应商，单供应商模式不兼容。

### 重构为批次模型

- 发起下游结算时**按供应商合同自动分组**（产品→覆盖它的供应商，最新合同价优先），一个**结算批次（SB-）一次生成 N 张下游对账单**（每供应商一张，各按自家合同价计明细）。
- 上游单冻结改挂 `settlement_batch_code`；**批次内全部下游单结算完成，上游单才置已结清**；部分结算时保持冻结。
- 取消=**整批取消**（解冻上游、删除批内全部草稿单）；批内已有完成结算的单则禁止整批取消。
- 无任何供应商合同覆盖的货品仍然拦截提醒。弹窗自动展示分组预览（每供应商一块：货品/合同价/小计），不再需要手选供应商。
- DDL：migration_20260612_settlement_batch.sql（upstream/downstream 加 settlement_batch_code；旧 downstream_statement_id 保留兼容）。

### E2E（临时双供应商单，验后清理）

```text
定价自动分2组：四川极深(海晏河清10×8=80) + 博航(柱灯5×79=395) ✓
批次生成2张下游单 ✓ → 结算第1张：上游仍 frozen(remaining=1) ✓
结算第2张：上游 cleared ✓ → 清理后演示数据复原(上游2张/下游0) ✓
```

## 22. FIFO 合同消耗结算（2026-06-12 深夜十）

用户规则：结算量超出下游采购单对应商品（剩余）数量要拦截提示补采购单；多批采购价格不同按**先后顺序**结算；采购明细消耗满自动改状态——解决补货多批价格错乱。

### 实现

- `order_lines` 增 `settled_quantity`（已结算数量）+ `settlement_status`（unsettled 未结清 / exhausted 已结清），展开采购单明细可见（migration_20260612_fifo_settlement.sql）。
- 定价引擎重构为 **FIFO 消耗**：合同行按订单日期升序（**无日期视为最早** NULLS FIRST），先签先耗；同产品跨批拆段，各段按各自合同价计入对应供应商的下游单（明细一行一段，来源采购单可追溯）。
- **余量拦截**：需求 > 全部合同剩余 → 预览红色提示 + 生成 409：「需 N，剩余 M，此前已结 K，请先补录新的采购单」。
- **消耗落库**：生成批次时累加 settled_quantity，消耗满自动置「已结清」（后续结算不再碰该行）；**取消批次精确回退**消耗量与状态。
- 修正：FIFO 排序最初 NULLS LAST 把无日期旧合同排后，已改 NULLS FIRST。

### E2E（临时数据，验后清理）

```text
200件 > 剩余170 → 预览缺口提示 + 生成拦截（需/剩余/已结 三数齐全）✓
补第二批采购单(50件@85)后 → FIFO拆两段：先签批50@85耗尽置exhausted + 后签批150@79 ✓
取消批次 → 两行 settled_quantity 精确归零、状态回未结清 ✓
```
注：验证期间用户在界面自行发起了批次 SB-...2320（5月+6月单→四川极深 1174件/10965元，draft），属真实操作已保留。
