# Fbo Fill Boxes

> Ozon FBO 填装箱 + 箱唛 PDF + 对照表 (fill_boxes_labels.py). 当用户说"填箱"、"申报装箱"、"拉箱唛"、"拉 PDF 标签"、"箱唛打印"、"已建单要填箱"、"补上装箱明细"、"跑 skip-fill 后的下一步"、"单个 order 填箱" 时触发。支持 (a) 单 order_id 填; (b) 批量按 plan-result.json 填.

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

---


# FBO 填装箱 + 箱唛 PDF + 对照表

**输入已建好的 supply_order → 查实际 items → 切箱 → /v1/cargoes/create → 拉箱唛 PDF → 写 Excel 对照表**.

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

## 触发场景

- "已经建好 order 100452984, 帮我填箱 + 拉箱唛"
- "`create_fbo_plan.py --skip-fill` 跑完了, 现在要填"
- "restock_planner 建的一批 order, 批量填装箱"
- "重新拉箱唛 PDF"
- "对照表没导出, 补一份"

## 工作原理

1. 从 `/v3/supply-order/get` 拿 order detail + supply_id
2. 从 `/v1/supply-order/bundle` 拿 **Ozon 实际接受的 items** (很关键 — 任何模式都可能过滤 SKU)
3. 按实际 items 切箱 (件/box_size, 最后一箱余数)
4. 调 `/flow/upload-cargoes`: cargoes/create → cargoes/create/info 轮询 → cargoes-label/create → cargoes-label/get → PDF 保存到 `LABEL_DOWNLOAD_DIR`
5. 写 Excel 对照表 (22 列和 `/fbo-plan` 产物同结构)

## 前置

- 4182 服务健康 (`/fbo-service-up`)
- order_id 已存在 + supply 状态 `DATA_FILLING` (未申报过箱) — 否则会报错

## 两种模式

### 模式 A: 单 order 填 (用户直接报 order_id)

```bash
cd /Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货

python3 fill_boxes_labels.py \
  --order-id 100452984 \
  --offer-id Q_MeiGongDao-HeiSe-Free \
  --sku 3214793665 \
  --qty 4200 \
  --cluster Москва \
  --box-size 300 \
  --account 丝绸生活
```

**必填** (单 order 模式): `--order-id` + `--offer-id` + `--sku` + `--qty` + `--box-size`.
**可选**: `--cluster` (写进对照表), `--account` (默认 丝绸生活), `--fbo-service` (默认 127.0.0.1:4182), `--out-xlsx`, `--dry-run`.

### 模式 B: 批量按 plan-result.json 填

`plan-result.json` 是 `restock_planner.py --execute` 产出的 `fbo_plan_*.result.json`, 里面有已建好的 order_ids.

```bash
python3 fill_boxes_labels.py \
  --plan-result /path/to/fbo_plan_Q_MeiGongDao-HeiSe-Free_20260422.result.json \
  --box-size 300 \
  --account 丝绸生活
```

脚本会逐行读 result.json, 对每条有 order_ids 的记录, 逐 order 申报.

## 关键参数

- `--order-id` 或 `--plan-result` (二选一, 互斥必填)
- `--box-size` (必填): 单箱装箱率
- `--offer-id` / `--sku` / `--qty`: 单 order 模式必填
- `--cluster`: 对照表列用, 单 order 模式可选
- `--account`: 账号名 (默认 `丝绸生活`)
- `--fbo-service`: 默认 `http://127.0.0.1:4182`
- `--out-xlsx`: 默认 `fbo_mapping_<date>.xlsx`
- `--dry-run`: 只切箱 + 打表, **不调 Ozon** (保护已建好的单, 看切箱计划是否合理)

## 产出物

1. **Excel 对照表**: `fbo_mapping_<YYYYMMDD_HHMMSS>.xlsx` (22 列)
2. **箱唛 PDF**: 落到 `LABEL_DOWNLOAD_DIR` (容器 `/app/labels` → 宿主 `/Users/mac/Documents/ozns/github/ozon_fbo_shipment_service/labels/`); 每 supply_id 一个 PDF

## 常见问题

### Q1: 报 `SUPPLY_ITEM_NOT_FOUND`
原计划件数 ≠ supply 实际接受件数. 脚本已默认调 /supply-order/bundle, 所以不会发. 若还出, 说明 supply 后来被改过 (时段 update 可能引发), 重看 `/supply-order/get` 对一下数.

### Q2: `DATA_FILLING` 之外的状态也想填
不行. Ozon 的 cargoes/create 只接受 `DATA_FILLING` 状态. `SHIPPING_PREPARED` 之后只能用 `/supply-order/content/update` 改.

### Q3: PDF 下不下来, `file_url` 返 S3 URL
正常. /v1/cargoes-label/get 返的 file_url 是 presigned S3 (1h 过期), 脚本已自动下并落本地. `/v1/cargoes-label/file/{guid}` 这个路径是 404, 不要用.

### Q4: 对照表少列 / 有空列
脚本沿用 `create_fbo_plan.py` 的 22 列 schema, 空集群/cargo_ids 就是空. 检查 order 是不是多 SKU 但只填了一个.

### Q5: `--dry-run` 和真实跑差别?
dry-run 不会调 cargoes/create, 只 print 切箱方案 + 写 xlsx. 真实跑会建箱 + 拉 PDF.

## 实测 (2026-04-24)

- order 100452984 (Moscow, 4200 Q_MeiGongDao, CROSSDOCK) ✓ 单 order 模式跑通
- order 100532100 (NSK, 600 Q_ChongQiZui, CROSSDOCK) ✓ 跟在 `create_fbo_plan.py` 里 fill 阶段跑的

## label 失败恢复 (cargo recovery)

`/cargoes/create` 已成功 (cargoes READY_TO_SUPPLY) 但 label/create 返 500/429 时, 不要重跑 `/flow/upload-cargoes` (会建重复 cargoes). 直接走恢复:

```python
# 1. 拿现有 cargo_id
cargoes = post("/v1/cargoes/get", {"supply_id": supply_id})
# 注意: 响应里 supply[0].cargoes 嵌套 (不是顶层 cargoes)
cargo_ids = [c["cargo_id"] for c in cargoes["supply"][0]["cargoes"]]

# 2. 直接 label/create (跳过 cargoes/create)
post("/v1/cargoes-label/create", {"cargo_ids": cargo_ids})

# 3. poll cargoes-label/get
post("/v1/cargoes-label/get", {"task_id": task_id})

# 4. PDF 用 curl 下 (Surge MITM, urllib 拿不到)
curl -o supply_X.pdf "$file_url"
```

相关 memory: `feedback_cargo_label_recovery.md`

## 同步本 skill 的 .py 文件

```bash
bash /Users/mac/.claude/skills/sync_fbo_scripts.sh
```

## 相关 skill / memory

- `/fbo-service-up` — 4182 服务运维
- `/fbo-plan` — 建 plan + 自动 fill (默认行为, 不需要再单独 fill, 除非 `--skip-fill`)
- `/restock-to-fbo` — 批量建 order, 产 plan-result.json 直接喂模式 B
- memory `reference_fill_boxes_labels.md` — 脚本契约
- memory `feedback_always_verify_supply_actual_items.md` — 必须 bundle 再切箱的原因
- memory `reference_ozon_fbo_api.md` — cargoes-label PDF URL 细节

