业务日期端到端:状态库快照 + 下载写入 + 报告读库

- state_store: site_status 加 expected/actual/undelivered_business_date 列(建表+旧库迁移);set_data_state 加可选 business_date(None 时保留,心跳不覆盖下载快照)
- runtime: dispatch_task 下载成功后快照业务日期(today−offset);4 站 undelivered 一并补写 expected/actual/undelivered 三列(_site_undelivered_handler 内部连下绕过 dispatch,否则应到/实到业务日期丢失)
- expected_undelivered: 报告数据日期列改读状态库(_read_business_dates),退役 mtime 反推;KPI 卡 2-3-2-2 等宽对齐 + 柱状图图例 overlay 分行

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-07-18 14:09:08 +08:00
parent d44521d953
commit 0be3399165
3 changed files with 139 additions and 45 deletions

View File

@@ -326,7 +326,24 @@ def write_site_file(name):
return True
def build_full_report(include):
def _read_business_dates(include):
"""从状态库读各站业务日期dispatch 下载成功时快照写入),供报告「数据日期」列。
4 站取 expected_business_date报告按应到口径百世取 undelivered_business_date。
从未下过的站返回空串(诚实留空,不反推)。"""
import state_store # lazy import比对模块本身保持纯离线
status = state_store.get_all_status()
dates = {}
for name in include:
s = status.get(name, {})
if name == "百世":
dates[name] = s.get("undelivered_business_date", "")
else:
dates[name] = s.get("expected_business_date", "")
return dates
def build_full_report(include, dates=None):
"""生成全站汇总报表 output/应到未到数据.xlsx。
include: 本次成功的站点集合;未成功站点在汇总里保留行、无数据(不影响他站)。
返回 {站点: 未到件或None} 供日志。"""
@@ -350,7 +367,12 @@ def build_full_report(include):
ws = wb.create_sheet(name)
write_station(ws, columns, rows)
build_summary(summary_ws, summary, datetime.now().strftime("%Y-%m-%d %H:%M"))
build_summary(
summary_ws,
summary,
datetime.now().strftime("%Y-%m-%d %H:%M"),
dates=dates or {},
)
wb.save(OUTFILE)
return {n: (s["未到件"] if s else None) for (n, s) in summary}
@@ -358,7 +380,8 @@ def build_full_report(include):
# ============================ 写汇总报表 ============================
def build_summary(ws, results, generated_at):
def build_summary(ws, results, generated_at, dates=None):
dates = dates or {}
center = Alignment(horizontal="center", vertical="center")
left = Alignment(horizontal="left", vertical="center", indent=1)
@@ -375,31 +398,33 @@ def build_summary(ws, results, generated_at):
ws.sheet_view.showGridLines = False
ws.column_dimensions["A"].width = 2.5
# 列宽按「4 个 KPI 卡等宽」设计B+C = D+E+F = G+H = I+J = 26
for col, w in {
"B": 11,
"C": 12,
"D": 12,
"E": 12,
"F": 12,
"G": 11,
"B": 12,
"C": 14,
"D": 9,
"E": 9,
"F": 8,
"G": 12,
"H": 14,
"I": 14,
"I": 13,
"J": 13,
}.items():
ws.column_dimensions[col].width = w
ws.row_dimensions[1].height = 6
# —— 标题栏 ——
ws.merge_cells("B2:I2")
ws.merge_cells("B2:J2")
t = ws["B2"]
t.value = "应到未到比对 · 汇总报表"
t.fill = PatternFill("solid", fgColor=NAVY)
t.font = Font(name=FONT, bold=True, size=18, color="FFFFFF")
t.alignment = center
for row in ws["B2:I2"]:
for row in ws["B2:J2"]:
for c in row:
c.fill = PatternFill("solid", fgColor=NAVY)
ws.row_dimensions[2].height = 34
ws.merge_cells("B3:I3")
ws.merge_cells("B3:J3")
sub = ws["B3"]
sub.value = f"数据快照 · 生成于 {generated_at}"
sub.font = Font(name=FONT, size=10, color=GRAY)
@@ -413,11 +438,12 @@ def build_summary(ws, results, generated_at):
("未到总件数", t_miss, RED, "#,##0"),
("总体未到率", rate, RED, "0.0%"),
]
# 2-3-2-2 分布填满 B-J9 列),配合上方列宽使 4 卡视觉等宽
spans = [
("B5:C5", "B6:C6"),
("D5:E5", "D6:E6"),
("F5:G5", "F6:G6"),
("H5:I5", "H6:I6"),
("D5:F5", "D6:F6"),
("G5:H5", "G6:H6"),
("I5:J5", "I6:J6"),
]
card_bg = PatternFill("solid", fgColor=CARD_BG)
thin = Side(style="thin", color=TILE)
@@ -446,12 +472,12 @@ def build_summary(ws, results, generated_at):
ws.row_dimensions[7].height = 8
# —— 小节标题 ——
ws.merge_cells("B8:I8")
ws.merge_cells("B8:J8")
sec = ws["B8"]
sec.value = "各站点明细统计"
sec.font = Font(name=FONT, bold=True, size=12, color=NAVY)
sec.alignment = Alignment(horizontal="left", vertical="center")
for row in ws["B8:I8"]:
for row in ws["B8:J8"]:
for c in row:
c.border = Border(bottom=Side(style="medium", color=BLUE))
ws.row_dimensions[8].height = 22
@@ -466,16 +492,18 @@ def build_summary(ws, results, generated_at):
"未到率",
"完全未到运单",
"部分未到运单",
"数据日期",
]
head_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
for i, h in enumerate(headers):
col = chr(ord("B") + i)
cell = ws[f"{col}9"]
cell.value = h
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = center
cell.alignment = head_align
cell.border = BORDER
ws.row_dimensions[9].height = 22
ws.row_dimensions[9].height = 30
# —— 各站数据行4 站 + 百世)——
r = 10
@@ -498,6 +526,7 @@ def build_summary(ws, results, generated_at):
s["完全未到"],
s["部分未到"],
]
vals.append(dates.get(name, "")) # 末列:该站业务日期
for i, v in enumerate(vals):
col = chr(ord("B") + i)
cell = ws[f"{col}{r}"]
@@ -519,7 +548,7 @@ def build_summary(ws, results, generated_at):
# —— 合计行 ——
tot_fill = PatternFill("solid", fgColor=LIGHTBLUE)
tot_font = Font(name=FONT, bold=True, size=10)
totals = ["合计", t_wb, t_exp, t_arr, t_miss, rate, t_full, t_part]
totals = ["合计", t_wb, t_exp, t_arr, t_miss, rate, t_full, t_part, ""]
for i, v in enumerate(totals):
col = chr(ord("B") + i)
cell = ws[f"{col}{r}"]
@@ -554,6 +583,7 @@ def build_summary(ws, results, generated_at):
chart.x_axis.delete = False
chart.y_axis.delete = False
chart.legend.position = "b"
chart.legend.overlay = False # 不覆盖绘图区:图例独占底部一行,与 X 轴站点名错开
chart.height = 9
chart.width = 20
ws.add_chart(chart, f"B{chart_anchor}")
@@ -565,10 +595,11 @@ def build_summary(ws, results, generated_at):
"合计 / 图表仅含 4 站(顺心/中通/韵达/安能,应到−实到口径);百世为站点直供未到、无应到基数,单列不计入合计。",
"本次下载失败的站点标注为(无数据)并计 0不影响其余站点统计。",
"明细见各站点工作表4 站缺件的子单号 / 扫描单号按各站编号规则生成,并非实到原始记录。",
"数据日期:各站本次纳入数据对应的业务日期(=应到数据下载日 日期偏移;韵达偏移 1 为前一日);合计为多站混合、不标注。",
]
for k, text in enumerate(notes):
rr = note_row + k
ws.merge_cells(f"B{rr}:I{rr}")
ws.merge_cells(f"B{rr}:J{rr}")
cell = ws[f"B{rr}"]
cell.value = text
cell.font = Font(name=FONT, size=9, color=GRAY)
@@ -579,7 +610,7 @@ def build_summary(ws, results, generated_at):
ws.page_setup.fitToHeight = 0
ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)
ws.page_margins = PageMargins(left=0.4, right=0.4, top=0.5, bottom=0.5)
ws.print_area = f"A1:I{note_row + 1}"
ws.print_area = f"A1:J{note_row + 1}"
# ============================ 主流程 ============================
@@ -605,7 +636,8 @@ def main():
if not include:
print("未处理任何站点:请确认 downloads/ 下存在源数据文件。")
return
undel = build_full_report(include)
dates = _read_business_dates(include)
undel = build_full_report(include, dates=dates)
print("-" * 56)
for name in ALL_REPORT_SITES:
if name in include: