794 lines
32 KiB
Python
794 lines
32 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
全流程:用GPS汇总文件替换测试车辆里程 → 重新核算6月 → 离职转嫁 → 输出
|
|
输出目录: 20260713_1500/
|
|
"""
|
|
import openpyxl, os, shutil, xlrd
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
from openpyxl.utils import get_column_letter
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta
|
|
from calc_engine import *
|
|
import excel_writer as ew
|
|
|
|
os.chdir('/Users/kkfluous/Downloads')
|
|
GPS_BASE = 'gps里程汇总2026年1-6月 3'
|
|
OUT_DIR = '20260713_1500'
|
|
os.makedirs(OUT_DIR, exist_ok=True)
|
|
|
|
# ============================================================
|
|
# Step 1: Build GPS daily mileage lookup from all 4 providers
|
|
# ============================================================
|
|
print("=" * 60)
|
|
print("Step 1: 构建GPS日里程映射")
|
|
print("=" * 60)
|
|
|
|
# {plate: {day_number (1-30): mileage_km}}
|
|
gps_daily = defaultdict(lambda: defaultdict(float))
|
|
|
|
# --- G7 ---
|
|
print("\n读取 G7...")
|
|
fp_g7 = os.path.join(GPS_BASE, 'G7/202606_G7.xlsx')
|
|
wb = openpyxl.load_workbook(fp_g7, data_only=True)
|
|
ws = wb.active
|
|
h_g7 = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
|
# Find daily columns: 06月01日 ... 06月30日
|
|
g7_day_cols = {}
|
|
for i, h in enumerate(h_g7):
|
|
if h and '月' in str(h) and '日' in str(h):
|
|
# Parse "06月15日" -> day=15
|
|
import re
|
|
m = re.search(r'(\d+)月(\d+)日', str(h))
|
|
if m:
|
|
day = int(m.group(2))
|
|
g7_day_cols[day] = i
|
|
|
|
print(f" G7日期列: {len(g7_day_cols)} 天, 共 {ws.max_row-1} 辆车")
|
|
g7_count = 0
|
|
for row in ws.iter_rows(min_row=2, values_only=True):
|
|
plate = str(row[0]).strip() if row[0] else ''
|
|
if not plate: continue
|
|
for day, col in g7_day_cols.items():
|
|
val = float(row[col] or 0)
|
|
if val > 0:
|
|
gps_daily[plate][day] += val
|
|
g7_count += 1
|
|
wb.close()
|
|
print(f" G7: {g7_count} 辆车")
|
|
|
|
# --- 东方北斗 ---
|
|
print("\n读取 东方北斗...")
|
|
fp_df = os.path.join(GPS_BASE, '东方北斗/202606_东方北斗.xlsx')
|
|
wb = openpyxl.load_workbook(fp_df, data_only=True)
|
|
ws = wb.active
|
|
h_df = [c.value for c in next(ws.iter_rows(min_row=3, max_row=3))]
|
|
# Daily columns: 1号(Col 11) ... 30号(Col 40)
|
|
df_day_cols = {}
|
|
for i, h in enumerate(h_df):
|
|
if h and '号' in str(h) and str(h)[0].isdigit():
|
|
try:
|
|
day = int(str(h).replace('号', ''))
|
|
df_day_cols[day] = i
|
|
except: pass
|
|
|
|
print(f" 东方北斗日期列: {len(df_day_cols)} 天")
|
|
df_count = 0
|
|
for row in ws.iter_rows(min_row=4, values_only=True):
|
|
plate = str(row[5]).strip() if row[5] else ''
|
|
if not plate: continue
|
|
for day, col in df_day_cols.items():
|
|
val = float(row[col] or 0)
|
|
if val > 0:
|
|
gps_daily[plate][day] += val
|
|
df_count += 1
|
|
wb.close()
|
|
print(f" 东方北斗: {df_count} 辆车")
|
|
|
|
# --- 信达GPS (daily detail format) ---
|
|
print("\n读取 信达GPS...")
|
|
fp_xd = os.path.join(GPS_BASE, '信达GPS/202606_信达.xlsx')
|
|
wb = openpyxl.load_workbook(fp_xd, data_only=True)
|
|
ws = wb.active
|
|
xd_count = 0
|
|
for row in ws.iter_rows(min_row=4, values_only=True):
|
|
plate = str(row[7]).strip() if row[7] else ''
|
|
date_str = str(row[14]).strip() if row[14] else ''
|
|
mileage = float(row[15] or 0)
|
|
if not plate or not date_str or mileage <= 0: continue
|
|
try:
|
|
# Parse date: "2026-06-01"
|
|
dt = datetime.strptime(date_str[:10], '%Y-%m-%d')
|
|
day = dt.day
|
|
gps_daily[plate][day] += mileage
|
|
xd_count += 1
|
|
except: pass
|
|
wb.close()
|
|
print(f" 信达GPS: {xd_count} 条日记录")
|
|
|
|
# --- 广安北斗 (monthly only - xls) ---
|
|
print("\n读取 广安北斗...")
|
|
fp_ga = os.path.join(GPS_BASE, '广安北斗/202606_广安北斗.xls')
|
|
gdwb = xlrd.open_workbook(fp_ga)
|
|
gdws = gdwb.sheet_by_index(0)
|
|
ga_count = 0
|
|
for r in range(7, gdws.nrows):
|
|
plate = str(gdws.cell_value(r, 3)).strip()
|
|
mileage = float(gdws.cell_value(r, 11) or 0)
|
|
if not plate or mileage <= 0: continue
|
|
# Spread across all 30 days proportionally? No - store as a special total
|
|
# Better: store at day 0 as "full month total"
|
|
if 0 not in gps_daily[plate] or gps_daily[plate][0] < mileage:
|
|
gps_daily[plate][0] = mileage # day 0 = full month total marker
|
|
ga_count += 1
|
|
print(f" 广安北斗: {ga_count} 辆车 (月度汇总)")
|
|
|
|
total_plates = len(gps_daily)
|
|
print(f"\nGPS映射总计: {total_plates} 辆车")
|
|
|
|
# ============================================================
|
|
# Step 2: Read test vehicle plates
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 2: 提取测试车辆清单")
|
|
print("=" * 60)
|
|
|
|
# Extract from干预情况说明
|
|
fp_interv = '租赁任务考核_2026年6月_干预情况说明.xlsx'
|
|
if os.path.exists(fp_interv):
|
|
wb = openpyxl.load_workbook(fp_interv, data_only=True)
|
|
ws = wb['涉及测试的业务考核车辆']
|
|
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
|
plate_col = h.index('车牌号')
|
|
test_plates = set()
|
|
for row in ws.iter_rows(min_row=2, values_only=True):
|
|
p = row[plate_col]
|
|
if p: test_plates.add(str(p).strip())
|
|
wb.close()
|
|
else:
|
|
# Fallback: vehicles with test mileage > 0 in current source
|
|
wb = openpyxl.load_workbook('租赁任务考核_2026年6月.xlsx', data_only=True)
|
|
ws = wb['业务考核视图']
|
|
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
|
pcol = h.index('车牌号')
|
|
tcol = h.index('测试里程(km)')
|
|
test_plates = set()
|
|
for row in ws.iter_rows(min_row=2, values_only=True):
|
|
if float(row[tcol] or 0) > 0:
|
|
test_plates.add(str(row[pcol]).strip())
|
|
wb.close()
|
|
|
|
print(f"测试车辆: {len(test_plates)} 辆")
|
|
|
|
# Check GPS coverage
|
|
gps_hit = sum(1 for p in test_plates if p in gps_daily)
|
|
gps_miss = test_plates - set(gps_daily.keys())
|
|
print(f"GPS命中: {gps_hit}, GPS缺失: {len(gps_miss)}")
|
|
if gps_miss:
|
|
print(f"缺失车辆(前10): {sorted(gps_miss)[:10]}")
|
|
|
|
# ============================================================
|
|
# Step 3: Replace GPS mileage for test vehicles
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 3: 替换测试车辆实际行驶里程")
|
|
print("=" * 60)
|
|
|
|
# Start from current source file (has correct structure with TBOX/GPS/测试 columns)
|
|
src_fp = '租赁任务考核_2026年6月.xlsx'
|
|
wb_src = openpyxl.load_workbook(src_fp)
|
|
ws_src = wb_src['业务考核视图']
|
|
h_src = [c.value for c in next(ws_src.iter_rows(min_row=1, max_row=1))]
|
|
|
|
# Column indices
|
|
col_plate = h_src.index('车牌号')
|
|
col_tbox = h_src.index('原TBOX里程(km)')
|
|
col_actual = h_src.index('实际行驶里程(km)')
|
|
col_test = h_src.index('测试里程(km)')
|
|
col_rate = h_src.index('完成率(%)')
|
|
col_dabiao = h_src.index('是否达标')
|
|
col_start = h_src.index('考核开始日期')
|
|
col_end = h_src.index('考核结束日期')
|
|
col_target_km = h_src.index('应考核里程(km)')
|
|
|
|
YELLOW_FILL = PatternFill(start_color='FFFF00', end_color='FFFF00', fill_type='solid')
|
|
|
|
updated_count = 0
|
|
no_gps_count = 0
|
|
records = []
|
|
|
|
for row_idx, row in enumerate(ws_src.iter_rows(min_row=2), start=2):
|
|
plate = str(row[col_plate].value or '').strip()
|
|
tbox_val = float(row[col_tbox].value or 0)
|
|
start_date = row[col_start].value
|
|
end_date = row[col_end].value
|
|
target_km = float(row[col_target_km].value or 0)
|
|
|
|
if plate not in test_plates:
|
|
records.append((row_idx, plate, False, None, None))
|
|
continue
|
|
|
|
# Calculate GPS mileage within考核日期区间
|
|
gps_sum = 0.0
|
|
gps_data = gps_daily.get(plate, {})
|
|
|
|
if 0 in gps_data and len(gps_data) == 1:
|
|
# 广安北斗: only has full month total
|
|
gps_sum = gps_data[0]
|
|
elif start_date and end_date:
|
|
# Parse dates and sum daily GPS
|
|
if isinstance(start_date, datetime) and isinstance(end_date, datetime):
|
|
start_day = start_date.day
|
|
end_day = end_date.day
|
|
for day in range(start_day, end_day + 1):
|
|
gps_sum += gps_data.get(day, 0)
|
|
elif isinstance(start_date, str):
|
|
try:
|
|
sd = datetime.strptime(str(start_date)[:10], '%Y-%m-%d')
|
|
ed = datetime.strptime(str(end_date)[:10], '%Y-%m-%d')
|
|
for day in range(sd.day, ed.day + 1):
|
|
gps_sum += gps_data.get(day, 0)
|
|
except: pass
|
|
else:
|
|
# No dates, use full month
|
|
gps_sum = sum(gps_data.values())
|
|
else:
|
|
gps_sum = sum(gps_data.values())
|
|
|
|
if gps_sum <= 0 and tbox_val <= 0:
|
|
no_gps_count += 1
|
|
records.append((row_idx, plate, True, tbox_val, tbox_val))
|
|
continue
|
|
|
|
# Test mileage = max(0, TBOX - GPS)
|
|
test_mileage = max(0, tbox_val - gps_sum)
|
|
|
|
# Completion rate
|
|
rate = (gps_sum / target_km * 100) if target_km > 0 else 0
|
|
|
|
# 达标判断
|
|
dabiao = '达标' if gps_sum >= target_km and target_km > 0 else '未达标'
|
|
|
|
# Update cells
|
|
row[col_actual].value = round(gps_sum, 2)
|
|
row[col_test].value = round(test_mileage, 2)
|
|
row[col_rate].value = round(rate, 2)
|
|
row[col_dabiao].value = dabiao
|
|
|
|
# Yellow highlight
|
|
for cell in row:
|
|
cell.fill = YELLOW_FILL
|
|
|
|
updated_count += 1
|
|
records.append((row_idx, plate, True, gps_sum, test_mileage))
|
|
|
|
print(f"测试车辆记录: {updated_count} 条已更新, {no_gps_count} 条无GPS数据")
|
|
|
|
# Save updated source file
|
|
new_src = os.path.join(OUT_DIR, '租赁任务考核_2026年6月.xlsx')
|
|
wb_src.save(new_src)
|
|
print(f"✅ 新数据源: {new_src}")
|
|
|
|
# ============================================================
|
|
# Step 4: Run 核算
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 4: 运行核算")
|
|
print("=" * 60)
|
|
|
|
# Temporarily copy source to working location
|
|
shutil.copy(new_src, '租赁任务考核_2026年6月.xlsx')
|
|
|
|
# Re-run main.py logic
|
|
print("读取源数据...")
|
|
D = {}
|
|
for m in range(1, 7):
|
|
fp = f'租赁任务考核_2026年{m}月.xlsx'
|
|
if os.path.exists(fp):
|
|
D[m] = read_file(fp, m)
|
|
print(f" {m}月: {len(D[m])}条")
|
|
|
|
G = {m: grp(D[m], m) for m in D}
|
|
calc_jan_carryover(G[1])
|
|
all_data = {}
|
|
all_data[1] = {'当月': [{'车牌':k[0],'销售':g['销售'],'部门':g['部门'],'额':g['奖金']}
|
|
for k,g in G[1].items() if g['奖金'] > 0]}
|
|
|
|
for m in range(2, 7):
|
|
if m not in G: continue
|
|
data_m, G[m] = calc_month(m, G, all_data, G[m-1], G[m])
|
|
|
|
vehicle_payments, vehicle_info = collect_vehicle_payments(G, all_data)
|
|
master_vehicles = read_master_vehicles()
|
|
|
|
# Loss data
|
|
loss_data = {}
|
|
for m in D:
|
|
ld = read_loss_data(m)
|
|
if ld:
|
|
loss_data[m] = ld
|
|
else:
|
|
from collections import defaultdict as dd
|
|
ld_tmp = dd(lambda: '否')
|
|
for r in D[m]:
|
|
c = r.get('客户名称')
|
|
if c: ld_tmp[c] = '否'
|
|
loss_data[m] = ld_tmp
|
|
|
|
plate_client = get_vehicle_client_map(D)
|
|
|
|
all_persons = {}
|
|
for m in G:
|
|
for k, g in G[m].items():
|
|
all_persons[g['销售']] = g['部门']
|
|
|
|
# Print summary
|
|
settle_month = 6
|
|
month_data = all_data[settle_month]
|
|
total_6 = sum(sum(d['额'] for d in v) for v in month_data.values())
|
|
print(f"\n6月考核应发: {total_6:.2f}")
|
|
|
|
# Generate核算 file
|
|
print(f"\n生成 {settle_month}月核算文件...")
|
|
wb = openpyxl.Workbook()
|
|
ew.write_rules_sheet(wb)
|
|
ew.write_detail_sheet(wb, D[settle_month], settle_month)
|
|
ew.write_calc_process_generic(wb, settle_month, G, all_data)
|
|
ew.write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client)
|
|
|
|
class _AllPass:
|
|
def get(self, _k, _d=None): return '否'
|
|
def __bool__(self): return True
|
|
payment_records = ew.build_payment_records(settle_month, month_data, {m: _AllPass() for m in D}, plate_client)
|
|
ew.write_payment_record_sheet(wb, settle_month, payment_records)
|
|
ew.write_summary_from_records(wb, settle_month, payment_records)
|
|
|
|
for person in sorted(all_persons.keys()):
|
|
ew.write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments)
|
|
|
|
if 'Sheet' in wb.sheetnames:
|
|
del wb['Sheet']
|
|
|
|
calc_fp = os.path.join(OUT_DIR, '里程任务考核_6月核算.xlsx')
|
|
wb.save(calc_fp)
|
|
print(f"✅ 核算文件: {calc_fp}")
|
|
|
|
# ============================================================
|
|
# Step 5: 离职转嫁 + 格式重建
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 5: 离职转嫁 + 汇总格式重建")
|
|
print("=" * 60)
|
|
|
|
RESIGNED = {'赵连飞', '伍仲文', '岑彦'}
|
|
TRANSFERS = [
|
|
('刘念念', '赵连飞', 2260.00),
|
|
('钟祥', '伍仲文、岑彦', 180.00),
|
|
]
|
|
TRANSFER_TOTAL = sum(t[2] for t in TRANSFERS)
|
|
TRANSFER_MAP = {'赵连飞': ('刘念念', 2260.00), '伍仲文': ('钟祥', 150.00), '岑彦': ('钟祥', 30.00)}
|
|
|
|
# Styles
|
|
HEAD_FILL = PatternFill('solid', fgColor='4472C4')
|
|
HEAD_FONT = Font(bold=True, color='FFFFFF', size=10, name='宋体')
|
|
SUB_FILL = PatternFill('solid', fgColor='D9E1F2')
|
|
TITLE_FNT = Font(bold=True, size=12, name='宋体')
|
|
SECTION_FNT = Font(bold=True, size=11, name='宋体')
|
|
NOTE_FNT = Font(size=10, color='C00000', name='宋体')
|
|
NORM_FNT = Font(size=10, name='宋体')
|
|
BOLD_FNT = Font(bold=True, size=10, name='宋体')
|
|
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)
|
|
|
|
def hdr_row(ws, r, headers):
|
|
for c, h in enumerate(headers, 1):
|
|
cell = ws.cell(row=r, column=c, value=h)
|
|
cell.font = HEAD_FONT; cell.fill = HEAD_FILL; cell.alignment = CC; cell.border = BORD
|
|
|
|
def data_row(ws, r, values, bold=False):
|
|
for c, v in enumerate(values, 1):
|
|
cell = ws.cell(row=r, column=c, value=v)
|
|
cell.font = BOLD_FNT if bold else NORM_FNT; cell.alignment = CC; cell.border = BORD
|
|
if isinstance(v, float): cell.number_format = '#,##0.00'
|
|
|
|
def sub_total(ws, r, values):
|
|
for c, v in enumerate(values, 1):
|
|
cell = ws.cell(row=r, column=c, value=v)
|
|
cell.font = BOLD_FNT; cell.fill = SUB_FILL; cell.alignment = CC; cell.border = BORD
|
|
if isinstance(v, float): cell.number_format = '#,##0.00'
|
|
|
|
# Parse existing summary
|
|
ws_old = wb['6月汇总']
|
|
person_dept = {}
|
|
for r in range(1, ws_old.max_row + 1):
|
|
v1 = str(ws_old.cell(row=r, column=1).value or '')
|
|
if '6月最终发放(按销售人员)' in v1:
|
|
for r2 in range(r+1, ws_old.max_row + 1):
|
|
name = str(ws_old.cell(row=r2, column=1).value or '')
|
|
dept = str(ws_old.cell(row=r2, column=2).value or '')
|
|
if name == '合计': break
|
|
if name and name != '销售人员' and dept:
|
|
person_dept[name] = dept
|
|
break
|
|
|
|
# Parse考核应发 subsections
|
|
subsections = []
|
|
current_title = None; current_rows = []
|
|
for r in range(1, ws_old.max_row + 1):
|
|
v1 = str(ws_old.cell(row=r, column=1).value or '')
|
|
if v1.startswith('考核应发-'):
|
|
if current_title and current_rows:
|
|
subsections.append((current_title, current_rows))
|
|
current_title = v1; current_rows = []
|
|
continue
|
|
if current_title:
|
|
name = str(ws_old.cell(row=r, column=1).value or '')
|
|
vehicles = ws_old.cell(row=r, column=2).value
|
|
amount = ws_old.cell(row=r, column=3).value
|
|
if name == '总计':
|
|
subsections.append((current_title, current_rows))
|
|
current_title = None; current_rows = []
|
|
elif name and name != '销售人员':
|
|
current_rows.append((name, vehicles, amount))
|
|
|
|
# Filter out resigned
|
|
filtered_subsections = []
|
|
grand_total = 0.0
|
|
for title, rows in subsections:
|
|
filtered = [(n, v, a) for (n, v, a) in rows if n not in RESIGNED]
|
|
new_veh = sum(int(v or 0) for _, v, _ in filtered)
|
|
new_amt = sum(float(a or 0) for _, _, a in filtered)
|
|
grand_total += new_amt
|
|
filtered_subsections.append((title, filtered, new_veh, new_amt))
|
|
|
|
# Aggregate最终发放
|
|
person_data = defaultdict(lambda: {'vehicles': 0, 'amount': 0.0})
|
|
for title, rows, _, _ in filtered_subsections:
|
|
for name, vehicles, amount in rows:
|
|
person_data[name]['vehicles'] += int(vehicles or 0)
|
|
person_data[name]['amount'] += float(amount or 0)
|
|
|
|
for resigned, (receiver, amt) in TRANSFER_MAP.items():
|
|
person_data[receiver]['amount'] += amt
|
|
|
|
for name in RESIGNED:
|
|
person_data.pop(name, None)
|
|
|
|
final_total = sum(d['amount'] for d in person_data.values())
|
|
|
|
dept_order = ['业务二部', '业务三部', '业务五部', '业务六部']
|
|
def sort_key(item):
|
|
name = item[0]
|
|
dept = person_dept.get(name, '')
|
|
try: di = dept_order.index(dept)
|
|
except: di = 99
|
|
return (di, name)
|
|
|
|
sorted_persons = sorted(person_data.items(), key=sort_key)
|
|
|
|
dept_totals = defaultdict(float)
|
|
for name, data in sorted_persons:
|
|
dept = person_dept.get(name, '')
|
|
dept_totals[dept] += data['amount']
|
|
|
|
# Rebuild 6月汇总 sheet
|
|
ws = wb['6月汇总']
|
|
ws.delete_rows(1, ws.max_row)
|
|
rn = 1
|
|
|
|
# 一、考核应发明细
|
|
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
|
|
ws.cell(row=rn, column=1, value='一、考核应发明细').font = TITLE_FNT
|
|
rn += 2
|
|
|
|
for title, rows, total_veh, total_amt in filtered_subsections:
|
|
ws.cell(row=rn, column=1, value=title).font = SECTION_FNT
|
|
rn += 1
|
|
hdr_row(ws, rn, ['销售人员', '车辆数', '金额'])
|
|
rn += 1
|
|
sorted_rows = sorted(rows, key=lambda x: sort_key((x[0],)) if x[0] in person_dept else (99, x[0]))
|
|
for name, vehicles, amount in sorted_rows:
|
|
data_row(ws, rn, [name, vehicles, round(float(amount or 0), 2)])
|
|
rn += 1
|
|
sub_total(ws, rn, ['总计', total_veh, round(total_amt, 2)])
|
|
rn += 2
|
|
|
|
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=2)
|
|
ws.cell(row=rn, column=1, value='考核应发合计').font = BOLD_FNT
|
|
ws.cell(row=rn, column=1).alignment = CC
|
|
ws.cell(row=rn, column=3, value=round(grand_total, 2)).font = BOLD_FNT
|
|
ws.cell(row=rn, column=3).alignment = CC; ws.cell(row=rn, column=3).number_format = '#,##0.00'
|
|
for c in range(1, 4):
|
|
ws.cell(row=rn, column=c).border = BORD; ws.cell(row=rn, column=c).fill = SUB_FILL
|
|
rn += 2
|
|
|
|
# 二、离职人员转嫁收入
|
|
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
|
|
ws.cell(row=rn, column=1, value='二、离职人员转嫁收入').font = TITLE_FNT
|
|
rn += 1
|
|
hdr_row(ws, rn, ['接收人', '来源', '转嫁金额'])
|
|
rn += 1
|
|
for receiver, source, amt in TRANSFERS:
|
|
data_row(ws, rn, [receiver, source, amt]); rn += 1
|
|
sub_total(ws, rn, ['合计', '', TRANSFER_TOTAL]); rn += 2
|
|
|
|
# 三、最终发放
|
|
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
|
|
ws.cell(row=rn, column=1, value='三、最终发放').font = TITLE_FNT
|
|
rn += 2
|
|
|
|
ws.cell(row=rn, column=1, value='最终发放明细').font = SECTION_FNT; rn += 1
|
|
hdr_row(ws, rn, ['销售人员', '车辆数', '金额']); rn += 1
|
|
for name, data in sorted_persons:
|
|
data_row(ws, rn, [name, data['vehicles'], round(data['amount'], 2)]); rn += 1
|
|
sub_total(ws, rn, ['总计', sum(d['vehicles'] for _, d in sorted_persons), round(final_total, 2)]); rn += 2
|
|
|
|
ws.cell(row=rn, column=1, value='6月最终发放(按销售人员)').font = SECTION_FNT; rn += 1
|
|
hdr_row(ws, rn, ['销售人员', '部门名称', '最终发放']); rn += 1
|
|
for name, data in sorted_persons:
|
|
dept = person_dept.get(name, '')
|
|
data_row(ws, rn, [name, dept, round(data['amount'], 2)]); rn += 1
|
|
sub_total(ws, rn, ['合计', '', round(final_total, 2)]); rn += 2
|
|
|
|
ws.cell(row=rn, column=1, value='6月最终发放(按部门)').font = SECTION_FNT; rn += 1
|
|
hdr_row(ws, rn, ['部门名称', '最终发放', '']); rn += 1
|
|
for dept in dept_order:
|
|
if dept in dept_totals:
|
|
data_row(ws, rn, [dept, round(dept_totals[dept], 2), '']); rn += 1
|
|
sub_total(ws, rn, ['合计', round(sum(dept_totals.values()), 2), '']); rn += 2
|
|
|
|
# 总览
|
|
ws.cell(row=rn, column=1, value='总览').font = SECTION_FNT; rn += 1
|
|
data_row(ws, rn, ['考核应发', round(final_total, 2), '']); rn += 1
|
|
data_row(ws, rn, ['亏损拦截', 0, '']); rn += 1
|
|
data_row(ws, rn, ['最终发放', round(final_total, 2), '']); rn += 2
|
|
|
|
# 备注
|
|
note = '备注:赵连飞(2026-04-30离职)、伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)\n三人已从上述汇总中剔除,其6月当月达标奖金已转嫁至新销售;其余结转、补发、累计补发等一律不发了。'
|
|
ws.merge_cells(start_row=rn, start_column=1, end_row=rn, end_column=3)
|
|
ws.cell(row=rn, column=1, value=note).font = NOTE_FNT
|
|
ws.cell(row=rn, column=1).alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
|
|
ws.row_dimensions[rn].height = 36
|
|
|
|
# Column widths
|
|
ws.column_dimensions['A'].width = 16
|
|
ws.column_dimensions['B'].width = 18
|
|
ws.column_dimensions['C'].width = 16
|
|
|
|
# Rename resigned sheets
|
|
for old, new in [('二部-赵连飞','【已离职】二部-赵连飞'),('六部-伍仲文','【已离职】六部-伍仲文'),('六部-岑彦','【已离职】六部-岑彦')]:
|
|
if old in wb.sheetnames:
|
|
wb[old].title = new
|
|
wb.move_sheet(new, offset=len(wb.sheetnames)-1-wb.sheetnames.index(new))
|
|
|
|
wb.save(calc_fp)
|
|
print(f"✅ 核算文件(含转嫁): {calc_fp}")
|
|
|
|
# ============================================================
|
|
# Step 6: 干预情况说明 (copy from existing)
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 6: 干预情况说明")
|
|
print("=" * 60)
|
|
|
|
old_interv = '租赁任务考核_2026年6月_干预情况说明.xlsx'
|
|
if os.path.exists(old_interv):
|
|
shutil.copy(old_interv, os.path.join(OUT_DIR, '租赁任务考核_2026年6月_干预情况说明.xlsx'))
|
|
print(f"✅ 干预说明已复制")
|
|
|
|
# ============================================================
|
|
# Step 7: 离职明细 (regenerate with new amounts)
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 7: 离职人员处理明细")
|
|
print("=" * 60)
|
|
|
|
# Collect resigned data from all_data[6]
|
|
detail_records = []
|
|
for ptype, items in month_data.items():
|
|
for it in items:
|
|
if it['销售'] not in RESIGNED: continue
|
|
name = it['销售']
|
|
amount = it['额']
|
|
is_dangyue = (ptype == '当月')
|
|
transfer_to = TRANSFER_MAP[name][0]
|
|
handling = f'转{transfer_to}(需确认)' if is_dangyue else '不发了'
|
|
transfer_disp = transfer_to if is_dangyue else '—'
|
|
note_txt = '6月当月达标,转新销售' if is_dangyue else f'{ptype},不发了'
|
|
|
|
client = ''; target_name = ''
|
|
for r in D.get(6, []):
|
|
if r['车牌号'] == it['车牌'] and r['销售经理'] == name:
|
|
client = r.get('客户名称', ''); target_name = r.get('考核目标', '')
|
|
break
|
|
|
|
g6 = G[6].get((it['车牌'], name))
|
|
monthly_bonus = g6['奖励达标'] if g6 else 0
|
|
days = g6['天数'] if g6 else ''
|
|
|
|
if ptype in ('当月', f'累计补发{settle_month}月', '结转'):
|
|
loss_month = settle_month
|
|
elif ptype.startswith('补发'):
|
|
try: loss_month = int(ptype.replace('补发','').replace('月',''))
|
|
except: loss_month = settle_month
|
|
else: loss_month = settle_month
|
|
|
|
detail_records.append({
|
|
'name': name, 'plate': it['车牌'], 'pay_type': ptype,
|
|
'amount': amount, 'handling': handling, 'transfer': transfer_disp,
|
|
'note': note_txt, 'client': client, 'loss_month': loss_month,
|
|
'loss_status': '否', 'monthly_bonus': monthly_bonus,
|
|
'days': days, 'target': target_name,
|
|
})
|
|
|
|
print(f"离职人员发放记录: {len(detail_records)} 条")
|
|
|
|
# Generate resignation workbook (simplified)
|
|
wb_res = openpyxl.Workbook()
|
|
|
|
# Sheet 1: 总览
|
|
ws1 = wb_res.active; ws1.title = '离职人员发放处理总览'
|
|
ws1.merge_cells('A1:G1')
|
|
ws1.cell(row=1, column=1, value='2026年6月核算 — 离职人员奖金处理方案').font = Font(bold=True, size=14, name='宋体')
|
|
ws1.cell(row=1, column=1).alignment = CC
|
|
ws1.row_dimensions[1].height = 30
|
|
|
|
ws1.merge_cells('A2:G2')
|
|
ws1.cell(row=2, column=1, value='处理规则:仅6月当月达标奖金转给新接手的销售(需与其他部门确认是否发放);其余结转、补发、累计补发等一律不发了。').font = NOTE_FNT
|
|
ws1.cell(row=2, column=1).alignment = LL
|
|
|
|
r = 4
|
|
for c, h in enumerate(['离职人员', '部门', '业务转嫁至', '不发了', '转新销售(需确认)', '原考核应发合计'], 1):
|
|
ws1.cell(row=r, column=c, value=h)
|
|
ws1.cell(row=r, column=c).font = HEAD_FONT; ws1.cell(row=r, column=c).fill = HEAD_FILL
|
|
ws1.cell(row=r, column=c).alignment = CC; ws1.cell(row=r, column=c).border = BORD
|
|
r += 1
|
|
|
|
dept_map = {'赵连飞': '业务二部', '伍仲文': '业务六部', '岑彦': '业务六部'}
|
|
total_not = 0; total_trf = 0
|
|
for name in ['赵连飞', '伍仲文', '岑彦']:
|
|
recs = [p for p in detail_records if p['name'] == name]
|
|
not_pay = sum(p['amount'] for p in recs if p['pay_type'] != '当月')
|
|
transfer = sum(p['amount'] for p in recs if p['pay_type'] == '当月')
|
|
total_not += not_pay; total_trf += transfer
|
|
data_row(ws1, r, [name, dept_map[name], TRANSFER_MAP[name][0], round(not_pay,2), round(transfer,2), round(not_pay+transfer,2)])
|
|
r += 1
|
|
|
|
sub_total(ws1, r, ['合计', '', '', round(total_not,2), round(total_trf,2), round(total_not+total_trf,2)])
|
|
r += 2
|
|
|
|
ws1.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
|
|
ws1.cell(row=r, column=1, value='转嫁接收方汇总(需与其他部门确认是否发放)').font = Font(bold=True, size=12, color='1F4E78')
|
|
ws1.cell(row=r, column=1).alignment = CC; r += 1
|
|
|
|
for c, h in enumerate(['接收人', '来源离职人员', '转嫁金额', '备注'], 1):
|
|
ws1.cell(row=r, column=c, value=h)
|
|
ws1.cell(row=r, column=c).font = HEAD_FONT; ws1.cell(row=r, column=c).fill = HEAD_FILL
|
|
ws1.cell(row=r, column=c).alignment = CC; ws1.cell(row=r, column=c).border = BORD
|
|
r += 1
|
|
|
|
by_recv = defaultdict(lambda: {'sources':[], 'amt':0})
|
|
for rec in detail_records:
|
|
if rec['pay_type'] == '当月':
|
|
t = rec['transfer']
|
|
by_recv[t]['sources'].append(rec['name'])
|
|
by_recv[t]['amt'] += rec['amount']
|
|
|
|
for receiver in ['刘念念', '钟祥']:
|
|
d = by_recv[receiver]
|
|
data_row(ws1, r, [receiver, '、'.join(sorted(set(d['sources']))), round(d['amt'],2), '需与相关部门确认是否发放给新销售'])
|
|
r += 1
|
|
|
|
for i, w in enumerate([14, 22, 18, 18, 18, 18], 1):
|
|
ws1.column_dimensions[get_column_letter(i)].width = w
|
|
|
|
# Sheet 2: 逐条明细
|
|
ws2 = wb_res.create_sheet('发放明细(逐条)')
|
|
ws2.merge_cells('A1:M1')
|
|
ws2.cell(row=1, column=1, value='离职人员 — 6月考核应发逐条明细').font = Font(bold=True, size=14, name='宋体')
|
|
ws2.cell(row=1, column=1).alignment = CC
|
|
|
|
dh = ['离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '处理方式', '转嫁接收人', '6月考核天数', '月度奖励', '考核目标', '备注']
|
|
r2 = 3
|
|
hdr_row(ws2, r2, dh); r2 += 1
|
|
|
|
name_order = ['赵连飞', '伍仲文', '岑彦']
|
|
detail_records.sort(key=lambda x: (name_order.index(x['name']), 0 if x['pay_type']=='当月' else 1, x['plate']))
|
|
|
|
for i, rec in enumerate(detail_records):
|
|
vals = [rec['name'], rec['plate'], rec['pay_type'], round(rec['amount'],2), f"{rec['loss_month']}月",
|
|
rec['client'], rec['loss_status'], rec['handling'], rec['transfer'],
|
|
rec['days'] if rec['days'] else '', rec['monthly_bonus'] if rec['monthly_bonus'] else '', rec['target'], rec['note']]
|
|
data_row(ws2, r2, vals); ws2.row_dimensions[r2].height = 22; r2 += 1
|
|
|
|
sub_total(ws2, r2, ['', '', '合计', round(sum(r['amount'] for r in detail_records),2), '', '', '', '', '', '', '', '', ''])
|
|
|
|
for i, w in enumerate([10, 12, 14, 14, 10, 24, 8, 18, 14, 12, 10, 22, 28], 1):
|
|
ws2.column_dimensions[get_column_letter(i)].width = w
|
|
|
|
# Personal sheets (3-5) + Transfer sheets (6-7) - simplified
|
|
for name, title_txt, transfer_to in [
|
|
('赵连飞', '赵连飞(2026-04-30离职,业务转→刘念念)', '刘念念'),
|
|
('伍仲文', '伍仲文(2026-05-09离职,业务转→钟祥)', '钟祥'),
|
|
('岑彦', '岑彦(2026-05-09离职,业务转→钟祥)', '钟祥'),
|
|
]:
|
|
ws = wb_res.create_sheet(name)
|
|
ws.merge_cells('A1:K1')
|
|
ws.cell(row=1, column=1, value=title_txt).font = Font(bold=True, size=14, name='宋体')
|
|
ws.cell(row=1, column=1).alignment = CC
|
|
|
|
ph = ['车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '处理方式', '6月考核天数', '月度奖励', '考核目标', '备注']
|
|
r3 = 3
|
|
hdr_row(ws, r3, ph); r3 += 1
|
|
|
|
person_recs = [p for p in detail_records if p['name'] == name]
|
|
for rec in person_recs:
|
|
vals = [rec['plate'], rec['pay_type'], round(rec['amount'],2), f"{rec['loss_month']}月",
|
|
rec['client'], rec['loss_status'], rec['handling'],
|
|
rec['days'] if rec['days'] else '', rec['monthly_bonus'] if rec['monthly_bonus'] else '',
|
|
rec['target'], rec['note']]
|
|
data_row(ws, r3, vals); ws.row_dimensions[r3].height = 22; r3 += 1
|
|
|
|
dn = sum(p['amount'] for p in person_recs if p['pay_type']=='当月')
|
|
ot = sum(p['amount'] for p in person_recs if p['pay_type']!='当月')
|
|
sub_total(ws, r3, ['', '不发了小计', round(ot,2), '', '', '', '', '', '', '', '']); r3 += 1
|
|
sub_total(ws, r3, ['', f'转{transfer_to}小计', round(dn,2), '', '', '', '', '', '', '', '需与其他部门确认']); r3 += 1
|
|
sub_total(ws, r3, ['', '合计', round(dn+ot,2), '', '', '', '', '', '', '', ''])
|
|
|
|
for i, w in enumerate([12, 14, 14, 10, 26, 8, 18, 12, 10, 22, 30], 1):
|
|
ws.column_dimensions[get_column_letter(i)].width = w
|
|
|
|
# Transfer detail sheets
|
|
for title_txt, sources, receiver in [
|
|
('转嫁至刘念念的奖金 — 来源:赵连飞(2026-04-30离职)', ['赵连飞'], '刘念念'),
|
|
('转嫁至钟祥的奖金 — 来源:伍仲文(2026-05-09离职)、岑彦(2026-05-09离职)', ['伍仲文', '岑彦'], '钟祥'),
|
|
]:
|
|
ws = wb_res.create_sheet(f'转{receiver}明细')
|
|
ws.merge_cells('A1:K1')
|
|
ws.cell(row=1, column=1, value=title_txt).font = Font(bold=True, size=14, name='宋体')
|
|
ws.cell(row=1, column=1).alignment = CC
|
|
|
|
ws.merge_cells('A2:K2')
|
|
ws.cell(row=2, column=1, value='⚠ 以下金额需与其他部门确认是否可发放给新销售').font = NOTE_FNT
|
|
ws.cell(row=2, column=1).alignment = LL
|
|
|
|
th = ['来源离职人员', '车牌号', '发放类型', '考核应发', '盈亏查询月', '客户名称', '客户盈亏', '6月考核天数', '月度奖励', '考核目标', '备注']
|
|
r4 = 4
|
|
hdr_row(ws, r4, th); r4 += 1
|
|
|
|
trf_recs = [p for p in detail_records if p['name'] in sources and p['pay_type']=='当月']
|
|
for rec in trf_recs:
|
|
vals = [rec['name'], rec['plate'], '当月', round(rec['amount'],2), f"{rec['loss_month']}月",
|
|
rec['client'], rec['loss_status'], rec['days'] if rec['days'] else '',
|
|
rec['monthly_bonus'] if rec['monthly_bonus'] else '', rec['target'], '需确认是否发给新销售']
|
|
data_row(ws, r4, vals); r4 += 1
|
|
|
|
sub_total(ws, r4, ['', '', '合计', round(sum(r['amount'] for r in trf_recs),2), '', '', '', '', '', '', ''])
|
|
|
|
for i, w in enumerate([14, 12, 14, 14, 10, 26, 8, 12, 10, 22, 22], 1):
|
|
ws.column_dimensions[get_column_letter(i)].width = w
|
|
|
|
resign_fp = os.path.join(OUT_DIR, '离职人员6月奖金处理明细.xlsx')
|
|
wb_res.save(resign_fp)
|
|
print(f"✅ 离职明细: {resign_fp}")
|
|
|
|
# ============================================================
|
|
# Step 8: Summary
|
|
# ============================================================
|
|
print("\n" + "=" * 60)
|
|
print("Step 8: 输出汇总")
|
|
print("=" * 60)
|
|
|
|
files = os.listdir(OUT_DIR)
|
|
for f in sorted(files):
|
|
fp = os.path.join(OUT_DIR, f)
|
|
size_kb = os.path.getsize(fp) / 1024
|
|
print(f" {f} ({size_kb:.0f} KB)")
|
|
|
|
print(f"\n{'=' * 60}")
|
|
print(f"6月考核应发: {total_6:.2f}")
|
|
print(f"最终发放: {final_total:.2f}")
|
|
print(f"全部文件已输出到 {OUT_DIR}/")
|
|
print(f"{'=' * 60}")
|