# Fbo Plan

> Ozon FBO 多集群发货 SOP

- Skill: `yulianggan/fbo-plan` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add yulianggan/fbo-plan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yulianggan/fbo-plan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: yulianggan (https://skillmd.com/u/yulianggan)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yulianggan/fbo-plan

---


# Ozon FBO 多集群发货 SOP

**一个脚本跑通**: plan.json → draft → 分拆 (可发走 multi-cluster, 不可发走单集群 CROSSDOCK fallback) → supply_order → bundle 查实际 items → 切箱 → cargoes → 标签 PDF → Excel 对照表 → ledger 台账.

脚本: `/Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货/create_fbo_plan.py` (~1050 行)

## 触发场景

- "按 plan.json 发 FBO"
- "跑 FBO 发货 SOP"
- "多 SKU 多集群要发 FBO"
- "多集群发一票"
- "从卡累利阿 + 新西伯利亚发这几款"

## 前置依赖

1. **FBO 代理服务 4182 必须健康** (docker 容器 `ozon-fbo-shipment`):
   ```bash
   python3 -c "import urllib.request,json; print(json.load(urllib.request.urlopen('http://localhost:4182/health')).get('ok'))"
   ```
   没起 → 先跑 `/fbo-service-up`.

2. **plan.json 配置文件** (用户要提供, 或从 `/restock-to-fbo` 产出):
   ```json
   {
     "source_warehouse_keyword": "ЖУКОВСКИЙ_РФЦ",
     "drop_off_keyword": "ЩЕРБИНКА",
     "matrix": {
       "Новосибирск":    {"Q_ChongQiZui-HuangSe-Free": 600,  "Q_MeiGongDao-HeiSe-Free": 1800},
       "Дальний Восток": {"Q_ChongQiZui-HuangSe-Free": 300,  "Q_MeiGongDao-HeiSe-Free": 900}
     }
   }
   ```
   - `drop_off_keyword` 必须有时段的 SORTING_CENTER 关键字 (如 ЩЕРБИНКА; **不是所有 CROSS_DOCK 都行**).
   - `source_warehouse_keyword` 卖家发货仓关键字 (如 ЖУКОВСКИЙ_РФЦ); 可选.
   - `matrix` 键=集群俄文名/关键字, 值={offer_id: 件数}. 件数 > 0.

## SOP 6 步 (脚本内固化)

```
0. /health + 账号存在性
1. 解析 clusters (名→macrolocal_cluster_id) / SKUs (offer_id→数字 sku)
   / drop-off (SORTING_CENTER 带时段) / seller warehouse (可选)
2. POST /draft/multi-cluster/create + 轮询 /draft/create/info (v2)
3. 按 availability_status 分拆:
   - AVAILABLE → multi-cluster 一票
   - PARTIAL_AVAILABLE / NOT_AVAILABLE → 降级单集群 CROSSDOCK fallback
   - 若 multi_ok < 2 集群, 全部降级 CROSSDOCK (单集群走 multi-cluster 无意义)
4. multi-cluster: timeslot/info → supply/create (v2) → create/status
5. 每个 fallback 集群: draft/crossdock/create → timeslot → supply/create → status
6. 所有成功 supply_order:
   - 先 /supply-order/bundle 拿 **supply 实际接受的 items** (Ozon 会悄悄过滤!)
   - 按实际 items 切箱 → /flow/upload-cargoes (批量 cargoes/create + label/create + PDF)
   - 写 Excel 对照表 (22 列)
   - 未发的写 ledger.skipped (reason_code: MATRIX_DROPPED_FROM_SUPPLY / NO_AVAILABLE_WAREHOUSE / DRAFT_OR_SUPPLY_FAILED)
```

## 三种输入源

### 源 1: plan.json (原生)
手工维护 matrix, 适合测试或非决策驱动场景.

### 源 2: 4181 plan_id (HITL 集成) ★★★
已有 `/decide/batch` 跑过 + Bitable 审批到 `status=approved` 的 plan, 直接喂过来:
```bash
python3 create_fbo_plan.py --plan-id <pid> --account 丝绸生活
# 或多个:
python3 create_fbo_plan.py --plan-id <pid1>,<pid2> --account 丝绸生活
```
- box_size 自动取 plan.box_size, `--box-size` 可覆盖
- source/drop-off keyword 用 `--source-warehouse-keyword` / `--drop-off-keyword` (默认 ЖУКОВСКИЙ_РФЦ / ЩЕРБИНКА)
- 非 approved → 报错退出 (安全门)
- 建单成功 (ledger.shipped 有该 plan 的 SKU) → **自动** POST `/plans/<pid>/transition {to_status:dispatched}` (4181 状态机)
- 全 skipped (矩阵拒) → plan 保留 approved, 可 `/fbo-retry` 后续重试

### 源 3: 4181 batch_id (批次一把梭)
一个 `/decide/batch` 产生的多 SKU plan 合并发:
```bash
python3 create_fbo_plan.py --batch-id <bid> --account 丝绸生活
```
- 内部扫 /plans 列表 + 逐个拉 detail 筛 batch_id (列表 view 不暴露 batch_id)
- 要求批次内所有 plan 都 approved
- 所有 plan 的 box_size 必须一致 (不同 → 报错, 用 --box-size 覆盖或分批)

## 执行命令

### 标准跑 (真实建单)
```bash
cd /Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货
python3 create_fbo_plan.py \
  --config plan.json \
  --box-size 300 \
  --account 丝绸生活
```

### dry-run (只探可发性, 不建单)
```bash
python3 create_fbo_plan.py --config plan.json --box-size 300 --account 丝绸生活 --dry-run
```
输出: 每集群 availability_status (AVAILABLE / PARTIAL_AVAILABLE / NOT_AVAILABLE + reason) + 分拆方案打印.

### 建完 supply 就停 (不填箱不拉标签, 用于稍后人工确认后再手工申报)
```bash
python3 create_fbo_plan.py --config plan.json --box-size 300 --account 丝绸生活 --skip-fill
```
之后用 `/fbo-fill-boxes` 跑 `fill_boxes_labels.py --plan-result <ledger.json>`.

### 调整时段窗口 (默认 3~28 天后)
```bash
python3 create_fbo_plan.py ... --days-from 5 --days-to 14
```

### 全参数
- `--config` (必填): plan.json 路径
- `--box-size` (必填): 单箱装箱率
- `--account`: 默认 `丝绸生活`
- `--fbo-service`: 默认 `http://127.0.0.1:4182`
- `--out-xlsx`: 对照表输出路径, 默认 `fbo_plan_<date>.xlsx`
- `--dry-run`: 只探不建
- `--skip-fill`: 建完 supply 停
- `--days-from` / `--days-to`: 时段窗口 (默认 3/28)

## 关键点 / 坑 (都已在脚本里处理, 但要知道)

1. **Ozon 静默过滤 SKU**: 任何模式建完 supply 都可能悄悄过滤 SKU, 申报装箱前必须 `/v1/supply-order/bundle` 拿实际 items 再切箱. 脚本内 `fetch_supply_actual_items()` 已实现. 过滤的部分自动进 ledger.skipped (reason_code=MATRIX_DROPPED_FROM_SUPPLY).

2. **drop-off 必须选 SORTING_CENTER 带时段**: CROSS_DOCK 类仓可能没 multi-cluster 时段, 选 ЩЕРБИНКА 这类 SORTING_CENTER.

3. **PARTIAL_AVAILABLE 不保留 multi-cluster**: 无法精确识别 bundle 里可发的 SKU 子集, 强发会被 supply/create 拒. 直接 fallback 到单集群 CROSSDOCK.

4. **multi_ok < 2 → 全部走 CROSSDOCK**: 1 个集群走多集群无意义.

5. **矩阵限制典型**: 远东 NOT_AVAILABLE_MATRIX 对 Q_MeiGongDao+Q_ChongQiZui 组合 (2026-04-24 实测).

6. **v1↔v2 timestamp Z 反转**: v1 supply/create 要 `Z` 后缀, v2 拒绝. 脚本已按 v2 走.

7. **NOT_AVAILABLE_MATRIX/ROUTE 不再 single-fallback** (2026-04-25): multi-cluster 预检阶段, 这两类 reason 直接归 skipped (reason_code=NO_AVAILABLE_WAREHOUSE), 省 draft 配额. single CROSSDOCK 走的是同一矩阵, 救不活. 短间隔 (12h) retry 实测无效, 至少 24h+ 才有意义.

8. **80h safe_cancel 规则**: 取消 supply 必先 shift timeslot ≥ 80h (`safe_cancel_supply` 落地), 否则 Ozon 罚款. ISO+Z 格式 (UTC).

9. **MIN_BOX_FILL_RATE=0.5 自动 cancel**: bundle 拿到的 actual qty / planned qty < 50% 触发自动 cancel, 防 supply 被 trim 后只发几件还要走全流程 (Саратов/Казань ~52-58 件 hard cap 反例已落地). 跟 80h 规则联动 (即先 shift 再 cancel).

10. **残箱 ≥ box_size/2 否则 floor**: 件数若有不足半箱残箱直接砍掉. list_pending --emit-plan 入口 + create_fbo_plan 入口都已落地. 实物装箱限制.

## 产出物

脚本跑完, 当前目录下:

1. **Excel 对照表**: `fbo_plan_<YYYYMMDD_HHMMSS>.xlsx` (22 列: 交货 ID / 供货 ID / 状态 / 链路 / 集群 / macrolocal / 货位 / drop-off / 货号 / SKU / 条码 / 箱号 / cargo_id / 该箱件数 / 总件数 / 总箱数 / 装箱率 / 时段 / PDF 路径 / 备注)

2. **ledger 台账**: `ledger/shipment_ledger_<YYYYMMDD_HHMMSS>.json` (shipped[] + skipped[])

3. **pending_summary.json**: 跨 run 聚合的未发清单 (自动去重 `(cluster, offer_id, sku)`, 保留最新 timestamp)

4. **箱唛 PDF**: 每个 supply_order 一份, 路径在 Excel 的"箱唛 PDF 路径"列和 ledger.shipped[].pdf_path.

## 验证建议

跑完看:
- `tail -20 ledger/shipment_ledger_<stamp>.json` 看 shipped 数
- Excel 打开看有没有红色行 (备注列有错误)
- Ozon seller.ozon.ru「供货单」页面能搜到 order_id
- 箱唛 PDF 能打开

## 常见问题

**Q1**: 整个 multi-cluster 全拒, 全部降 CROSSDOCK 了, 对吗?
A: 看 availability_status. 如果所有集群都是 NOT_AVAILABLE_MATRIX, 可能是这批 SKU 组合在业务矩阵里全被拒. dry-run 看分拆再决定要不要拆 plan.

**Q2**: supply_order 建了但 cargoes/create 返 `SUPPLY_ITEM_NOT_FOUND`
A: 脚本自动调 `/supply-order/bundle` 先拿实际 items 避免这个. 如果还出, 可能 supply 被改过, 重跑或手动 `/supply-order/get` 核对.

**Q3**: `DROP_OFF_POINT_HAS_NO_TIMESLOTS`
A: `drop_off_keyword` 选错了. 换 ЩЕРБИНКА ХАБ 这种 SORTING_CENTER 关键字, 不是 КРОССДОКИНГ 类.

**Q4**: 账号不在 .env
A: 去 `/Users/mac/Documents/ozns/github/ozon_fbo_shipment_service/.env` 加 `OZON_ACCOUNTS_JSON`, `docker compose restart`.

**Q5**: 有些 SKU 一直被过滤, 想跳过重发剩下
A: 不用手改 plan. 等脚本跑完, ledger 里已记 skipped. 用 `/fbo-retry` 导 retry 计划, 等矩阵松了再跑.

## 相关 skill / memory

- `/fbo-service-up` — 4182 服务运维
- `/fbo-retry` — 查/重试未发
- `/fbo-fill-boxes` — 单独填装箱+箱唛 (--skip-fill 后补流程)
- `/fbo-status` — 台账查询
- `/restock-to-fbo` — 从补货决策直出 plan.json
- memory `reference_create_fbo_plan_sop.md` — SOP 契约 + 完整配置 shape
- memory `reference_shipment_ledger.md` — ledger schema
- memory `feedback_always_verify_supply_actual_items.md` — 为什么必须查 bundle
- memory `reference_ozon_fbo_api.md` — v1↔v2 路径 / supply_type 枚举 / Ozon 坑点

