Files
mileage-bonus/excel_writer.py
kkfluous 8c2f4e73dd fix: 车辆追踪补全所有492辆车的考核目标和月度奖励
通过(归属公司+车型)→考核目标映射,补全109辆无考核记录车辆的考核目标。
含全角/半角括号兼容(现代氢能科技)。
现在492辆车全部有考核目标和月度奖励金额。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:47:41 +08:00

417 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Excel输出工具函数 + 各类sheet生成"""
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from collections import defaultdict
from calc_engine import RULES, DAYS
hf = Font(bold=True)
hfl = PatternFill(start_color='D9E1F2', end_color='D9E1F2', fill_type='solid')
bd = Border(left=Side(style='thin'),right=Side(style='thin'),top=Side(style='thin'),bottom=Side(style='thin'))
def WH(ws, headers, row=1):
for c, h in enumerate(headers, 1):
cl = ws.cell(row=row, column=c, value=h)
cl.font=hf; cl.fill=hfl; cl.border=bd; cl.alignment=Alignment(horizontal='center', wrap_text=True)
def WR(ws, rn, vals):
for c, v in enumerate(vals, 1):
cl = ws.cell(row=rn, column=c, value=v); cl.border=bd
def AW(ws):
for col in ws.columns:
ml = max((len(str(c.value or '')) for c in col), default=0)
ws.column_dimensions[col[0].column_letter].width = min(ml+3, 22)
def R(v, n=2):
return round(v, n) if isinstance(v, (int,float)) and v else v
def agg(dl):
bp = defaultdict(lambda: {'n':0,'a':0})
for d in dl: bp[d['销售']]['n']+=1; bp[d['销售']]['a']+=d['']
return dict(bp)
def write_sec(ws, rn, title, dl):
ws.cell(row=rn, column=1, value=title).font=Font(bold=True, size=11); rn+=1
WH(ws, ['销售人员','车辆数','金额'], rn); rn+=1
bp=agg(dl); tv=ta=0
for p in sorted(bp.keys()):
WR(ws,rn,[p,bp[p]['n'],R(bp[p]['a'])]); tv+=bp[p]['n']; ta+=bp[p]['a']; rn+=1
WR(ws,rn,['总计',tv,R(ta)]); ws.cell(row=rn,column=1).font=hf; return rn+2
def write_total(ws, rn, month, all_data):
total_bp = defaultdict(lambda: {'部门':'','':0})
for cat,dl in all_data.items():
for d in dl: total_bp[d['销售']]['']+=d['']; total_bp[d['销售']]['部门']=d['部门']
ws.cell(row=rn,column=1,value=f'{month}月合计应发奖励(按销售人员)').font=Font(bold=True,size=11); rn+=1
WH(ws,['销售人员','部门名称','合计应发奖励'],rn); rn+=1
gt=0
for p in sorted(total_bp.keys()):
WR(ws,rn,[p,total_bp[p]['部门'],R(total_bp[p][''])]); gt+=total_bp[p]['']; rn+=1
WR(ws,rn,['合计','',R(gt)]); ws.cell(row=rn,column=1).font=hf; rn+=2
by_dept=defaultdict(float)
for p,d in total_bp.items(): by_dept[d['部门']]+=d['']
ws.cell(row=rn,column=1,value=f'{month}月合计应发奖励(按部门)').font=Font(bold=True,size=11); rn+=1
WH(ws,['部门名称','合计应发奖励'],rn); rn+=1
for dept in sorted(by_dept.keys()): WR(ws,rn,[dept,R(by_dept[dept])]); rn+=1
WR(ws,rn,['合计',R(gt)]); ws.cell(row=rn,column=1).font=hf
# ============================================================
# Sheet生成函数
# ============================================================
def write_rules_sheet(wb):
ws = wb.active; ws.title='考核奖励规则'
WH(ws,['考核目标','月度目标里程(km)','奖励金额(元)'])
for i,(n,km,b) in enumerate([('交投40辆4.5T普货',3000,150),('交投190辆4.5T冷链车',3000,150),
('羚牛136辆4.5T冷链车',5000,260),('恒运50辆4.5T普货',5000,260),('羚牛100辆18T',6000,1000)],2):
WR(ws,i,[n,km,b])
AW(ws)
def write_detail_sheet(wb, records, month):
ws = wb.create_sheet(f'里程明细{month}')
WH(ws,['车牌号','部门名称','销售经理','客户名称','合同编号','考核目标','月度目标里程','对应月奖励金额',
'考核天数',f'{month}月应考核里程',f'{month}月实际行驶里程','完成率',f'{month}月是否达标',
f'{month}月奖金(天数折算)','多跑里程','可结转月数','考核状态'])
rn=2
for r in sorted(records, key=lambda x: (x['销售经理'],x['车牌号'])):
t,a = r['应考核里程(km)'], r['实际行驶里程(km)']
WR(ws,rn,[r['车牌号'],r['部门名称'],r['销售经理'],r.get('客户名称',''),r.get('合同编号',''),
r.get('考核目标',''),r['月度目标里程'],r['对应月奖励金额'],r['考核天数'],
R(t),R(a),R(a/t,4) if t>0 else 0,r['是否达标'],R(r['奖金']),R(r['多跑']),r['可结转'],
r.get('考核状态','')]); rn+=1
AW(ws)
def write_calc_process_jan(wb, G1):
ws = wb.create_sheet('1月计算过程')
WH(ws,['车牌号','销售经理','部门','①记录数','②总考核天数','③总应考核里程','④总实际里程',
'⑤有达标记录','⑥达标记录奖金合计','⑦总多跑里程','⑧月度目标里程','⑨可结转月数=floor(⑦/⑧)',
'→1月发放金额','→发放类型'])
rn=2
for k in sorted(G1.keys(), key=lambda x: (x[1],x[0])):
g=G1[k]
excess=sum(r['多跑'] for r in g['recs'] if r['是否达标']=='达标')
WR(ws,rn,[k[0],k[1],g['部门'],len(g['recs']),g['天数'],R(g['应考核']),R(g['实际']),
'' if g['有达标'] else '',R(g['奖金']),R(excess),g['目标km'],g['可结转'],
R(g['奖金']) if g['有达标'] else 0,'当月达标' if g['有达标'] else '未达标']); rn+=1
AW(ws)
def write_calc_process_feb(wb, G1, G2):
ws = wb.create_sheet('2月计算过程')
WH(ws,['车牌号','销售经理','部门',
'1月总应考核','1月总实际','1月有达标','1月奖金','1月多跑','1月可结转',
'2月记录数','2月总天数','2月总应考核','2月总实际','2月有达标','2月达标奖金',
'累计应完成','累计实际','累计是否达标',
'判断①结转','→结转金额(完整月)','判断②补发1月','→补发1月金额',
'判断③当月','→2月当月金额','判断④累计补发2月','→累计补发2月金额','2月发放合计'])
rn=2
for k in sorted(G2.keys(), key=lambda x: (x[1],x[0])):
g2=G2[k]; g1=G1.get(k)
j_q=g1['有达标'] if g1 else False; j_c=g1['可结转'] if g1 else 0
j_t=g1['应考核'] if g1 else 0; j_a=g1['实际'] if g1 else 0
j_bonus=g1['奖金'] if g1 else 0
je=sum(r['多跑'] for r in g1['recs'] if r['是否达标']=='达标') if g1 else 0
ct=g2.get('cum_t',0); ca=g2.get('cum_a',0); cq=g2.get('cum_q',False)
carry=g2.get('结转',0); bp1=g2.get('补发1月',0); bonus2=g2.get('当月奖金',0); cbp2=g2.get('累计补发2月',0)
c1=f"1月达标={j_q},可结转={j_c}{'' if carry>0 else ''}"
c2=f"1月未达标={not j_q},累计达标={cq}{'' if bp1>0 else ''}" if g1 else "无1月"
c3=f"2月达标={g2['有达标']},无结转={carry==0}{'' if bonus2>0 else ''}"
c4=f"2月未达标={not g2['有达标']},无结转={carry==0},累计达标={cq}{'' if cbp2>0 else ''}"
total=carry+bp1+bonus2+cbp2
WR(ws,rn,[k[0],k[1],g2['部门'],R(j_t),R(j_a),'' if j_q else '',R(j_bonus),R(je),j_c,
len(g2['recs']),g2['天数'],R(g2['应考核']),R(g2['实际']),'' if g2['有达标'] else '',R(g2['奖金']),
R(ct),R(ca),'达标' if cq else '未达标',
c1,R(carry) if carry>0 else 0,c2,R(bp1) if bp1>0 else 0,
c3,R(bonus2) if bonus2>0 else 0,c4,R(cbp2) if cbp2>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws)
def write_calc_process_mar(wb, G1, G2, G3, feb_data):
ws = wb.create_sheet('3月计算过程')
WH(ws,['车牌号','销售经理','部门',
'1月应考核','1月实际','1月有达标','1月奖金已发',
'2月应考核','2月实际','2月有达标','2月奖金已发',
'3月记录数','3月总天数','3月总应考核','3月总实际','3月有达标','3月达标奖金',
'累计应完成','累计实际','累计是否达标',
'判断①结转','→结转金额','判断②补发1月','→补发1月金额',
'判断③补发2月','→补发2月金额','判断④当月','→3月当月金额',
'判断⑤累计补发3月','→累计补发3月金额','3月发放合计'])
rn=2
for k in sorted(G3.keys(), key=lambda x: (x[1],x[0])):
g3=G3[k]; g2=G2.get(k); g1=G1.get(k)
j_t=g1['应考核'] if g1 else 0; j_a=g1['实际'] if g1 else 0
j_q=g1['有达标'] if g1 else False; j_paid=(g1['奖金']>0) if g1 else False
if g1 and j_q: j_paid=True
if g2 and g2.get('1月已补发',False): j_paid=True
f_t=g2['应考核'] if g2 else 0; f_a=g2['实际'] if g2 else 0
f_q=g2['有达标'] if g2 else False; f_paid=g2['2月已发'] if g2 else False
f_carry=g2['可结转'] if g2 else 0
ct=g3.get('cum_t',0); ca=g3.get('cum_a',0); cq=g3.get('cum_q',False)
carry=g3.get('结转',0); bj=g3.get('补发1月',0); bf2=g3.get('补发2月',0)
bonus3=g3.get('当月奖金',0); cbp3=g3.get('累计补发3月',0)
c1=f"2月可结转={f_carry}{'' if carry>0 else ''}"
c2=f"1月未发={not j_paid},累计达标={cq}{'' if bj>0 else ''}"
c3=f"2月未发={not f_paid},累计达标={cq}{'' if bf2>0 else ''}"
c4=f"3月达标={g3['有达标']},无结转={carry==0}{'' if bonus3>0 else ''}"
c5=f"3月未达标={not g3['有达标']},无结转={carry==0},累计达标={cq}{'' if cbp3>0 else ''}"
total=carry+bj+bf2+bonus3+cbp3
WR(ws,rn,[k[0],k[1],g3['部门'],R(j_t),R(j_a),'' if j_q else '','' if j_paid else '',
R(f_t),R(f_a),'' if f_q else '','' if f_paid else '',
len(g3['recs']),g3['天数'],R(g3['应考核']),R(g3['实际']),'' if g3['有达标'] else '',R(g3['奖金']),
R(ct),R(ca),'达标' if cq else '未达标',
c1,R(carry) if carry>0 else 0,c2,R(bj) if bj>0 else 0,
c3,R(bf2) if bf2>0 else 0,c4,R(bonus3) if bonus3>0 else 0,
c5,R(cbp3) if cbp3>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws)
def write_summary_jan(wb, records):
ws = wb.create_sheet('1月汇总')
jan_dl=[{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']} for r in records if r['是否达标']=='达标']
rn=write_sec(ws,1,'1月达标奖励',jan_dl)
write_total(ws,rn,1,{'达标':jan_dl})
AW(ws)
def write_summary_month(wb, month, month_data, section_names):
ws = wb.create_sheet(f'{month}月汇总')
rn=1
for i,cat in enumerate(section_names):
label_map = {'结转':f'一、结转:{month-1}月多跑在{month}月发',
'补发1月':'二、补发1月','补发2月':'三、补发2月',
'当月':f'{"" if month==2 else ""}{month}月当月奖励',
f'累计补发{month}':f'{"" if month==2 else ""}、累计达标补发{month}'}
rn=write_sec(ws,rn,label_map.get(cat,cat),month_data.get(cat,[]))
write_total(ws,rn,month,month_data)
AW(ws)
# ============================================================
# 新增业务员sheet
# ============================================================
def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments):
ws = wb.create_sheet(f'业务员_{person}')
ws.cell(row=1,column=1,value=f'{person} | {dept} | {settle_month}月绩效对账单').font=Font(bold=True,size=14)
plates = set()
for m in range(1, settle_month+1):
for k in G.get(m, {}):
if k[1] == person: plates.add(k[0])
person_total = sum(d[''] for cat,dl in month_data.items() for d in dl if d['销售']==person)
ws.cell(row=2,column=1,value=f'本月考核车辆: {len(plates)}辆 | 本月发放合计: {R(person_total)}').font=Font(bold=True,size=11)
# 表头:车辆信息 + 各月里程/目标 + 累计 + 奖金池
headers = ['车牌号','考核目标','月奖励']
for m in range(1, settle_month+1):
headers.append(f'{m}月里程/目标')
if settle_month >= 2:
headers += ['累计里程/目标']
headers.append('达标')
WH(ws, headers, 4)
rn = 5
for plate in sorted(plates):
g_cur = None
for m in range(settle_month, 0, -1):
g_cur = G.get(m, {}).get((plate, person))
if g_cur: break
if not g_cur: continue
first = g_cur['recs'][0]
mkm = g_cur['目标km']
# 第1行车辆信息 + 各月里程/目标
row = [plate, first.get('考核目标',''), g_cur['奖励额']]
cum_t = 0; cum_a = 0
for m in range(1, settle_month+1):
gm = G.get(m, {}).get((plate, person))
if gm:
t=gm['应考核']; a=gm['实际']; cum_t+=t; cum_a+=a
row.append(f'{R(a,0)}/{R(t,0)}')
else:
row.append('-')
if settle_month >= 2:
row.append(f'{R(cum_a,0)}/{R(cum_t,0)}')
cum_q = cum_a >= cum_t and cum_t > 0
# 达标判断取settle_month的group
g_s = G.get(settle_month, {}).get((plate, person))
if g_s and g_s.get('有达标'):
row.append('达标')
elif cum_q:
row.append('累计达标')
else:
row.append('未达标')
WR(ws, rn, row)
ws.cell(row=rn, column=1).font = Font(bold=True)
rn += 1
# 第2-N行发放明细历史已发 + 本月发放)
WH(ws, ['', '发放项', '金额', '说明', '奖金池'], rn); rn += 1
pays = vehicle_payments.get(plate, [])
pays_to_date = [p for p in pays if p['结算月'] <= settle_month]
total_periods = len(pays_to_date)
# 历史已发m < settle_month
for m in range(1, settle_month):
gm = G.get(m, {}).get((plate, person))
# 查该月是否有发放记录
m_pays = [p for p in pays_to_date if p['结算月'] == m and p['业务员'] == person]
if m_pays:
amt = sum(p['金额'] for p in m_pays)
types = ', '.join(p['类型'] for p in m_pays)
WR(ws, rn, ['', f'{m}月: 已发', R(amt), types, ''])
elif gm:
# 有考核但未发
# 检查是否在后续月被补发过settle_month之前
bp_pays = [p for p in pays_to_date if p['对应考核月'] == m and p['业务员'] == person]
if bp_pays:
amt = sum(p['金额'] for p in bp_pays)
sm = bp_pays[0]['结算月']
WR(ws, rn, ['', f'{m}月: 已发', R(amt), f'{sm}月补发', ''])
else:
WR(ws, rn, ['', f'{m}月: 未发', 0, f'未达标(实际{R(gm["实际"],0)}<目标{R(gm["应考核"],0)})', ''])
else:
WR(ws, rn, ['', f'{m}月: 无记录', '', '', ''])
rn += 1
# 本月发放
g_s = G.get(settle_month, {}).get((plate, person), {})
if not isinstance(g_s, dict): g_s = {}
gm_s = G.get(settle_month, {}).get((plate, person))
plate_this_month = 0
# 结转
carry = g_s.get('结转', 0)
if carry > 0:
prev = settle_month - 1
g_prev = G.get(prev, {}).get((plate, person))
prev_excess = sum(r['多跑'] for r in g_prev['recs'] if r['是否达标'] == '达标') if g_prev else 0
WR(ws, rn, ['', '结转', R(carry), f'{prev}月多跑{R(prev_excess,0)}{mkm},结转(完整月奖金)', ''])
plate_this_month += carry; rn += 1
# 补发过去月份
for prev_m in range(1, settle_month):
bp_key = f'补发{prev_m}'
bp_amt = g_s.get(bp_key, 0)
if bp_amt > 0:
WR(ws, rn, ['', f'补发{prev_m}', R(bp_amt), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{prev_m}', ''])
plate_this_month += bp_amt; rn += 1
# 当月
bonus = g_s.get('当月奖金', 0)
if bonus > 0:
excess = sum(r['多跑'] for r in gm_s['recs'] if r['是否达标'] == '达标') if gm_s else 0
desc = '当月达标'
if excess > 0 and mkm > 0 and int(excess // mkm) >= 1:
desc += f',多跑{R(excess,0)}{mkm}可结转'
WR(ws, rn, ['', f'{settle_month}月当月', R(bonus), desc, ''])
plate_this_month += bonus; rn += 1
# 累计补发当月
cum_bp = g_s.get(f'累计补发{settle_month}', 0)
if cum_bp > 0:
WR(ws, rn, ['', f'累计补发{settle_month}', R(cum_bp), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{settle_month}', ''])
plate_this_month += cum_bp; rn += 1
# 无发放
if plate_this_month == 0 and carry == 0:
if gm_s:
a_s = gm_s['实际']; t_s = gm_s['应考核']
if gm_s.get('结转占位'):
WR(ws, rn, ['', '本月不发', 0, '结转占位,本月不另发', ''])
else:
WR(ws, rn, ['', '本月不发', 0, f'未达标(实际{R(a_s,0)}<目标{R(t_s,0)}),累计也未达标', ''])
else:
WR(ws, rn, ['', '本月不发', 0, '本月无考核记录', ''])
rn += 1
# 本月合计 + 奖金池
pool_str = f'已发{total_periods}期/共12期剩余{12-total_periods}'
WR(ws, rn, ['', '本月合计', R(plate_this_month), '', pool_str])
ws.cell(row=rn, column=2).font = Font(bold=True)
ws.cell(row=rn, column=5).font = Font(italic=True)
rn += 2 # 空行
# 尾部
ws.cell(row=rn,column=1,value=f'{person} {settle_month}月合计: {len(plates)}辆车, 发放 {R(person_total)}').font=Font(bold=True,size=12)
AW(ws)
# ============================================================
# 新增车辆考核追踪sheet
# ============================================================
def write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info):
ws = wb.create_sheet('车辆考核追踪')
wrap_align = Alignment(wrap_text=True, vertical='top')
# 表头:每月用一列"业务员/里程/目标/达标"(多人换行)
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励金额']
for m in range(1, settle_month+1):
headers.append(f'{m}月考核明细')
if settle_month >= 2:
headers += ['累计里程/目标','累计达标']
headers += ['本月发放明细','累计已发期数','累计已发金额','剩余可发期数']
WH(ws, headers)
from calc_engine import VEHICLE_TARGET_MAP, RULES
rn=2
for mv in master_vehicles:
plate=mv['车牌号']
info=vehicle_info.get(plate, {})
pays=vehicle_payments.get(plate, [])
# 补全缺失的考核目标
target_name = info.get('考核目标','')
monthly_bonus = info.get('月度奖励',0)
if not target_name:
company = mv.get('归属公司','')
vtype = mv.get('车型确定','')
mapped = VEHICLE_TARGET_MAP.get((company, vtype))
if mapped:
target_name, _, monthly_bonus = mapped
row=[plate,mv.get('车架号',''),mv.get('归属公司',''),mv.get('车型确定',''),
target_name or '',monthly_bonus or '']
cum_t=0; cum_a=0
for m in range(1, settle_month+1):
mgs=[(k,g) for k,g in G.get(m,{}).items() if k[0]==plate]
if mgs:
# 每个(车牌,销售)组一行,换行显示
lines = []
for _,g in sorted(mgs, key=lambda x: x[0][1]):
t=g['应考核']; a=g['实际']
cum_t+=t; cum_a+=a
q='达标' if g['有达标'] else '未达标'
lines.append(f"{g['销售']}: {R(a,0)}/{R(t,0)} {q}")
row.append('\n'.join(lines))
else:
row.append('')
if settle_month>=2:
cum_q='达标' if (cum_a>=cum_t and cum_t>0) else '未达标'
row+=[f'{R(cum_a,0)}/{R(cum_t,0)}', cum_q]
# 本月发放明细(多人多类型换行)
tp=[p for p in pays if p['结算月']==settle_month]
if tp:
pay_lines = []
for p in sorted(tp, key=lambda x: x['业务员']):
pay_lines.append(f"{p['业务员']}: {R(p['金额'])}({p['类型']})")
row.append('\n'.join(pay_lines))
else:
row.append('')
# 奖金池截至settle_month
pays_to_date=[p for p in pays if p['结算月']<=settle_month]
tp_count=len(pays_to_date); tp_amt=sum(p['金额'] for p in pays_to_date)
row+=[tp_count,R(tp_amt) if tp_amt>0 else 0,12-tp_count]
WR(ws,rn,row)
# 对含换行的单元格设置自动换行
for ci in range(len(row)):
cell = ws.cell(row=rn, column=ci+1)
if isinstance(cell.value, str) and '\n' in cell.value:
cell.alignment = wrap_align
rn+=1
AW(ws)