# Fbo Status

> Ozon FBO 发货台账查询 (ledger/). 当用户说"看发货台账"、"FBO 全局状态"、"历史发了多少"、"总览 shipped/skipped"、"最近几天发货情况"、"哪个 SKU 发多少件"、"ledger 看一下"时触发。汇总所有 `shipment_ledger_*.json` + `pending_summary.json`, 展示 shipped 总览 (按日期/集群/SKU) + pending 清单.

- Skill: `yulianggan/fbo-status` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add yulianggan/fbo-status`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yulianggan/fbo-status/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-status

---


# FBO 发货台账全局状态

**综合读 `ledger/shipment_ledger_*.json` (每次 run 一份)**, 按日期/集群/SKU 聚合展示 shipped 和 skipped, 补 pending 总数.

## 触发场景

- "看一下 FBO 总体发货状态"
- "过去一周发了多少件"
- "所有 SKU 目前 shipped/pending 数"
- "哪个集群发得最多"
- "看看 ledger"
- "有没有新的 skipped"

## 路径

- 台账目录: `/Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货/ledger/`
- 文件: `shipment_ledger_<YYYYMMDD_HHMMSS>.json`, `pending_summary.json`

## 查看工具

没有独立 CLI, 用 bash + python 一段脚本即可. 标准用法:

### 1. 所有 run 的发货总览 (最常用)

```bash
cd /Users/mac/Documents/ozns/丝绸生活/海外仓发货表/2026-04-23海外仓发货
python3 <<'EOF'
import json, glob
from collections import defaultdict
files = sorted(glob.glob("ledger/shipment_ledger_*.json"))
print(f"=== {len(files)} runs in ledger/ ===\n")
total_ship = total_skip = 0
by_cluster_ship = defaultdict(int); by_cluster_skip = defaultdict(int)
by_sku_ship = defaultdict(int);     by_sku_skip = defaultdict(int)
for f in files:
    d = json.load(open(f))
    run_at = d.get("run_at","")
    ship_n = sum(sum(it.get("quantity",0) for it in s.get("items",[])) for s in d.get("shipped",[]))
    skip_n = sum(s.get("quantity",0) for s in d.get("skipped",[]))
    orders = len(d.get("shipped",[]))
    print(f"{run_at[:19]:20}  orders={orders:3}  shipped={ship_n:6}件  skipped={skip_n:6}件  ({f.split('/')[-1]})")
    total_ship += ship_n; total_skip += skip_n
    for s in d.get("shipped",[]):
        for it in s.get("items",[]):
            for c in (s.get("cluster_names") or [s.get("cluster")]):
                if c: by_cluster_ship[c] += it.get("quantity",0)
            by_sku_ship[it.get("offer_id","")] += it.get("quantity",0)
    for s in d.get("skipped",[]):
        by_cluster_skip[s.get("cluster","")] += s.get("quantity",0)
        by_sku_skip[s.get("offer_id","")] += s.get("quantity",0)
print(f"\n=== 汇总: shipped={total_ship} 件, skipped={total_skip} 件 (含重复, 看 pending 去重) ===")
print("\n按集群 shipped:")
for c,n in sorted(by_cluster_ship.items(), key=lambda x:-x[1]):
    print(f"  {c:40} {n:6} 件 (skipped={by_cluster_skip.get(c,0)})")
print("\n按 SKU shipped:")
for k,n in sorted(by_sku_ship.items(), key=lambda x:-x[1]):
    print(f"  {k:40} {n:6} 件 (skipped={by_sku_skip.get(k,0)})")
EOF
```

### 2. 看最新 run 的明细
```bash
python3 <<'EOF'
import json, glob
f = sorted(glob.glob("ledger/shipment_ledger_*.json"))[-1]
d = json.load(open(f))
print(f"=== {f.split('/')[-1]} ===")
print(f"run_at: {d.get('run_at')}  account: {d.get('account')}")
print(f"shipped: {len(d.get('shipped',[]))} orders")
for s in d.get("shipped",[]):
    cl = s.get("cluster_names") or [s.get("cluster")]
    print(f"  order={s.get('order_id')}  supply={s.get('supply_id')}  cluster={cl}  mode={s.get('mode')}")
    for it in s.get("items",[]):
        print(f"    {it.get('offer_id'):30}  ×{it.get('quantity'):5}  boxes={it.get('boxes')}")
print(f"\nskipped: {len(d.get('skipped',[]))} 条")
for s in d.get("skipped",[]):
    print(f"  {s.get('cluster'):25} {s.get('offer_id'):30} ×{s.get('quantity'):5}  {s.get('reason_code','')}")
EOF
```

### 3. 看 pending 当前总数 (跨 run 去重后)
```bash
python3 list_pending.py
# 或分组:
python3 list_pending.py --by-cluster
python3 list_pending.py --by-sku
python3 list_pending.py --by-reason
```
(`list_pending.py` 内部自动消解 — 后续 run 发成功的会从 pending 里移除)

### 4. 查某个 order_id 的历史记录
```bash
python3 <<'EOF'
import json, glob, sys
ORDER = 100532100   # 改这里
for f in sorted(glob.glob("ledger/shipment_ledger_*.json")):
    d = json.load(open(f))
    for s in d.get("shipped",[]):
        if s.get("order_id") == ORDER:
            print(f"--- {f.split('/')[-1]} / run_at={d.get('run_at')} ---")
            print(json.dumps(s, ensure_ascii=False, indent=2))
EOF
```

### 5. 哪些 SKU 在哪个集群发最多
```bash
python3 <<'EOF'
import json, glob
from collections import defaultdict
grid = defaultdict(lambda: defaultdict(int))
for f in glob.glob("ledger/shipment_ledger_*.json"):
    d = json.load(open(f))
    for s in d.get("shipped",[]):
        cl = s.get("cluster_names") or [s.get("cluster")]
        for it in s.get("items",[]):
            for c in cl:
                if c: grid[it.get("offer_id","")][c] += it.get("quantity",0)
for sku, by in grid.items():
    print(f"\n[{sku}]")
    for c,n in sorted(by.items(), key=lambda x:-x[1]):
        print(f"  {c:40} {n:6} 件")
EOF
```

## 字段速查 (shipped 条)

```json
{
  "order_id": 100532100,
  "mode": "CROSSDOCK" | "MULTI_CLUSTER",
  "supply_id": 2000050076860,
  "cluster_kws": ["Новосибирск"],
  "cluster_names": ["Новосибирск"],
  "macrolocal_cluster_ids": ["4067"],
  "drop_off": {"id": 22190776129000, "name": "МО_ЩЕРБИНКА_ХАБ"},
  "storage_warehouse": {"id": 18044341087000, "name": "НОВОСИБИРСК_РФЦ_НОВЫЙ"},
  "timeslot": {"from": "...", "to": "..."},
  "state": "DATA_FILLING",
  "items": [{"offer_id":"...","sku":...,"barcode":"...","quantity":600,"boxes":2,"cargo_ids":[...]}],
  "pdf_path": "/app/labels/supply_2000050076860.pdf",
  "pdf_file_url": "https://...s3...",
  "timestamp": "..."
}
```

## 字段速查 (skipped 条)

```json
{
  "cluster": "Новосибирск",
  "cluster_kw": "Новосибирск",
  "macrolocal_cluster_id": "4067",
  "offer_id": "Q_MeiGongDao-HeiSe-Free",
  "sku": 3214793665,
  "barcode": "...",
  "quantity": 1800,
  "reason": "...",
  "reason_code": "MATRIX_DROPPED_FROM_SUPPLY" | "NO_AVAILABLE_WAREHOUSE" | "DRAFT_OR_SUPPLY_FAILED",
  "related_order_id": 100532100,
  "related_supply_id": 2000050076860,
  "timestamp": "..."
}
```

## 常见问题

### Q1: 总 shipped 件数 vs Ozon 后台数对不上
ledger 是"本机发起的 run"维度. Ozon 后台还含:
- 手工在 UI 建的 supply
- 旧脚本 demo.py / writeShiploaction.py 发的 (没台账)
- 被取消的 (ledger 有 shipped 但 UI 可能 cancelled 了)
去 Ozon seller.ozon.ru 「供货单」里按 date 筛选再对.

### Q2: pending_summary.json 和 `list_pending.py` 输出不一致
`pending_summary.json` 是 create_fbo_plan.py 写的快照, `list_pending.py` 是动态重算. 以后者为准 (能反映最新 run 的自动消解).

### Q3: 想删某个 ledger 文件
可以直接 `rm ledger/shipment_ledger_xxx.json`, 不会影响已发的 Ozon 单. pending 会重新去重计算 (相应的 skipped 记录会被去掉).

### Q4: 备份
整个 `ledger/` 目录用 tar 压一下备份即可, 纯 JSON 文本无副作用. 恢复用 `tar -xf` 回来, `list_pending.py` 自动读.

## build_shipment_overview.py — 计划 vs 已发对照表 (2026-04-25 起首选)

聚合 4181 plans (cap_total) + Ozon `/supply-order/list+get+bundle` (active) 出 Excel, 直观看每集群每 SKU 计划/已发/差额.

```bash
python3 build_shipment_overview.py --exclude-before 2026-04-23T00:00:00Z
```

输出列: 集群 | offer_id | 计划件数 | 已发件数 | 差额 | 状态 | 关联 supply_id

**新加状态 (2026-04-25, per-SKU 算法配合)**:
- `改派抵扣已完成` — 本 cluster cap > active 但全 SKU 总 active ≥ cap_total (远东 600 → Москва 抵扣)
- `无计划-意外发货 (改派目标)` — 本 cluster planned=0 但 active>0 (Москва 接了远东改派)
- `缺 X 件` — 还没发齐
- `已发齐` — cluster 单独够数

跟 list_pending 共用 per-SKU 算法 (cap_total / active_total); supply-order/list 必传 `sort_by:1` (`reference_shipment_overview_script.md`).

## 同步本 skill 的 .py 文件

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

## 相关 skill

- `/fbo-retry` — pending 清单 + 重试
- `/fbo-plan` — 新一轮发货 run
- memory `reference_shipment_ledger.md` — ledger schema + pending 去重规则完整说明
- memory `reference_shipment_overview_script.md` — build_shipment_overview.py 详解

