#!/usr/bin/env python3 """复刻6月干预说明配色,并将达标分类改为一行一人。""" from copy import copy from collections import defaultdict from pathlib import Path import openpyxl BASE = Path('/Users/kkfluous/Downloads') JUL = BASE / '7月核算结果/租赁任务考核_2026年7月_干预情况说明.xlsx' JUN = BASE / '6月核算结果/租赁任务考核_2026年6月_干预情况说明.xlsx' SRC = BASE / '7月核算结果/租赁任务考核_2026年7月.xlsx' BAK = BASE / '7月核算结果/租赁任务考核_2026年7月_原版备份.xlsx' TEST = Path('/Users/kkfluous/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/wxid_1704407055112_3af8/temp/RWTemp/2026-08/4340099467fbf08b4c6583c49adba4ee/7月测试车辆明细.xlsx') def recs(path): wb = openpyxl.load_workbook(path, data_only=True) ws = wb['业务考核视图'] h = [c.value for c in ws[1]] out = [dict(zip(h, r)) for r in ws.iter_rows(min_row=2, values_only=True) if r[0]] wb.close() return out def clone_style(src, dst): dst.font = copy(src.font) dst.fill = copy(src.fill) dst.border = copy(src.border) dst.alignment = copy(src.alignment) dst.protection = copy(src.protection) dst.number_format = src.number_format def style_row(ws, row, cols, ref_ws, ref_row, ref_cols=None): ref_cols = ref_cols or cols for c, rc in zip(cols, ref_cols): clone_style(ref_ws.cell(ref_row, rc), ws.cell(row, c)) new = recs(SRC) old = recs(BAK) old_map = {} for r in old: old_map[r['车牌号']] = r twb = openpyxl.load_workbook(TEST, data_only=True) test_plates = {str(r[0]).strip() for r in twb.active.iter_rows(min_row=2, values_only=True) if r[0]} test_plates.discard('粤AG37316') twb.close() intervention = [r for r in new if r['车牌号'] in test_plates] flip = [] for r in intervention: old_r = old_map.get(r['车牌号']) if old_r and old_r.get('是否达标') == '达标' and r.get('是否达标') != '达标': flip.append(r) groups = defaultdict(lambda: defaultdict(int)) for r in flip: groups[r.get('考核目标') or '未知'][(r.get('部门名称') or '', r.get('销售经理') or '')] += 1 wb = openpyxl.load_workbook(JUL) ref = openpyxl.load_workbook(JUN) ws = wb['测试与考核情况说明'] rws = ref['测试与考核情况说明'] # 清空原说明页后,按6月视觉规范重建。 for merged in list(ws.merged_cells.ranges): ws.unmerge_cells(str(merged)) ws.delete_rows(1, ws.max_row) for col in range(1, 8): letter = openpyxl.utils.get_column_letter(col) ws.column_dimensions[letter].width = rws.column_dimensions[letter].width total_plates = len({r['车牌号'] for r in new}) interv_plates = len({r['车牌号'] for r in intervention}) total_tbox = sum(float(r.get('原TBOX里程(km)') or 0) for r in new) total_actual = sum(float(r.get('实际行驶里程(km)') or 0) for r in new) total_test = sum(float(r.get('测试里程(km)') or 0) for r in new) interv_tbox = sum(float(r.get('原TBOX里程(km)') or 0) for r in intervention) interv_actual = sum(float(r.get('实际行驶里程(km)') or 0) for r in intervention) interv_test = sum(float(r.get('测试里程(km)') or 0) for r in intervention) # 标题及说明:深蓝标题、浅黄说明,与6月一致。 ws.merge_cells('A1:G1'); ws['A1'] = '2026年7月 业务考核 — 测试与考核情况说明'; clone_style(rws['A1'], ws['A1']) ws.row_dimensions[1].height = rws.row_dimensions[1].height ws.merge_cells('A2:G3'); ws['A2'] = ('7月业务考核车辆中,测试车辆统一以指定GPS日里程按考核期汇总替换TBOX里程,差额作为测试里程剥离且最低取0。\n' '粤AG37316因GPS缺失,经业务确认暂按0核算。'); clone_style(rws['A2'], ws['A2']) ws.row_dimensions[2].height = rws.row_dimensions[2].height ws.merge_cells('A5:G5'); ws['A5'] = '一、7月业务考核与干预情况'; clone_style(rws['A5'], ws['A5']) headers = ['项目', '7月业务考核车辆', '涉及测试的车辆', '占比'] for c, v in enumerate(headers, 1): ws.cell(6,c,v); clone_style(rws.cell(6,c), ws.cell(6,c)) stats = [ ('车辆数(辆)', total_plates, interv_plates), ('产生考核记录数(条)', len(new), len(intervention)), ('仪表盘里程(km)', round(total_tbox), round(interv_tbox)), ('业务考核里程 GPS(km)', round(total_actual), round(interv_actual)), ('测试里程(km)', round(total_test), round(interv_test)), ] for rr, (label, allv, partv) in enumerate(stats, 7): for c, v in enumerate([label, allv, partv, partv/allv if allv else 0], 1): ws.cell(rr,c,v); clone_style(rws.cell(7 + ((rr-7)%2),c), ws.cell(rr,c)) # “测试里程”比例改为测试车辆剔除率,避免原100%被理解成车辆全部里程均为测试。 ws['D6'] = '占比/剔除率' ws['D11'] = interv_test / interv_tbox if interv_tbox else 0 negative_floor_adjustment = sum(max(0, float(r.get('实际行驶里程(km)') or 0) - float(r.get('原TBOX里程(km)') or 0)) for r in intervention) ws.merge_cells('A12:G12') ws['A12'] = (f'勾稽:测试里程 {round(interv_tbox):,} − {round(interv_actual):,} + ' f'{round(negative_floor_adjustment):,}(负差归0调整)= {round(interv_test):,} km') clone_style(rws['A2'], ws['A12']) ws.row_dimensions[12].height = 22 ws.merge_cells('A13:G13'); ws['A13']='二、干预车辆考核达标情况说明'; clone_style(rws['A13'],ws['A13']) ws.merge_cells('A14:G14'); ws['A14']=f'共 {len(intervention)} 条考核记录涉及里程干预;替换为GPS里程后,共 {len(flip)} 条考核车辆记录由“达标”转判为“未达标”。按车型分类如下:'; clone_style(rws['A14'],ws['A14']) target_order = ['羚牛136辆4.5T冷链车','羚牛100辆18T','交投40辆4.5T普货','交投190辆4.5T冷链车','恒运50辆4.5T普货','恒运50辆4.5T 普货'] rn = 16 for target in target_order: items = sorted(groups.get(target, {}).items()) if not items: continue total = sum(v for _, v in items) ws.merge_cells(start_row=rn,start_column=1,end_row=rn,end_column=3) ws.cell(rn,1,f'◎{target}({total} 辆)'); clone_style(rws['A15'],ws.cell(rn,1)); rn += 1 for c,v in enumerate(['部门','销售经理','数量'],1): ws.cell(rn,c,v); clone_style(rws.cell(16,c),ws.cell(rn,c)) rn += 1 for idx, ((dept,sales),cnt) in enumerate(items): for c,v in enumerate([dept,sales,cnt],1): ws.cell(rn,c,v); clone_style(rws.cell(17 + idx%2,c),ws.cell(rn,c)) rn += 1 rn += 1 # 数据来源说明沿用6月蓝色区域。 ws.merge_cells(start_row=rn,start_column=1,end_row=rn,end_column=7); ws.cell(rn,1,'三、里程三列数据来源说明'); clone_style(rws['A13'],ws.cell(rn,1)); rn += 1 for c,v in enumerate(['列','字段名','数据来源','说明'],1): ws.cell(rn,c,v); clone_style(rws.cell(6,c),ws.cell(rn,c)) rn += 1 descs = [ ('O','原TBOX里程(km)','车辆 T-Box / 仪表盘读数','业务系统原始TBOX里程,包含运营、测试及调试里程。'), ('P','实际行驶里程(km)','指定GPS日里程文件','按考核日期逐日汇总GPS里程;粤AG37316经确认暂按0核算。'), ('Q','测试里程(km)','O − P(最低取0)','原TBOX里程与GPS里程的非负差额,不计入考核。'), ] for idx,row in enumerate(descs): for c,v in enumerate(row,1): ws.cell(rn,c,v); clone_style(rws.cell(28 + idx,c),ws.cell(rn,c)) ws.row_dimensions[rn].height = rws.row_dimensions[28 + idx].height rn += 1 ws.sheet_view.showGridLines = rws.sheet_view.showGridLines ws.freeze_panes = rws.freeze_panes wb.save(JUL) wb.close(); ref.close() print(f'updated={JUL}') print(f'rows={rn-1} flip={len(flip)} groups={sum(len(v) for v in groups.values())}')