新增 build_full_report:4 站走 compare_site_date、百世走 baishi_daily_stats(基数) + undelivered_record(按 ingested_at 日期过滤),复用 compare.build_summary 渲染 KPI/柱状图/口径说明,产 output/应到未到数据.xlsx。跑比对入口(__compare__)从 compare.main() Excel 路径切换到 build_full_report。百世未到件用基数差(undelivered_pieces)与应到/已到自洽。 集成验证:前端跑比对 -> build_full_report -> /report 下载,各站未到件 顺心8/中通10/韵达2/安能0/百世8。 Co-Authored-By: Claude <noreply@anthropic.com>
659 lines
21 KiB
Python
659 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
db_compare.py — 基于 PostgreSQL 的应到未到差缺比对引擎。
|
||
|
||
与 compare.py(Excel 版)并行:本模块直接从 DB 查询数据进行比对,
|
||
不依赖 downloads/ 下的 Excel 文件。
|
||
|
||
核心思路:以实到扫描日期为锚点 → 反推交接批次 → 展开批次全量比对。
|
||
|
||
每个站点只需提供配置(waybill 列名 / piece 列名 / 是否有 SF 特殊处理),
|
||
核心比对逻辑完全通用。
|
||
|
||
顺心站点 SF 运单特殊处理:SF 运单的子单号(piece_no)为随机号码,不能用
|
||
COUNT(DISTINCT piece_no) 去重计数,改为 COUNT(*) 行计数。
|
||
|
||
用法:
|
||
from inbound_verify.db_compare import compare_site_date, SITE_COMPARE_CONFIG
|
||
|
||
result = compare_site_date("顺心", "2026-07-25")
|
||
if result:
|
||
print(result.stats)
|
||
for row in result.rows:
|
||
print(row)
|
||
"""
|
||
|
||
import os
|
||
from dataclasses import dataclass, field
|
||
from datetime import date, datetime, timedelta
|
||
|
||
import psycopg
|
||
import yaml
|
||
from openpyxl import Workbook
|
||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||
|
||
from inbound_verify.paths import CONFIG_PATH, OUTPUT_DIR, DOWNLOAD_DIR
|
||
from inbound_verify.domain import _site_cfg, ALL_REPORT_SITES, BAISHI_COLUMNS
|
||
|
||
# ============================== 结果类型 ==============================
|
||
|
||
|
||
@dataclass
|
||
class CompareStats:
|
||
"""单站点/单批次比对统计。"""
|
||
|
||
waybill_count: int = 0 # 应到运单数
|
||
expected_pieces: int = 0 # 应到件数
|
||
arrived_pieces: int = 0 # 实到件数
|
||
undelivered_pieces: int = 0 # 未到件数
|
||
undelivered_wb: int = 0 # 差缺运单数
|
||
full_miss: int = 0 # 完全未到
|
||
part_miss: int = 0 # 部分未到
|
||
sf_wb_count: int = 0 # SF 运单数
|
||
sf_undelivered: int = 0 # SF 差缺数
|
||
|
||
|
||
@dataclass
|
||
class UndeliveredRow:
|
||
"""单条差缺明细。"""
|
||
|
||
handover_no: str = "" # 交接单号
|
||
waybill_no: str = "" # 运单号
|
||
total_pieces: int = 0 # 总件数(交接件数)
|
||
arrived_pieces: int = 0 # 已到件数
|
||
arrived_list: list = field(default_factory=list) # 已到单号列表
|
||
is_sf: bool = False # 是否 SF 运单
|
||
|
||
|
||
@dataclass
|
||
class CompareResult:
|
||
"""一次比对的完整结果。"""
|
||
|
||
site: str = ""
|
||
date: str = ""
|
||
batches: list = field(default_factory=list) # 涉及的交接批次
|
||
stats: CompareStats = field(default_factory=CompareStats)
|
||
rows: list = field(default_factory=list) # UndeliveredRow 列表
|
||
|
||
|
||
# ============================== 站点比对配置 ==============================
|
||
|
||
|
||
@dataclass
|
||
class SiteCompareConfig:
|
||
"""DB 比对的站点参数。"""
|
||
|
||
name: str # 站点名
|
||
has_sf: bool = False # 是否需要区分 SF 运单
|
||
|
||
|
||
# 四站点 DB 比对配置(百世不参与 4 站比对)
|
||
SITE_COMPARE_CONFIG: dict[str, SiteCompareConfig] = {
|
||
"顺心": SiteCompareConfig(name="顺心", has_sf=True),
|
||
"中通": SiteCompareConfig(name="中通", has_sf=False),
|
||
"韵达": SiteCompareConfig(name="韵达", has_sf=False),
|
||
"安能": SiteCompareConfig(name="安能", has_sf=False),
|
||
}
|
||
|
||
|
||
# ============================== DB 连接 ==============================
|
||
|
||
|
||
def _load_pg_config():
|
||
"""从 config.yaml 读 postgres 段。与 store.py 共用同一配置源。"""
|
||
if not os.path.exists(CONFIG_PATH):
|
||
raise FileNotFoundError(
|
||
f"未找到配置文件 {CONFIG_PATH}(请参考 config.example.yaml 创建 config.yaml)"
|
||
)
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
cfg = yaml.safe_load(f) or {}
|
||
pg = cfg.get("postgres") or {}
|
||
return {
|
||
"host": pg.get("host", "127.0.0.1"),
|
||
"port": int(pg.get("port", 5432)),
|
||
"user": pg.get("user", "postgres"),
|
||
"password": pg.get("password", ""),
|
||
"dbname": pg.get("dbname", "CQHXDB"),
|
||
"schema": pg.get("schema", "inbound_verify"),
|
||
"connect_timeout_seconds": int(pg.get("connect_timeout_seconds", 5)),
|
||
}
|
||
|
||
|
||
def _connect():
|
||
c = _load_pg_config()
|
||
return psycopg.connect(
|
||
host=c["host"],
|
||
port=c["port"],
|
||
dbname=c["dbname"],
|
||
user=c["user"],
|
||
password=c["password"],
|
||
options=f"-c search_path={c['schema']} -c statement_timeout=30s",
|
||
connect_timeout=c["connect_timeout_seconds"],
|
||
)
|
||
|
||
|
||
# ============================== 核心比对逻辑 ==============================
|
||
|
||
|
||
def compare_site_date(site: str, target_date: str) -> CompareResult | None:
|
||
"""对指定站点和日期执行 DB 差缺比对。
|
||
|
||
算法:
|
||
1. 取 scan_time::date = target_date 的实到运单(锚点)
|
||
2. 反推这些运单所属的交接批次(handover_no)
|
||
3. 展开批次全量应到运单
|
||
4. 查询批次全量实到扫描
|
||
5. 逐运单比对差缺(SF/non-SF 分支处理)
|
||
|
||
Args:
|
||
site: 站点名("顺心"/"中通"/"韵达"/"安能")
|
||
target_date: 日期 "YYYY-MM-DD"
|
||
|
||
Returns:
|
||
CompareResult 或 None(当天无实到数据时返回 None)
|
||
"""
|
||
cfg = SITE_COMPARE_CONFIG.get(site)
|
||
if cfg is None:
|
||
print(f"[db_compare] 不支持的站点: {site}")
|
||
return None
|
||
|
||
try:
|
||
conn = _connect()
|
||
cur = conn.cursor()
|
||
|
||
# ── Step 1: 取实到锚点 ──
|
||
cur.execute(
|
||
"""
|
||
SELECT DISTINCT waybill_no FROM actual_record
|
||
WHERE site = %s AND scan_time::date = %s
|
||
""",
|
||
(site, target_date),
|
||
)
|
||
anchor_wbs = [r[0] for r in cur.fetchall()]
|
||
if not anchor_wbs:
|
||
print(f"[db_compare] {site} {target_date}: 当天无实到数据")
|
||
conn.close()
|
||
return None
|
||
|
||
# ── Step 2: 反推交接批次 ──
|
||
cur.execute(
|
||
"""
|
||
SELECT DISTINCT e.handover_no FROM expected_record e
|
||
WHERE e.site = %s AND e.waybill_no = ANY(%s)
|
||
""",
|
||
(site, anchor_wbs),
|
||
)
|
||
batches = [r[0] for r in cur.fetchall()]
|
||
|
||
# ── Step 3: 展开批次全量应到 ──
|
||
cur.execute(
|
||
"""
|
||
SELECT waybill_no, handover_no, handover_pieces
|
||
FROM expected_record
|
||
WHERE site = %s AND handover_no = ANY(%s)
|
||
ORDER BY handover_no, waybill_no
|
||
""",
|
||
(site, batches),
|
||
)
|
||
exp_rows = cur.fetchall() # [(waybill_no, handover_no, handover_pieces), ...]
|
||
|
||
if not exp_rows:
|
||
conn.close()
|
||
return None
|
||
|
||
all_wbs = [r[0] for r in exp_rows]
|
||
|
||
# ── Step 4: 取批次全量实到 ──
|
||
cur.execute(
|
||
"""
|
||
SELECT waybill_no, piece_no FROM actual_record
|
||
WHERE site = %s AND waybill_no = ANY(%s)
|
||
ORDER BY waybill_no, piece_no
|
||
""",
|
||
(site, all_wbs),
|
||
)
|
||
act_rows = cur.fetchall() # [(waybill_no, piece_no), ...]
|
||
|
||
conn.close()
|
||
|
||
# ── Step 5: 逐运单比对 ──
|
||
return _do_compare(site, target_date, batches, exp_rows, act_rows, cfg)
|
||
|
||
except Exception as e:
|
||
print(f"[db_compare] {site} {target_date} 比对异常: {e}")
|
||
return None
|
||
|
||
|
||
def compare_site_batch(site: str, handover_no: str) -> CompareResult | None:
|
||
"""按指定交接单号执行全批次比对(不依赖实到锚点)。
|
||
|
||
用于已知交接单号后精确比对某一批次。
|
||
"""
|
||
cfg = SITE_COMPARE_CONFIG.get(site)
|
||
if cfg is None:
|
||
print(f"[db_compare] 不支持的站点: {site}")
|
||
return None
|
||
|
||
try:
|
||
conn = _connect()
|
||
cur = conn.cursor()
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT waybill_no, handover_no, handover_pieces
|
||
FROM expected_record
|
||
WHERE site = %s AND handover_no = %s
|
||
ORDER BY waybill_no
|
||
""",
|
||
(site, handover_no),
|
||
)
|
||
exp_rows = cur.fetchall()
|
||
if not exp_rows:
|
||
conn.close()
|
||
return None
|
||
|
||
all_wbs = [r[0] for r in exp_rows]
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT waybill_no, piece_no FROM actual_record
|
||
WHERE site = %s AND waybill_no = ANY(%s)
|
||
ORDER BY waybill_no, piece_no
|
||
""",
|
||
(site, all_wbs),
|
||
)
|
||
act_rows = cur.fetchall()
|
||
|
||
conn.close()
|
||
|
||
return _do_compare(
|
||
site,
|
||
f"batch:{handover_no}",
|
||
[handover_no],
|
||
exp_rows,
|
||
act_rows,
|
||
cfg,
|
||
)
|
||
|
||
except Exception as e:
|
||
print(f"[db_compare] {site} batch:{handover_no} 比对异常: {e}")
|
||
return None
|
||
|
||
|
||
# ============================== 比对核心 ==============================
|
||
|
||
|
||
def _do_compare(
|
||
site: str,
|
||
label: str,
|
||
batches: list[str],
|
||
exp_rows: list[tuple], # [(waybill_no, handover_no, handover_pieces), ...]
|
||
act_rows: list[tuple], # [(waybill_no, piece_no), ...]
|
||
cfg: SiteCompareConfig,
|
||
) -> CompareResult:
|
||
"""执行逐运单比对,产出统计 + 差缺明细。
|
||
|
||
与 compare.py:process() 口径一致:
|
||
- 应到件数 = handover_pieces(交接件数)
|
||
- 实到件数 = SF ? COUNT(*) : COUNT(DISTINCT piece_no)
|
||
- arrived_cnt >= handover_pieces → 足额到货,跳过
|
||
"""
|
||
# 构建实到索引: waybill_no → [piece_no, ...](保留所有行,不去重)
|
||
act_by_wb: dict[str, list[str]] = {}
|
||
for wb, piece in act_rows:
|
||
act_by_wb.setdefault(wb, []).append(piece)
|
||
|
||
stats = CompareStats()
|
||
rows: list[UndeliveredRow] = []
|
||
max_arrived = 0
|
||
|
||
for wb, handover_no, handover_pcs in exp_rows:
|
||
handover_pcs = handover_pcs or 0
|
||
if handover_pcs <= 0:
|
||
continue
|
||
|
||
stats.waybill_count += 1
|
||
stats.expected_pieces += handover_pcs
|
||
|
||
is_sf = cfg.has_sf and wb.startswith("SF")
|
||
if is_sf:
|
||
stats.sf_wb_count += 1
|
||
|
||
all_pieces = act_by_wb.get(wb, [])
|
||
|
||
if is_sf:
|
||
# SF: 行计数,不去重(piece_no 是随机号码)
|
||
arrived_cnt = len(all_pieces)
|
||
arrived_list = list(all_pieces)
|
||
else:
|
||
# non-SF: 子单号去重
|
||
unique_pieces = list(dict.fromkeys(all_pieces)) # 保序去重
|
||
arrived_cnt = len(unique_pieces)
|
||
arrived_list = unique_pieces
|
||
|
||
stats.arrived_pieces += arrived_cnt
|
||
|
||
if arrived_cnt >= handover_pcs:
|
||
continue # 足额或溢到,不进差缺表
|
||
|
||
if arrived_cnt == 0:
|
||
stats.full_miss += 1
|
||
else:
|
||
stats.part_miss += 1
|
||
|
||
if is_sf:
|
||
stats.sf_undelivered += 1
|
||
|
||
max_arrived = max(max_arrived, arrived_cnt)
|
||
rows.append(
|
||
UndeliveredRow(
|
||
handover_no=handover_no,
|
||
waybill_no=wb,
|
||
total_pieces=handover_pcs,
|
||
arrived_pieces=arrived_cnt,
|
||
arrived_list=arrived_list,
|
||
is_sf=is_sf,
|
||
)
|
||
)
|
||
|
||
stats.undelivered_pieces = max(0, stats.expected_pieces - stats.arrived_pieces)
|
||
stats.undelivered_wb = stats.full_miss + stats.part_miss
|
||
|
||
result = CompareResult(
|
||
site=site,
|
||
date=label,
|
||
batches=batches,
|
||
stats=stats,
|
||
rows=rows,
|
||
)
|
||
|
||
# 打印摘要
|
||
print(
|
||
f"[db_compare] {site} {label}: "
|
||
f"batches={len(batches)}, "
|
||
f"wb={stats.waybill_count}(SF:{stats.sf_wb_count}), "
|
||
f"exp={stats.expected_pieces}, arr={stats.arrived_pieces}, "
|
||
f"miss={stats.undelivered_pieces}, "
|
||
f"miss_wb={stats.undelivered_wb}(full={stats.full_miss}, part={stats.part_miss})"
|
||
)
|
||
if stats.sf_undelivered:
|
||
print(f" SF 差缺: {stats.sf_undelivered} 个运单")
|
||
|
||
return result
|
||
|
||
|
||
# ============================== Excel 输出 ==============================
|
||
|
||
|
||
# 样式常量(与 compare.py 对齐)
|
||
_FONT = "微软雅黑"
|
||
_BLUE = "305496"
|
||
|
||
_HEADER_FILL = PatternFill("solid", fgColor=_BLUE)
|
||
_HEADER_FONT = Font(name=_FONT, bold=True, color="FFFFFF", size=11)
|
||
_BODY_FONT = Font(name=_FONT, size=10)
|
||
_THIN = Side(style="thin", color="D9D9D9")
|
||
_BORDER = Border(left=_THIN, right=_THIN, top=_THIN, bottom=_THIN)
|
||
|
||
|
||
def write_result_excel(result: CompareResult, output_path: str | None = None) -> str:
|
||
"""将比对结果写入 Excel 文件。
|
||
|
||
Args:
|
||
result: compare_site_date 或 compare_site_batch 的返回值
|
||
output_path: 输出路径,为 None 时自动生成:
|
||
output/{站}-{日期}-未到数据.xlsx
|
||
|
||
Returns:
|
||
实际写入的文件路径
|
||
"""
|
||
if output_path is None:
|
||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||
date_tag = result.date.replace(":", "-").replace("batch:", "batch-")
|
||
output_path = os.path.join(
|
||
OUTPUT_DIR, f"{result.site}-{date_tag}-未到数据.xlsx"
|
||
)
|
||
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = result.site
|
||
|
||
_write_sheet(ws, result)
|
||
wb.save(output_path)
|
||
print(f"[db_compare] Excel 已输出: {output_path}")
|
||
return output_path
|
||
|
||
|
||
def _write_sheet(ws, result: CompareResult):
|
||
"""写单个站点的差缺明细 sheet。"""
|
||
s = result.stats
|
||
rows = result.rows
|
||
|
||
# 动态列: 交接单号 | 运单号 | 总件数 | 已到单号1 | 已到单号2 | ...
|
||
max_arrived = max((len(r.arrived_list) for r in rows), default=0)
|
||
columns = ["交接单号", "运单号", "总件数"] + [
|
||
f"已到单号{i + 1}" for i in range(max_arrived)
|
||
]
|
||
|
||
ws.sheet_view.showGridLines = False
|
||
|
||
# 表头
|
||
ws.append(columns)
|
||
for c in range(1, len(columns) + 1):
|
||
cell = ws.cell(row=1, column=c)
|
||
cell.fill = _HEADER_FILL
|
||
cell.font = _HEADER_FONT
|
||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||
cell.border = _BORDER
|
||
|
||
# 数据行
|
||
for row in rows:
|
||
values = {
|
||
"交接单号": row.handover_no,
|
||
"运单号": row.waybill_no,
|
||
"总件数": row.total_pieces,
|
||
}
|
||
for i, piece in enumerate(row.arrived_list):
|
||
values[f"已到单号{i + 1}"] = piece
|
||
ws.append([values.get(c, "") for c in columns])
|
||
|
||
# 格式
|
||
for r in range(2, ws.max_row + 1):
|
||
for c, col in enumerate(columns, start=1):
|
||
cell = ws.cell(row=r, column=c)
|
||
cell.font = _BODY_FONT
|
||
cell.border = _BORDER
|
||
if col == "总件数":
|
||
cell.number_format = "#,##0"
|
||
cell.alignment = Alignment(horizontal="right", vertical="center")
|
||
else:
|
||
cell.number_format = "@"
|
||
|
||
# 列宽
|
||
for c, col in enumerate(columns, start=1):
|
||
body_lens = [
|
||
len(str(ws.cell(row=r, column=c).value or ""))
|
||
for r in range(2, ws.max_row + 1)
|
||
]
|
||
width = min(max([len(str(col))] + body_lens) + 4, 36)
|
||
ws.column_dimensions[ws.cell(row=1, column=c).column_letter].width = max(
|
||
width, 12
|
||
)
|
||
|
||
ws.freeze_panes = "A2"
|
||
|
||
|
||
# ============================== 全站汇总报表(DB 版)=============================
|
||
|
||
|
||
def _stats_to_dict(s: CompareStats) -> dict:
|
||
"""CompareStats -> build_summary 要的中文键 stats dict。"""
|
||
return {
|
||
"运单数": s.waybill_count,
|
||
"应到件": s.expected_pieces,
|
||
"已到件": s.arrived_pieces,
|
||
"未到件": s.undelivered_pieces,
|
||
"完全未到": s.full_miss,
|
||
"部分未到": s.part_miss,
|
||
}
|
||
|
||
|
||
def _target_date_for(site: str) -> str:
|
||
"""4 站比对锚点:today - expected_offset(与 _site_undelivered_handler 一致)。"""
|
||
from inbound_verify import state_store # 懒导入,避免成环
|
||
|
||
offset = state_store.get_offset(site, "expected")
|
||
return (date.today() - timedelta(days=offset)).strftime("%Y-%m-%d")
|
||
|
||
|
||
def _baishi_from_pg(cur, target: str):
|
||
"""查百世当日基数(baishi_daily_stats)+ 当天未到明细(undelivered_record 按 ingested_at 过滤)。
|
||
返回 (stats_dict_or_None, rows_or_None);基数与明细均无 → (None, None)。
|
||
|
||
undelivered_record 是 UPSERT 累积表;按 ingested_at::date = target 取当天入库的未到快照
|
||
(= 当天下载的当前未到,站点已剔除已到),避免累积偏大。
|
||
"""
|
||
cur.execute(
|
||
"SELECT expected_pieces, arrived_pieces, undelivered_pieces "
|
||
"FROM baishi_daily_stats WHERE site = %s AND business_date = %s",
|
||
("百世", target),
|
||
)
|
||
basis = cur.fetchone()
|
||
cur.execute(
|
||
"SELECT waybill_no, piece_no, biz_type, last_scan FROM undelivered_record "
|
||
"WHERE site = %s AND ingested_at::date = %s",
|
||
("百世", target),
|
||
)
|
||
detail = cur.fetchall()
|
||
if basis is None and not detail:
|
||
return (None, None)
|
||
exp = basis[0] if basis else None
|
||
arr = basis[1] if basis else None
|
||
# 未到件优先取基数差(baishi_daily_stats.undelivered_pieces,与应到/已到同源自洽);
|
||
# 基数缺失时退回明细行数。
|
||
undel = basis[2] if (basis and basis[2] is not None) else len(detail)
|
||
wb_count = len({r[0] for r in detail if r[0]}) # 运单号去重
|
||
rows = [
|
||
{
|
||
"类型": r[2] or "",
|
||
"子单号": r[1] or "",
|
||
"运单号": r[0] or "",
|
||
"最新扫描记录": r[3] or "",
|
||
}
|
||
for r in detail
|
||
]
|
||
stats = {
|
||
"运单数": wb_count,
|
||
"应到件": exp,
|
||
"已到件": arr,
|
||
"未到件": undel,
|
||
"完全未到": None,
|
||
"部分未到": None,
|
||
}
|
||
return (stats, rows)
|
||
|
||
|
||
def build_full_report(date=None) -> str:
|
||
"""DB 版全站汇总报表:4 站走 DB 比对、百世走 PG,复用 compare.build_summary 渲染。
|
||
产出 output/应到未到数据.xlsx(/report 下载)。date=None 时各站按 expected_offset 算锚点。
|
||
返回输出路径。"""
|
||
from inbound_verify import compare # 复用 build_summary / write_station / OUTFILE
|
||
|
||
print("[db_compare] 开始生成全站汇总报表 ...")
|
||
wb = Workbook()
|
||
wb.remove(wb.active)
|
||
summary_ws = wb.create_sheet("汇总报表")
|
||
|
||
results = [] # [(name, stats_dict_or_None)],顺序 ALL_REPORT_SITES
|
||
site_targets = {} # name -> target_date(汇总表"数据日期"列)
|
||
|
||
conn = _connect()
|
||
cur = conn.cursor()
|
||
try:
|
||
for name in ALL_REPORT_SITES:
|
||
if name == "百世":
|
||
target = date or datetime.now().strftime("%Y-%m-%d")
|
||
site_targets[name] = target
|
||
stats, rows = _baishi_from_pg(cur, target)
|
||
results.append((name, stats))
|
||
if rows is not None:
|
||
compare.write_station(wb.create_sheet(name), BAISHI_COLUMNS, rows)
|
||
continue
|
||
if name not in SITE_COMPARE_CONFIG:
|
||
results.append((name, None))
|
||
continue
|
||
target = date or _target_date_for(name)
|
||
site_targets[name] = target
|
||
result = compare_site_date(name, target)
|
||
if result is not None:
|
||
results.append((name, _stats_to_dict(result.stats)))
|
||
_write_sheet(wb.create_sheet(name), result)
|
||
else:
|
||
results.append((name, None))
|
||
finally:
|
||
conn.close()
|
||
|
||
compare.build_summary(
|
||
summary_ws,
|
||
results,
|
||
datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||
dates=site_targets,
|
||
)
|
||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||
wb.save(compare.OUTFILE)
|
||
print(f"[db_compare] 全站汇总已输出: {compare.OUTFILE}")
|
||
for name, s in results:
|
||
print(f" {name}:未到 {s['未到件']} 件" if s else f" {name}:无数据,跳过")
|
||
return compare.OUTFILE
|
||
|
||
|
||
# ============================== 终端验证入口 ==============================
|
||
|
||
|
||
def main():
|
||
"""命令行验证入口:
|
||
python -m inbound_verify.db_compare 顺心 2026-07-25
|
||
"""
|
||
import sys
|
||
|
||
site = sys.argv[1] if len(sys.argv) > 1 else "顺心"
|
||
target_date = sys.argv[2] if len(sys.argv) > 2 else "2026-07-25"
|
||
|
||
result = compare_site_date(site, target_date)
|
||
if result is None:
|
||
print(f"{site} {target_date}: 无结果")
|
||
return
|
||
|
||
print(f"\n=== {result.site} {result.date} 差缺明细 ===")
|
||
print(f"涉及批次: {result.batches}")
|
||
print(f"应到运单: {result.stats.waybill_count} (SF: {result.stats.sf_wb_count})")
|
||
print(f"应到件数: {result.stats.expected_pieces}")
|
||
print(f"实到件数: {result.stats.arrived_pieces}")
|
||
print(f"未到件数: {result.stats.undelivered_pieces}")
|
||
print(
|
||
f"差缺运单: {result.stats.undelivered_wb} (完全未到: {result.stats.full_miss}, 部分未到: {result.stats.part_miss})"
|
||
)
|
||
if result.stats.sf_undelivered:
|
||
print(f"SF 差缺: {result.stats.sf_undelivered}")
|
||
|
||
if result.rows:
|
||
print(f"\n--- 差缺明细 (共 {len(result.rows)} 条) ---")
|
||
for row in result.rows[:20]:
|
||
sf = "[SF]" if row.is_sf else ""
|
||
arrived_preview = row.arrived_list[:5]
|
||
print(
|
||
f" {sf} {row.waybill_no}: "
|
||
f"应到{row.total_pieces}件, 实到{row.arrived_pieces}件"
|
||
f" {f'已到: {arrived_preview}' if arrived_preview else ''}"
|
||
)
|
||
if len(result.rows) > 20:
|
||
print(f" ... 还有 {len(result.rows) - 20} 条")
|
||
|
||
# 输出 Excel
|
||
path = write_result_excel(result)
|
||
print(f"\n结果文件: {path}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|