#!/usr/bin/env python3 """生成 租赁任务考核_2026年5月_干预情况说明.xlsx — 公式计算 + %格式 + 完整匹配参考样式""" import os, openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers from openpyxl.utils import get_column_letter from collections import defaultdict os.chdir('/Users/kkfluous/Downloads') SRC = '租赁任务考核_2026年5月.xlsx' BAK = '租赁任务考核_2026年5月_原版备份.xlsx' # ─── Styles ─── DARK_BLUE = PatternFill('solid', fgColor='1F4E78') LIGHT_BLUE = PatternFill('solid', fgColor='D9E1F2') HD_BLUE = PatternFill('solid', fgColor='4472C4') YELLOW_BG = PatternFill('solid', fgColor='FFF2CC') ALT_GRAY = PatternFill('solid', fgColor='F2F2F2') YELLOW_HL = PatternFill('solid', fgColor='FFFF00') NO_FILL = PatternFill('solid', fgColor='00000000') TITLE_F = Font(bold=True, size=14, color='FFFFFF') SEC_F = Font(bold=True, size=12, color='1F4E78') SUB_F = Font(bold=True, size=11, color='1F4E78') HDR_F = Font(bold=True, size=11, color='FFFFFF') BODY_F = Font(size=11) BOLD_F = Font(bold=True, size=11) RED_F = Font(size=10, color='FF0000') DESC_F = Font(size=10) THIN = Side('thin', color='B4B4B4') BORD = Border(THIN, THIN, THIN, THIN) CC = Alignment(horizontal='center', vertical='center', wrap_text=True) LL = Alignment(horizontal='left', vertical='center', wrap_text=True) LT = Alignment(horizontal='left', vertical='top', wrap_text=True) # ─── Load yellow intervention plates ─── wb_i = openpyxl.load_workbook('标黄为5月干预车辆.xlsx') ws_i = wb_i['Sheet1'] YELLOW_PLATES = set() for row in ws_i.iter_rows(min_row=2, values_only=False): cell = row[0] fill = cell.fill is_y = (fill and fill.patternType == 'solid' and fill.start_color and fill.start_color.rgb and 'FFFF00' in str(fill.start_color.rgb)) if is_y: p = str(cell.value).strip() if cell.value else '' if p: YELLOW_PLATES.add(p) wb_i.close() # 手动移除4辆已确认非干预 YELLOW_PLATES.difference_update({'粤A03186F', '粤A01128F', '粤A08391F', '粤A01396F'}) print(f"Yellow plates: {len(YELLOW_PLATES)} (removed 4)") # ─── Load data ─── def load_recs(fp): wb = openpyxl.load_workbook(fp, data_only=True) ws = wb['业务考核视图'] h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))] recs = [] for row in ws.iter_rows(min_row=2, values_only=True): if row[0]: recs.append(dict(zip(h, row))) wb.close() return recs, h src_recs, src_h = load_recs(SRC) bak_recs, bak_h = load_recs(BAK) print(f"Source: {len(src_recs)} rows, cols={len(src_h)}") print(f"Backup: {len(bak_recs)} rows, cols={len(bak_h)}") # Identify intervention records by plate (yellow list) intv_recs = [] for sr in src_recs: plate = str(sr['车牌号']).strip() if plate not in YELLOW_PLATES: continue gps = float(sr.get('实际行驶里程(km)', 0) or 0) tbox = float(sr.get('原TBOX里程(km)', 0) or 0) test = float(sr.get('测试里程(km)', 0) or 0) tgt = float(sr.get('应考核里程(km)', 0) or 0) # Find backup counterpart key = (plate, str(sr.get('销售经理','')), str(sr.get('项目名称',''))) br = None for b in bak_recs: bk = (str(b['车牌号']).strip(), str(b.get('销售经理','')), str(b.get('项目名称',''))) if bk == key: br = b; break old_actual = float(br['实际行驶里程(km)'] or 0) if br else 0 old_pass = str(br['是否达标']).strip() if br else '?' new_pass = str(sr.get('是否达标','')).strip() flip = '' if old_pass == '达标' and new_pass == '未达标': flip = '达→未达标' elif old_pass == '未达标' and new_pass == '达标': flip = '未→达标' intv_recs.append(dict(plate=plate, dept=sr.get('部门名称',''), sales=sr.get('销售经理',''), client=sr.get('客户名称',''), target_name=sr.get('考核目标',''), tgt_km=tgt, tbox=tbox, gps=gps, test=test, flip=flip, days=sr.get('考核天数'), rate=sr.get('完成率(%)'), new_pass=new_pass, old_pass=old_pass)) print(f"Intervention records: {len(intv_recs)}") flip_down = [r for r in intv_recs if r['flip'] == '达→未达标'] flip_up = [r for r in intv_recs if r['flip'] == '未→达标'] print(f"达→未达标: {len(flip_down)}, 未→达标: {len(flip_up)}") # ─── Reference column layout (O=GPS, P=TBOX, Q=Test) ─── # Build full records with columns in reference order REF_HEADERS = [ '车牌号', '考核年份', '考核月份', '部门名称', '销售经理', '客户名称', '合同编号', '项目名称', '考核目标ID', '考核目标', '考核开始日期', '考核结束日期', '考核天数', '应考核里程(km)', '实际行驶里程(km)', # O = GPS (考核口径) '原TBOX里程(km)', # P = TBOX (仪表盘) '测试里程(km)', # Q = test '完成率(%)', '是否达标', '考核状态', '交车日期', '预计还车日期', '实际还车日期', ] def reorder_rec(src_rec): """Map source record to reference column order. O=GPS, P=TBOX, Q=Test. For non-intv: O=仪表盘, P=空, Q=0. For intv: O=GPS, P=仪表盘, Q=GPS-仪表盘.""" out = {} plate = str(src_rec.get('车牌号','')).strip() is_intv = plate in YELLOW_PLATES for h in REF_HEADERS: if h == '实际行驶里程(km)': # O column: always use source's actual (which is仪表盘 for non-intv, GPS for intv) out[h] = src_rec.get('实际行驶里程(km)') elif h == '原TBOX里程(km)': # P column: only fill if intv, else empty out[h] = src_rec.get('原TBOX里程(km)') if is_intv else None elif h == '测试里程(km)': out[h] = src_rec.get('测试里程(km)') if is_intv else 0 else: out[h] = src_rec.get(h) return out all_ref = [reorder_rec(r) for r in src_recs] intv_ref = [reorder_rec(r) for r in src_recs if str(r['车牌号']).strip() in YELLOW_PLATES] # ─── Build workbook ─── wb = openpyxl.Workbook() def hdr_row(ws, row, ncols): for c in range(1, ncols+1): cell = ws.cell(row=row, column=c) cell.fill = HD_BLUE; cell.font = HDR_F; cell.alignment = CC; cell.border = BORD def dat_row(ws, row, ncols, alt=False): for c in range(1, ncols+1): cell = ws.cell(row=row, column=c) cell.font = BODY_F; cell.alignment = CC; cell.border = BORD if alt: cell.fill = ALT_GRAY # ═══════════════════════════════ # Sheets 2,3,4 first (so Sheet1 formulas can reference them) # ═══════════════════════════════ def write_data_sheet(wb, name, records, highlight_intv=False): ws = wb.create_sheet(name) for ci, h in enumerate(REF_HEADERS, 1): ws.cell(row=1, column=ci, value=h) hdr_row(ws, 1, len(REF_HEADERS)) for ri, rec in enumerate(records): r = ri + 2 is_intv = str(rec.get('车牌号','')).strip() in YELLOW_PLATES for ci, h in enumerate(REF_HEADERS, 1): val = rec.get(h) if val is not None: ws.cell(row=r, column=ci, value=val) ws.cell(row=r, column=ci).border = BORD ws.cell(row=r, column=ci).font = Font(size=10) ws.cell(row=r, column=ci).alignment = CC if highlight_intv and is_intv: for ci in range(1, len(REF_HEADERS)+1): ws.cell(row=r, column=ci).fill = YELLOW_HL for ci in range(1, len(REF_HEADERS)+1): ws.column_dimensions[get_column_letter(ci)].width = 12 return ws # Sheet 2: 业务考核视图 (all records, intv highlighted yellow) ws2 = write_data_sheet(wb, '业务考核视图', all_ref, highlight_intv=True) # Sheet 3: 5月业务考核车辆总览 (all records, no highlight) ws3 = write_data_sheet(wb, '5月业务考核车辆总览', all_ref, highlight_intv=False) # Sheet 4: 涉及测试的业务考核车辆 (intv only) ws4 = write_data_sheet(wb, '涉及测试的业务考核车辆', intv_ref, highlight_intv=False) # ═══════════════════════════════ # Sheet 1: 测试与考核情况说明 (with formulas) # ═══════════════════════════════ ws1 = wb.active ws1.title = '测试与考核情况说明' MAX_R = 500 # generous row range for formulas # R1: Title ws1.merge_cells('A1:G1') c = ws1.cell(row=1, column=1, value='2026年5月 业务考核 — 测试与考核情况说明') c.fill = DARK_BLUE; c.font = TITLE_F; c.alignment = CC ws1.row_dimensions[1].height = 32 # R2-R3: Description ws1.merge_cells('A2:G3') desc = ('5月业务考核车辆中,部分车辆当月存在测试行为(仪表盘里程 > GPS 实际行驶里程),其差额作为测试里程剥离,不参与考核。\n' '对于有测试行为的车辆,使用GPS里程作为实际业务考核里程并参与绩效计算。\n' '对于无5月GPS数据的干预车辆,实际行驶里程记为0,判定未达标。\n' '下表数据由公式实时计算,源sheet数据更新后自动刷新。') c = ws1.cell(row=2, column=1, value=desc) c.fill = YELLOW_BG; c.font = DESC_F; c.alignment = LT ws1.row_dimensions[2].height = 72 # R4: empty spacer # R5: Section 一 ws1.merge_cells('A5:G5') c = ws1.cell(row=5, column=1, value='一、5月业务考核与干预情况') c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL ws1.row_dimensions[5].height = 24 # R6: Table header for ci, h in enumerate(['项目', '5月业务考核车辆', '涉及测试的车辆', '占比', '', '', ''], 1): ws1.cell(row=6, column=ci, value=h) hdr_row(ws1, 6, 4) ws1.row_dimensions[6].height = 24 # R7-R11: Formula rows # Sheet references for formulas (openpyxl writes them as-is) S3 = "'5月业务考核车辆总览'" S4 = "'涉及测试的业务考核车辆'" formula_rows = [ # (row, label, B_formula, C_formula, B_fmt, C_fmt, D_fmt) (7, '车辆数(辆)', f'=SUMPRODUCT(({S3}!A2:A{MAX_R}<>"")/(COUNTIF({S3}!A2:A{MAX_R},{S3}!A2:A{MAX_R})+({S3}!A2:A{MAX_R}="")))', f'=SUMPRODUCT(({S4}!A2:A{MAX_R}<>"")/(COUNTIF({S4}!A2:A{MAX_R},{S4}!A2:A{MAX_R})+({S4}!A2:A{MAX_R}="")))', '#,##0', '#,##0', '0.00%'), (8, '产生考核记录数(条)', f'=COUNTA({S3}!A2:A{MAX_R})', f'=COUNTA({S4}!A2:A{MAX_R})', '#,##0', '#,##0', '0.00%'), (9, '仪表盘里程(km)', f'=SUM({S3}!O2:O{MAX_R})+SUM({S4}!Q2:Q{MAX_R})', f'=SUM({S4}!O2:O{MAX_R})+SUM({S4}!Q2:Q{MAX_R})', '#,##0.0', '#,##0.0', '0.00%'), (10, '业务考核里程 GPS(km)', f'=SUM({S3}!O2:O{MAX_R})', f'=SUM({S4}!O2:O{MAX_R})', '#,##0.0', '#,##0.0', '0.00%'), (11, '测试里程(km)', f'=SUM({S4}!Q2:Q{MAX_R})', f'=SUM({S4}!Q2:Q{MAX_R})', '#,##0.0', '#,##0.0', '0.00%'), ] for ri, (row_num, label, b_f, c_f, b_fmt, c_fmt, d_fmt) in enumerate(formula_rows): r = row_num ws1.cell(row=r, column=1, value=label).font = BOLD_F ws1.cell(row=r, column=1).alignment = LL ws1.cell(row=r, column=1).border = BORD ws1.cell(row=r, column=2, value=b_f).number_format = b_fmt ws1.cell(row=r, column=2).font = BODY_F ws1.cell(row=r, column=2).alignment = CC ws1.cell(row=r, column=2).border = BORD ws1.cell(row=r, column=3, value=c_f).number_format = c_fmt ws1.cell(row=r, column=3).font = BODY_F ws1.cell(row=r, column=3).alignment = CC ws1.cell(row=r, column=3).border = BORD ws1.cell(row=r, column=4, value=f'=IFERROR(C{r}/B{r},0)').number_format = d_fmt ws1.cell(row=r, column=4).font = BODY_F ws1.cell(row=r, column=4).alignment = CC ws1.cell(row=r, column=4).border = BORD if ri % 2 == 1: for c in range(1, 5): ws1.cell(row=r, column=c).fill = ALT_GRAY ws1.row_dimensions[r].height = 22 # R12: empty spacer # R13: Section 二 ws1.merge_cells('A13:G13') c = ws1.cell(row=13, column=1, value='二、干预车辆考核达标情况说明') c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL ws1.row_dimensions[13].height = 24 # R14: Summary description ws1.merge_cells('A14:G14') flip_desc = (f'共 {len(intv_recs)} 条考核记录涉及里程干预;替换为GPS里程后,' f'共 {len(flip_down)} 条考核车辆记录由"达标"转判为"未达标"。按车型分类如下:') c = ws1.cell(row=14, column=1, value=flip_desc) c.font = RED_F; c.alignment = LL ws1.row_dimensions[14].height = 28 # Group by vehicle type by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) flip_by_target = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) for r in intv_recs: by_target[r['target_name']][r['dept']][r['sales']] += 1 for r in flip_down: flip_by_target[r['target_name']][r['dept']][r['sales']] += 1 target_order = sorted(by_target.keys(), key=lambda t: -sum(sum(s.values()) for s in flip_by_target.get(t, {}).values())) row = 15 for tgt in target_order: dept_sales = flip_by_target.get(tgt, {}) total_count = sum(sum(s.values()) for s in by_target[tgt].values()) flip_count = sum(sum(s.values()) for s in dept_sales.values()) if flip_count == 0: continue # only show types with flips # Vehicle type title if flip_count > 0: title = f'▎{tgt}({flip_count} 辆)' else: title = f'▎{tgt}({total_count} 辆)' ws1.merge_cells(start_row=row, start_column=1, end_row=row, end_column=7) c = ws1.cell(row=row, column=1, value=title) c.fill = LIGHT_BLUE; c.font = SUB_F; c.alignment = LL for ci in range(1, 8): ws1.cell(row=row, column=ci).border = BORD ws1.row_dimensions[row].height = 22 row += 1 # Headers (two columns side-by-side: A-C and E-G) for base in [1, 5]: for ci, h in enumerate(['部门', '销售经理', '数量'], base): ws1.cell(row=row, column=ci, value=h) for ci in range(1, 8): ws1.cell(row=row, column=ci).fill = HD_BLUE ws1.cell(row=row, column=ci).font = HDR_F ws1.cell(row=row, column=ci).alignment = CC ws1.cell(row=row, column=ci).border = BORD ws1.row_dimensions[row].height = 22 row += 1 # Flatten sales list sorted by department then count dept_order = sorted(dept_sales.keys(), key=lambda d: -sum(dept_sales[d].values())) sales_list = [] for dept in dept_order: for sales in sorted(dept_sales[dept].keys()): sales_list.append((dept, sales, dept_sales[dept][sales])) n = len(sales_list) left = sales_list[:(n+1)//2] right = sales_list[(n+1)//2:] for ri2 in range(max(len(left), len(right))): if ri2 < len(left): d, s, cnt = left[ri2] ws1.cell(row=row, column=1, value=d) ws1.cell(row=row, column=2, value=s) ws1.cell(row=row, column=3, value=cnt) if ri2 < len(right): d, s, cnt = right[ri2] ws1.cell(row=row, column=5, value=d) ws1.cell(row=row, column=6, value=s) ws1.cell(row=row, column=7, value=cnt) for ci in range(1, 8): cell = ws1.cell(row=row, column=ci) cell.border = BORD; cell.font = BODY_F; cell.alignment = CC if ri2 % 2 == 1: cell.fill = ALT_GRAY ws1.row_dimensions[row].height = 22 row += 1 row += 1 # blank between vehicle types # Section 三 ws1.merge_cells(start_row=row, start_column=1, end_row=row, end_column=7) c = ws1.cell(row=row, column=1, value='三、里程三列数据来源说明') c.fill = LIGHT_BLUE; c.font = SEC_F; c.alignment = LL ws1.row_dimensions[row].height = 24 row += 1 for ci, h in enumerate(['列', '字段名', '数据来源', '说明', '', '', ''], 1): ws1.cell(row=row, column=ci, value=h) hdr_row(ws1, row, 4) ws1.row_dimensions[row].height = 24 row += 1 ref_data = [ ('O', '实际行驶里程(km)', 'GPS 轨迹还原', '基于 G7 GPS 定位轨迹按考核期逐日汇总计算的运营里程(total_mileage/100000=km),本月里程考核口径。干预车辆已替换为GPS里程,非干预车辆沿用原仪表盘里程。'), ('P', '原TBOX里程(km)', '车辆 T-Box / 仪表盘读数', '当月仪表总里程(业务系统原始数据),含运营+测试+调试里程。仅干预车辆填充,非干预车辆为空。'), ('Q', '测试里程(km)', 'P − O(原TBOX − GPS,差额)', '仪表盘跑了但 GPS 未记录到的部分,判定为厂内测试/调试/路试等非运营里程,不计入考核。仅干预车辆填充,非干预车辆为0。'), ] for ri, (col_letter, name, source, note) in enumerate(ref_data): ws1.cell(row=row, column=1, value=col_letter).font = BOLD_F ws1.cell(row=row, column=1).alignment = CC ws1.cell(row=row, column=2, value=name) ws1.cell(row=row, column=3, value=source) ws1.cell(row=row, column=4, value=note).alignment = LL dat_row(ws1, row, 4, alt=(ri%2==1)) ws1.cell(row=row, column=1).font = BOLD_F ws1.row_dimensions[row].height = 42 row += 1 # Column widths for ci, w in enumerate([12, 22, 13, 12, 13, 22, 13], 1): ws1.column_dimensions[get_column_letter(ci)].width = w # ─── Save ─── out = 'new-kpi-202605_1/租赁任务考核_2026年5月_干预情况说明_Final.xlsx' wb.save(out) print(f'✅ {out} ({len(wb.sheetnames)} sheets)')