#!/usr/bin/env python3 """Generate 租赁任务考核_2026年5月_干预情况说明.xlsx based on updated source data.""" import os, openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter from collections import defaultdict from copy import copy os.chdir('/Users/kkfluous/Downloads') SRC = '租赁任务考核_2026年5月.xlsx' BAK = '租赁任务考核_2026年5月_原版备份.xlsx' # ── Read source ── wb_s = openpyxl.load_workbook(SRC, data_only=True) ws_s = wb_s['业务考核视图'] src_headers = [c.value for c in next(ws_s.iter_rows(min_row=1, max_row=1))] src_data = list(ws_s.iter_rows(min_row=2, values_only=True)) wb_s.close() # ── Read backup ── wb_b = openpyxl.load_workbook(BAK, data_only=True) ws_b = wb_b['业务考核视图'] bak_data = list(ws_b.iter_rows(min_row=2, values_only=True)) wb_b.close() print(f"Source: {len(src_data)} records") print(f"Backup: {len(bak_data)} records") # ── Compare ── # Build plate -> row map for backup (keep all, including duplicates) def make_key(row): plate = str(row[0]).strip() if row[0] else '' sales = str(row[4]).strip() if len(row) > 4 and row[4] else '' target = str(row[9]).strip() if len(row) > 9 and row[9] else '' return (plate, sales, target) bak_map = {} for r in bak_data: k = make_key(r) bak_map[k] = r src_map = {} for r in src_data: k = make_key(r) src_map[k] = r # Find new/changed/removed new_records = [] changed_records = [] removed_records = [] for k, sr in src_map.items(): if k not in bak_map: new_records.append(sr) else: br = bak_map[k] old_pass = str(br[16]).strip() if len(br) > 16 else '' new_pass = str(sr[16]).strip() if len(sr) > 16 else '' old_km = float(br[14] or 0) if len(br) > 14 else 0 new_km = float(sr[14] or 0) if len(sr) > 14 else 0 old_rate = float(br[15] or 0) if len(br) > 15 else 0 new_rate = float(sr[15] or 0) if len(sr) > 15 else 0 if old_pass != new_pass or abs(old_km - new_km) > 0.01 or abs(old_rate - new_rate) > 0.01: changed_records.append((br, sr)) for k, br in bak_map.items(): if k not in src_map: removed_records.append(br) print(f"New: {len(new_records)}, Changed: {len(changed_records)}, Removed: {len(removed_records)}") # Count达标 changes old_pass_count = sum(1 for r in bak_data if str(r[16]).strip() == '达标' if len(r) > 16) new_pass_count = sum(1 for r in src_data if str(r[16]).strip() == '达标' if len(r) > 16) print(f"达标: {old_pass_count} -> {new_pass_count}") # Flip analysis (达标 → 未达标) flipped = [] for old_r, new_r in changed_records: old_p = str(old_r[16]).strip() if len(old_r) > 16 else '' new_p = str(new_r[16]).strip() if len(new_r) > 16 else '' if old_p == '达标' and new_p == '未达标': flipped.append((old_r, new_r)) # Reverse flip (未达标 → 达标) reversed_flip = [] for old_r, new_r in changed_records: old_p = str(old_r[16]).strip() if len(old_r) > 16 else '' new_p = str(new_r[16]).strip() if len(new_r) > 16 else '' if old_p == '未达标' and new_p == '达标': reversed_flip.append((old_r, new_r)) print(f"Flipped (达标→未达标): {len(flipped)}") print(f"Reversed (未达标→达标): {len(reversed_flip)}") # ── Generate Excel ── wb = openpyxl.Workbook() # Styles TITLE_FONT = Font(bold=True, size=14) SECTION_FONT = Font(bold=True, size=12, color='1F4E78') HEADER_FILL = PatternFill('solid', fgColor='4472C4') HEADER_FONT = Font(bold=True, color='FFFFFF', size=10) SUB_FILL = PatternFill('solid', fgColor='D9E1F2') ALT_FILL = PatternFill('solid', fgColor='F2F2F2') NOTE_FONT = Font(size=10) BOLD_FONT = Font(bold=True, size=10) THIN = Side('thin', color='B4B4B4') BORDER = Border(THIN, THIN, THIN, THIN) CC = Alignment(horizontal='center', vertical='center', wrap_text=True) LL = Alignment(horizontal='left', vertical='center', wrap_text=True) NUM_FMT = '#,##0.00' def style_header_row(ws, row, ncols, fill=HEADER_FILL, font=HEADER_FONT): for c in range(1, ncols + 1): cell = ws.cell(row=row, column=c) cell.fill = fill cell.font = font cell.alignment = CC cell.border = BORDER def style_data_row(ws, row, ncols, alt=False): for c in range(1, ncols + 1): cell = ws.cell(row=row, column=c) cell.border = BORDER cell.font = Font(size=10) cell.alignment = CC if alt: cell.fill = ALT_FILL # ── Sheet 1: 测试与考核情况说明 ── ws1 = wb.active ws1.title = '测试与考核情况说明' ws1.sheet_properties.tabColor = '4472C4' ws1.merge_cells('A1:G1') ws1.cell(row=1, column=1, value='2026年5月 业务考核 — 数据更新说明').font = TITLE_FONT ws1.cell(row=1, column=1).alignment = CC ws1.row_dimensions[1].height = 30 ws1.merge_cells('A2:G2') desc = ( '5月业务考核源数据已于近期更新。' f'共{len(src_data)}条考核记录,{new_pass_count}条达标(原{old_pass_count}条)。\n' f'本次数据更新:新增{len(new_records)}条,变更{len(changed_records)}条,删除{len(removed_records)}条。\n' f'其中达标→未达标翻转{len(flipped)}条,未达标→达标翻转{len(reversed_flip)}条。\n' '本文件记录更新前后的数据对比,供核查参考。' ) ws1.cell(row=2, column=1, value=desc).font = NOTE_FONT ws1.cell(row=2, column=1).alignment = Alignment(horizontal='left', vertical='top', wrap_text=True) ws1.row_dimensions[2].height = 80 r = 4 ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) ws1.cell(row=r, column=1, value='一、5月业务考核概况').font = SECTION_FONT ws1.cell(row=r, column=1).alignment = LL r += 1 summary_data = [ ('项目', '5月业务考核车辆', '变更记录', '占比/变化'), ('考核记录数(条)', len(src_data), len(changed_records), f'{len(changed_records)/len(src_data)*100:.1f}%' if len(src_data) > 0 else '0%'), ('达标记录数(条)', new_pass_count, new_pass_count - old_pass_count, f'{"+" if new_pass_count >= old_pass_count else ""}{new_pass_count - old_pass_count}'), ('未达标记录数(条)', len(src_data) - new_pass_count, (len(src_data) - new_pass_count) - (len(bak_data) - old_pass_count), ''), ('达标率', f'{new_pass_count/len(src_data)*100:.1f}%' if len(src_data) > 0 else '0%', '', ''), ] for i, row_data in enumerate(summary_data): for c, val in enumerate(row_data, 1): ws1.cell(row=r, column=c, value=val) if i == 0: style_header_row(ws1, r, 4) else: style_data_row(ws1, r, 4, alt=(i % 2 == 0)) ws1.cell(row=r, column=1).alignment = LL r += 1 r += 1 ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) ws1.cell(row=r, column=1, value='二、考核记录变更明细').font = SECTION_FONT ws1.cell(row=r, column=1).alignment = LL r += 1 if flipped: ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) ws1.cell(row=r, column=1, value=f'2.1 达→未达标翻转({len(flipped)}条)').font = Font(bold=True, color='C00000') r += 1 flip_headers = ['车牌号', '部门', '销售经理', '客户名称', '考核目标', '原实际里程', '新实际里程', '原是否达标', '新是否达标'] for c, h in enumerate(flip_headers, 1): ws1.cell(row=r, column=c, value=h) style_header_row(ws1, r, len(flip_headers)) r += 1 for i, (old_r, new_r) in enumerate(flipped): vals = [ str(old_r[0]) if old_r[0] else '', str(old_r[3]) if len(old_r) > 3 else '', str(old_r[4]) if len(old_r) > 4 else '', str(old_r[5]) if len(old_r) > 5 else '', str(old_r[9]) if len(old_r) > 9 else '', round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0, round(float(new_r[14] or 0), 2) if len(new_r) > 14 and new_r[14] else 0, str(old_r[16]) if len(old_r) > 16 else '', str(new_r[16]) if len(new_r) > 16 else '', ] for c, val in enumerate(vals, 1): ws1.cell(row=r, column=c, value=val) style_data_row(ws1, r, len(flip_headers), alt=(i % 2 == 1)) r += 1 r += 1 if reversed_flip: ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) ws1.cell(row=r, column=1, value=f'2.2 未达标→达标翻转({len(reversed_flip)}条)').font = Font(bold=True, color='008000') r += 1 for c, h in enumerate(flip_headers, 1): ws1.cell(row=r, column=c, value=h) style_header_row(ws1, r, len(flip_headers)) r += 1 for i, (old_r, new_r) in enumerate(reversed_flip): vals = [ str(old_r[0]) if old_r[0] else '', str(old_r[3]) if len(old_r) > 3 else '', str(old_r[4]) if len(old_r) > 4 else '', str(old_r[5]) if len(old_r) > 5 else '', str(old_r[9]) if len(old_r) > 9 else '', round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0, round(float(new_r[14] or 0), 2) if len(new_r) > 14 and new_r[14] else 0, str(old_r[16]) if len(old_r) > 16 else '', str(new_r[16]) if len(new_r) > 16 else '', ] for c, val in enumerate(vals, 1): ws1.cell(row=r, column=c, value=val) style_data_row(ws1, r, len(flip_headers), alt=(i % 2 == 1)) r += 1 r += 1 if other_changes := [(o, n) for o, n in changed_records if not (str(o[16]).strip() == '达标' and str(n[16]).strip() == '未达标') and not (str(o[16]).strip() == '未达标' and str(n[16]).strip() == '达标')]: ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=7) ws1.cell(row=r, column=1, value=f'2.3 其他变更({len(other_changes)}条,里程变化但达标状态不变)').font = BOLD_FONT r += 1 col_widths = [14, 12, 12, 30, 22, 16, 16, 12, 12] for i, w in enumerate(col_widths, 1): ws1.column_dimensions[get_column_letter(i)].width = w # ── Sheet 2: 业务考核视图 (current data) ── ws2 = wb.create_sheet('业务考核视图') ws2.sheet_properties.tabColor = '70AD47' for c, h in enumerate(src_headers, 1): ws2.cell(row=1, column=c, value=h) style_header_row(ws2, 1, len(src_headers)) for i, row in enumerate(src_data): for c, val in enumerate(row, 1): ws2.cell(row=i + 2, column=c, value=val) style_data_row(ws2, i + 2, len(src_headers), alt=(i % 2 == 1)) for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12], 1): ws2.column_dimensions[get_column_letter(i)].width = w # ── Sheet 3: 5月业务考核车辆总览 ── ws3 = wb.create_sheet('5月业务考核车辆总览') ws3.sheet_properties.tabColor = 'ED7D31' for c, h in enumerate(src_headers, 1): ws3.cell(row=1, column=c, value=h) style_header_row(ws3, 1, len(src_headers)) for i, row in enumerate(src_data): for c, val in enumerate(row, 1): ws3.cell(row=i + 2, column=c, value=val) style_data_row(ws3, i + 2, len(src_headers), alt=(i % 2 == 1)) for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12], 1): ws3.column_dimensions[get_column_letter(i)].width = w # ── Sheet 4: 涉及变更的业务考核车辆 ── ws4 = wb.create_sheet('涉及变更的业务考核车辆') ws4.sheet_properties.tabColor = 'FFC000' ws4.merge_cells('A1:U1') ws4.cell(row=1, column=1, value=f'变更记录汇总 — 新增{len(new_records)}条 / 变更{len(changed_records)}条 / 删除{len(removed_records)}条').font = Font(bold=True, size=11) ws4.cell(row=1, column=1).alignment = CC ws4.row_dimensions[1].height = 24 # Source headers + change type col ext_headers = list(src_headers) + ['变更类型', '原实际里程', '原是否达标'] for c, h in enumerate(ext_headers, 1): ws4.cell(row=3, column=c, value=h) style_header_row(ws4, 3, len(ext_headers)) r = 4 # New records for i, rec in enumerate(new_records): for c, val in enumerate(rec, 1): ws4.cell(row=r, column=c, value=val) ws4.cell(row=r, column=len(src_headers) + 1, value='新增') style_data_row(ws4, r, len(ext_headers), alt=(i % 2 == 1)) ws4.row_dimensions[r].height = 20 r += 1 # Changed records for i, (old_r, new_r) in enumerate(changed_records): for c, val in enumerate(new_r, 1): ws4.cell(row=r, column=c, value=val) ws4.cell(row=r, column=len(src_headers) + 1, value='变更') old_km = round(float(old_r[14] or 0), 2) if len(old_r) > 14 and old_r[14] else 0 old_pass = str(old_r[16]) if len(old_r) > 16 else '' ws4.cell(row=r, column=len(src_headers) + 2, value=old_km) ws4.cell(row=r, column=len(src_headers) + 3, value=old_pass) style_data_row(ws4, r, len(ext_headers), alt=(i % 2 == 1)) ws4.row_dimensions[r].height = 20 r += 1 for i, w in enumerate([12, 8, 8, 12, 10, 28, 20, 24, 10, 22, 12, 12, 10, 16, 16, 10, 10, 10, 12, 12, 12, 10, 14, 12], 1): ws4.column_dimensions[get_column_letter(i)].width = w # ── Save ── out_path = 'new-kpi-202605_1/租赁任务考核_2026年5月_干预情况说明.xlsx' wb.save(out_path) print(f'\n✅ {out_path} ({len(wb.sheetnames)} sheets)')