Files
mileage-bonus/build_july_multi_source_summary.py
T

217 lines
8.6 KiB
Python

from collections import defaultdict
from datetime import datetime
from pathlib import Path
from zipfile import ZipFile
from xml.etree import ElementTree as ET
import re
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
BASE = Path("/Users/kkfluous/Downloads")
ASSESSMENT = BASE / "7月核算结果/租赁任务考核_2026年7月.xlsx"
PLATFORM_DIR = BASE / "广州新能源平台_纯氢纯电里程_2026年7月_按日"
GPS_FILES = [
BASE / "里程统计[天][2026-07-01至2026-07-31].xlsx",
BASE / "里程统计[天][2026-07-01至2026-07-31] (1).xlsx",
]
OUTPUT = BASE / "7月核算结果/7月考核车辆多源里程汇总.xlsx"
NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
def normalize_plate(value):
return re.sub(r"\s+", "", str(value or "")).upper()
def parse_gps_file(path):
result = {}
with ZipFile(path) as zf:
root = ET.fromstring(zf.read("xl/worksheets/sheet1.xml"))
for row in root.findall(f".//{NS}row"):
if int(row.get("r")) < 4:
continue
values = {}
for cell in row.findall(f"{NS}c"):
col = re.match(r"[A-Z]+", cell.get("r")).group()
if cell.get("t") == "inlineStr":
values[col] = "".join(t.text or "" for t in cell.findall(f".//{NS}t"))
else:
node = cell.find(f"{NS}v")
values[col] = node.text if node is not None else None
plate = normalize_plate(values.get("H"))
date_text = str(values.get("O") or "").strip()
if not plate or plate == "合计" or not re.fullmatch(r"2026-07-\d{2}", date_text):
continue
result[(plate, datetime.strptime(date_text, "%Y-%m-%d").date())] = float(values.get("P") or 0)
return result
platform = defaultdict(lambda: [0.0, 0.0])
platform_presence = set()
for path in sorted(PLATFORM_DIR.glob("2026-07-??_纯氢纯电里程.xlsx")):
wb = load_workbook(path, read_only=True, data_only=True)
ws = wb.active
for row in ws.iter_rows(min_row=2, values_only=True):
plate = normalize_plate(row[5])
date_match = re.search(r"2026-07-\d{2}", str(row[6] or ""))
if not plate or not date_match:
continue
date = datetime.strptime(date_match.group(), "%Y-%m-%d").date()
electric = float(row[7] or 0)
hydrogen = float(row[8] or 0)
platform[(plate, date)][0] += hydrogen
platform[(plate, date)][1] += electric
platform_presence.add((plate, date))
wb.close()
gps = {}
for path in GPS_FILES:
incoming = parse_gps_file(path)
overlap = set(gps) & set(incoming)
if overlap:
raise RuntimeError(f"GPS来源存在重复车辆日期:{sorted(overlap)[:5]}")
gps.update(incoming)
source_wb = load_workbook(ASSESSMENT, read_only=True, data_only=True)
source_ws = source_wb["业务考核视图"]
headers = [c.value for c in source_ws[1]]
idx = {name: i for i, name in enumerate(headers)}
records = []
for row in source_ws.iter_rows(min_row=2, values_only=True):
plate = normalize_plate(row[idx["车牌号"]])
if not plate:
continue
start = row[idx["考核开始日期"]].date()
end = row[idx["考核结束日期"]].date()
dates = [(start.fromordinal(start.toordinal() + i)) for i in range((end - start).days + 1)]
p_dates = [d for d in dates if (plate, d) in platform_presence]
if p_dates:
hydrogen = sum(platform[(plate, d)][0] for d in p_dates)
electric = sum(platform[(plate, d)][1] for d in p_dates)
else:
hydrogen = electric = None
test_mileage = row[idx["测试里程(km)"]]
is_test = isinstance(test_mileage, (int, float)) and test_mileage > 0
g_dates = [d for d in dates if (plate, d) in gps]
gps_total = sum(gps[(plate, d)] for d in g_dates) if g_dates else None
# 7月最终考核源表的“实际行驶里程”已按原处理链路写入测试车辆GPS里程:
# 优先智能管车日报,缺失时用两份GPS日明细补充。补充明细未命中时,
# 必须从最终源表回填,不能误标为“无来源”。
if gps_total is None and is_test:
gps_total = row[idx["实际行驶里程(km)"]]
records.append({
"plate": plate,
"customer": row[idx["客户名称"]] or "",
"start": start,
"end": end,
"days": (end - start).days + 1,
"test": "是" if is_test else "否",
"hydrogen": hydrogen,
"electric": electric,
"tbox": row[idx["原TBOX里程(km)"]] if row[idx["原TBOX里程(km)"]] is not None else None,
"gps": gps_total,
})
source_wb.close()
wb = Workbook()
ws = wb.active
ws.title = "7月考核车辆里程汇总"
columns = [
"序号", "车牌", "客户", "考核开始日期", "考核结束日期", "考核天数", "是否存在测试",
"纯氢里程(km)", "纯电里程(km)", "广州平台总里程(km)", "TBOX里程(km)", "GPS里程(km)",
]
ws.append(columns)
for n, rec in enumerate(records, 1):
row_num = ws.max_row + 1
ws.append([
n, rec["plate"], rec["customer"], rec["start"], rec["end"], rec["days"], rec["test"],
rec["hydrogen"] if rec["hydrogen"] is not None else "无来源",
rec["electric"] if rec["electric"] is not None else "无来源",
round(rec["hydrogen"] + rec["electric"], 3) if rec["hydrogen"] is not None else "无来源",
rec["tbox"] if rec["tbox"] is not None else "无来源",
rec["gps"] if rec["gps"] is not None else "无来源",
])
ws.insert_rows(1)
ws.merge_cells("A1:L1")
ws["A1"] = (
"1. 纯氢里程(km)、纯电里程(km)来源于广州平台统计,TBOX里程来源于车机上报的仪表盘里程统计,"
"GPS里程优先按两份7月GPS日明细汇总;测试车辆未命中时,回填7月最终考核源表中已确认的GPS实际里程\n"
"2. 广州平台总里程(km)=纯氢里程(km)+纯电里程(km)\n"
"3. 已确认广州平台暂未接入86辆普货车数据;无对应记录统一标记为“无来源”"
)
header_fill = PatternFill("solid", fgColor="1F4E78")
platform_fill = PatternFill("solid", fgColor="DDEBF7")
tbox_fill = PatternFill("solid", fgColor="E2F0D9")
gps_fill = PatternFill("solid", fgColor="E4DFEC")
test_fill = PatternFill("solid", fgColor="FFF2CC")
missing_fill = PatternFill("solid", fgColor="F2F2F2")
thin = Side(style="thin", color="B7B7B7")
for cell in ws[2]:
cell.fill = header_fill
cell.font = Font(color="FFFFFF", bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = Border(bottom=Side(style="medium", color="FFFFFF"))
for row in ws.iter_rows(min_row=3, max_row=ws.max_row):
is_test = row[6].value == "是"
for cell in row:
cell.border = Border(left=thin, right=thin, top=thin, bottom=thin)
cell.alignment = Alignment(horizontal="center", vertical="center")
row[2].alignment = Alignment(horizontal="left", vertical="center")
if is_test:
for cell in row:
cell.fill = test_fill
else:
for cell in row[7:10]:
cell.fill = platform_fill
row[10].fill = tbox_fill
row[11].fill = gps_fill
for cell in row[7:12]:
if cell.value == "无来源":
cell.fill = missing_fill
cell.font = Font(color="7F7F7F", italic=True)
for row in range(3, ws.max_row + 1):
ws.cell(row, 4).number_format = "yyyy-mm-dd"
ws.cell(row, 5).number_format = "yyyy-mm-dd"
for col in range(8, 13):
ws.cell(row, col).number_format = "#,##0.0"
widths = [8, 14, 34, 15, 15, 11, 14, 17, 17, 23, 17, 17]
for col, width in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(col)].width = width
ws["A1"].fill = PatternFill("solid", fgColor="FFF2CC")
ws["A1"].font = Font(color="7F6000", italic=True)
ws["A1"].alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
ws.row_dimensions[1].height = 58
ws.row_dimensions[2].height = 36
ws.freeze_panes = "A3"
ws.auto_filter.ref = f"A2:L{ws.max_row}"
ws.sheet_view.showGridLines = False
ws.sheet_properties.pageSetUpPr.fitToPage = True
ws.page_setup.orientation = "landscape"
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0
ws.print_title_rows = "1:2"
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
wb.save(OUTPUT)
print({
"output": str(OUTPUT),
"records": len(records),
"test_records": sum(r["test"] == "是" for r in records),
"platform_missing": sum(r["hydrogen"] is None for r in records),
"tbox_missing": sum(r["tbox"] is None for r in records),
"gps_missing": sum(r["gps"] is None for r in records),
})