Files
mileage-bonus/excel_writer.py
kkfluous ee962c97ae fix: 无亏损表的月份全部不发放
3月无亏损表→所有车辆标注"未匹配"→拦截全部考核应发→实发0。
移除了"未匹配且有亏损表"的多余条件判断。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:56:03 +08:00

770 lines
38 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, loss_data=None, plate_client=None):
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)
if loss_data is not None:
# 亏损拦截
blocked = []
passed = []
unmatched = []
for d in jan_dl:
client = (plate_client or {}).get(d['车牌'], '')
status = loss_data.get(client, '未匹配') if client else '未匹配'
if status == '':
blocked.append({**d, '客户': client})
elif status == '未匹配':
unmatched.append({**d, '客户': client})
else:
passed.append(d)
rn = write_sec(ws, rn, '亏损拦截(客户亏损不发放)', blocked if blocked else [])
rn = write_sec(ws, rn, '未匹配亏损表(需人工确认)', unmatched if unmatched else [])
rn = write_sec(ws, rn, '最终发放', passed)
ws.cell(row=rn, column=1, value='汇总').font=Font(bold=True, size=11); rn+=1
total_考核 = sum(d[''] for d in jan_dl)
total_拦截 = sum(d[''] for d in blocked)
total_未匹配 = sum(d[''] for d in unmatched)
total_最终 = sum(d[''] for d in passed)
WR(ws, rn, ['考核应发', R(total_考核)]); rn+=1
WR(ws, rn, ['亏损拦截', R(total_拦截)]); rn+=1
WR(ws, rn, ['未匹配(待确认)', R(total_未匹配)]); rn+=1
WR(ws, rn, ['最终发放', R(total_最终)]); ws.cell(row=rn,column=1).font=Font(bold=True); rn+=2
write_total(ws,rn,1,{'达标':jan_dl})
AW(ws)
def build_payment_records(month, month_data, loss_data, plate_client):
"""构建奖金发放记录列表,每条考核应发一行,叠加亏损筛选"""
records = []
for cat, dl in month_data.items():
for d in dl:
client = (plate_client or {}).get(d['车牌'], '')
if loss_data:
loss_status = loss_data.get(client, '未匹配') if client else '未匹配'
else:
loss_status = '未匹配' # 无亏损表视为未匹配,不发放
考核应发 = d['']
if loss_status == '':
拦截 = 考核应发; 实发 = 0
elif loss_status == '未匹配':
拦截 = 考核应发; 实发 = 0
else:
拦截 = 0; 实发 = 考核应发
records.append({
'车牌号': d['车牌'],
'业务员': d['销售'],
'部门': d['部门'],
'客户名称': client,
'发放类型': cat,
'考核应发': 考核应发,
'客户盈亏': loss_status,
'亏损拦截': 拦截,
'实发金额': 实发,
})
return records
def write_payment_record_sheet(wb, month, payment_records):
"""写入奖金发放记录sheet"""
ws = wb.create_sheet(f'{month}月奖金发放记录')
headers = ['车牌号','业务员','部门','客户名称','发放类型',
'考核应发','客户盈亏','亏损拦截','实发金额']
WH(ws, headers)
green_fill = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
red_fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
yellow_fill = PatternFill(start_color='FFFFCC', end_color='FFFFCC', fill_type='solid')
rn = 2
for r in sorted(payment_records, key=lambda x: (x['业务员'], x['车牌号'])):
WR(ws, rn, [r['车牌号'], r['业务员'], r['部门'], r['客户名称'], r['发放类型'],
R(r['考核应发']), r['客户盈亏'], R(r['亏损拦截']), R(r['实发金额'])])
# 颜色
if r['客户盈亏'] == '':
for ci in [7, 8, 9]: ws.cell(row=rn, column=ci).fill = red_fill
elif r['客户盈亏'] == '未匹配':
for ci in [7, 8, 9]: ws.cell(row=rn, column=ci).fill = yellow_fill
elif r['实发金额'] > 0:
ws.cell(row=rn, column=9).fill = green_fill
rn += 1
# 合计行
total_应发 = sum(r['考核应发'] for r in payment_records)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
total_实发 = sum(r['实发金额'] for r in payment_records)
rn += 1
WR(ws, rn, ['', '', '', '', '合计', R(total_应发), '', R(total_拦截), R(total_实发)])
for ci in range(5, 10): ws.cell(row=rn, column=ci).font = Font(bold=True)
ws.auto_filter.ref = f"A1:I1"
AW(ws)
return payment_records
def write_summary_from_records(wb, month, payment_records):
"""从奖金发放记录生成月汇总"""
ws = wb.create_sheet(f'{month}月汇总')
rn = 1
# 按发放类型分section
by_type = defaultdict(list)
for r in payment_records:
by_type[r['发放类型']].append({'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['实发金额']})
# 考核应发(按类型)
ws.cell(row=rn, column=1, value='一、考核应发明细').font=Font(bold=True, size=12); rn+=2
by_type_应发 = defaultdict(list)
for r in payment_records:
by_type_应发[r['发放类型']].append({'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['考核应发']})
for cat in sorted(by_type_应发.keys()):
rn = write_sec(ws, rn, f'考核应发-{cat}', by_type_应发[cat])
total_应发 = sum(r['考核应发'] for r in payment_records)
WR(ws, rn, ['考核应发合计', '', R(total_应发)]); ws.cell(row=rn,column=1).font=Font(bold=True,size=11); rn+=2
# 亏损拦截
blocked = [r for r in payment_records if r['客户盈亏'] == '' and r['考核应发'] > 0]
unmatched = [r for r in payment_records if r['客户盈亏'] == '未匹配' and r['考核应发'] > 0]
if blocked or unmatched:
ws.cell(row=rn, column=1, value='二、亏损筛选').font=Font(bold=True, size=12); rn+=2
if blocked:
bl_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['亏损拦截']} for r in blocked]
rn = write_sec(ws, rn, '亏损拦截(客户亏损不发放)', bl_dl)
if unmatched:
um_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['亏损拦截']} for r in unmatched]
rn = write_sec(ws, rn, '未匹配亏损表(需人工确认)', um_dl)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
WR(ws, rn, ['拦截合计', '', R(total_拦截)]); ws.cell(row=rn,column=1).font=Font(bold=True); rn+=2
# 最终发放(按销售人员)
ws.cell(row=rn, column=1, value='三、最终发放').font=Font(bold=True, size=12); rn+=2
passed = [r for r in payment_records if r['实发金额'] > 0]
passed_dl = [{'车牌':r['车牌号'],'销售':r['业务员'],'部门':r['部门'],'':r['实发金额']} for r in passed]
rn = write_sec(ws, rn, '最终发放明细', passed_dl)
# 最终发放合计(按销售/按部门)
total_by_person = defaultdict(lambda: {'部门':'','':0})
for r in passed:
total_by_person[r['业务员']][''] += r['实发金额']
total_by_person[r['业务员']]['部门'] = r['部门']
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_by_person.keys()):
d = total_by_person[p]
WR(ws, rn, [p, d['部门'], R(d[''])]); gt += d['']; 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_by_person.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; rn+=2
# 汇总数字
total_实发 = sum(r['实发金额'] for r in payment_records)
total_拦截 = sum(r['亏损拦截'] for r in payment_records)
ws.cell(row=rn, column=1, value='总览').font=Font(bold=True, size=12); rn+=1
WR(ws, rn, ['考核应发', R(total_应发)]); rn+=1
WR(ws, rn, ['亏损拦截', R(total_拦截)]); rn+=1
WR(ws, rn, ['最终发放', R(total_实发)]); ws.cell(row=rn,column=1).font=Font(bold=True,size=12)
ws.cell(row=rn,column=2).font=Font(bold=True,size=12)
AW(ws)
# 保留旧函数兼容3月无亏损表时使用
def write_summary_month(wb, month, month_data, section_names, loss_data=None, plate_client=None):
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)
# 找最后一行
rn = ws.max_row + 2
if loss_data is not None:
# 合并所有发放记录
all_dl = []
for cat, dl in month_data.items():
all_dl.extend(dl)
blocked = []; passed = []; unmatched = []
for d in all_dl:
client = (plate_client or {}).get(d['车牌'], '')
status = loss_data.get(client, '未匹配') if client else '未匹配'
if status == '':
blocked.append(d)
elif status == '未匹配':
unmatched.append(d)
else:
passed.append(d)
ws.cell(row=rn, column=1, value='═══ 亏损筛选 ═══').font=Font(bold=True, size=12); rn+=2
rn = write_sec(ws, rn, '亏损拦截(客户亏损不发放)', blocked)
rn = write_sec(ws, rn, '未匹配亏损表(需人工确认)', unmatched)
rn = write_sec(ws, rn, '最终发放', passed)
total_考核 = sum(d[''] for d in all_dl)
total_拦截 = sum(d[''] for d in blocked)
total_未匹配 = sum(d[''] for d in unmatched)
total_最终 = sum(d[''] for d in passed)
ws.cell(row=rn, column=1, value='亏损筛选汇总').font=Font(bold=True, size=11); rn+=1
WR(ws, rn, ['考核应发合计', R(total_考核)]); rn+=1
WR(ws, rn, ['亏损拦截金额', R(total_拦截)]); rn+=1
WR(ws, rn, ['未匹配金额(待确认)', R(total_未匹配)]); rn+=1
WR(ws, rn, ['最终应发合计', R(total_最终)])
ws.cell(row=rn,column=1).font=Font(bold=True, size=12)
ws.cell(row=rn,column=2).font=Font(bold=True, size=12)
AW(ws)
# ============================================================
# 新增业务员sheet
# ============================================================
def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments):
short_dept = dept.replace('业务','') if '业务' in dept else dept
ws = wb.create_sheet(f'{short_dept}-{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, loss_data=None, plate_client=None):
ws = wb.create_sheet('车辆考核追踪')
from calc_engine import VEHICLE_TARGET_MAP, DAYS
center_top = Alignment(horizontal='center', vertical='top', wrap_text=True)
left_top = Alignment(vertical='top', wrap_text=True)
green_font = Font(color='006100')
green_fill = PatternFill(start_color='C6EFCE', end_color='C6EFCE', fill_type='solid')
red_font = Font(color='9C0006')
red_fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
grey_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
blue_fill = PatternFill(start_color='DAEEF3', end_color='DAEEF3', fill_type='solid')
gold_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
# 表头:基本信息 + 业务员 + 每月(应考核/实际/达标) + 累计 + 发放 + 奖金池
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励','业务员']
info_cols = 6 # 前6列是车辆信息需要合并
for m in range(1, settle_month+1):
headers += [f'{m}月应考核', f'{m}月实际', f'{m}月达标']
if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标']
if loss_data is not None:
headers += ['客户名称','客户是否亏损','考核应发','最终发放','未发放原因']
else:
headers += ['本月发放金额','发放类型']
headers += ['已发期数','已发金额','剩余期数']
WH(ws, headers)
ws.freeze_panes = 'H2' # 冻结车辆信息+业务员列
# 交替行底色
stripe_a = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid') # 白
stripe_b = PatternFill(start_color='DCE6F1', end_color='DCE6F1', fill_type='solid') # 浅蓝
vehicle_idx = 0
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
# 收集该车所有(业务员) - 跨月去重
person_set = {}
for m in range(1, settle_month+1):
for k,g in G.get(m, {}).items():
if k[0] == plate:
person_set[k[1]] = g['部门']
persons = sorted(person_set.keys())
if not persons:
persons = [''] # 无考核记录也占一行
row_stripe = stripe_b if vehicle_idx % 2 == 1 else stripe_a
start_rn = rn
for pi, person in enumerate(persons):
sd = person_set.get(person,'').replace('业务','') if person else ''
person_label = f'{sd}-{person}' if person and sd else person
# 车辆信息(只在第一行写,后面留空等合并)
if pi == 0:
row_base = [plate, mv.get('车架号',''), mv.get('归属公司',''),
mv.get('车型确定',''), target_name or '', monthly_bonus or '']
else:
row_base = ['','','','','','']
row = row_base + [person_label]
# 每月数据按该业务员的group
cum_t = 0; cum_a = 0
for m in range(1, settle_month+1):
gm = G.get(m, {}).get((plate, person)) if person else None
if gm:
# 逐条记录汇总(同人同月可能多条)
t_sum = gm['应考核']; a_sum = gm['实际']
cum_t += t_sum; cum_a += a_sum
# 每条记录的达标情况
rec_details = []
for rec in gm['recs']:
rq = rec['是否达标'] == '达标'
rec_details.append(f"{R(rec['实际行驶里程(km)'],0)}/{R(rec['应考核里程(km)'],0)}{'' if rq else ''}")
all_q = all(rec['是否达标'] == '达标' for rec in gm['recs'])
if len(gm['recs']) == 1:
detail = rec_details[0].split('')[0].split('')[0] # 只取数字
row += [R(t_sum), R(a_sum), '' if all_q else '']
else:
# 多条:显示汇总,但单元格内注明各条
row += [R(t_sum), R(a_sum), '\n'.join(rec_details)]
else:
row += ['', '', '']
# 累计
if settle_month >= 2:
if cum_t > 0:
cum_q = cum_a >= cum_t
row += [R(cum_t), R(cum_a), '' if cum_q else '']
else:
row += ['', '', '']
cum_q = False
else:
cum_q = False
# 本月发放(该业务员的)
tp = [p for p in pays if p['结算月'] == settle_month and p['业务员'] == person]
考核应发 = sum(p['金额'] for p in tp) if tp else 0
pay_types = ', '.join(p['类型'] for p in tp) if tp else ''
if loss_data is not None:
# 有亏损表:加客户名称、亏损状态、考核应发、最终发放、未发放原因
client = (plate_client or {}).get(plate, '')
loss_status = loss_data.get(client, '未匹配') if client else '未匹配'
if loss_status == '':
final_amt = 0
reason = '客户亏损不发放' if 考核应发 > 0 else ''
elif loss_status == '未匹配':
final_amt = 0
reason = '未匹配亏损表' if 考核应发 > 0 else ''
else:
final_amt = 考核应发
reason = ''
row += [client, loss_status, R(考核应发), R(final_amt), reason]
else:
# 无亏损表:直接发放
row += [R(考核应发), pay_types]
# 奖金池(整车,只在第一行显示)
if pi == 0:
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 += [f'{tp_count}/12', R(tp_amt) if tp_amt > 0 else 0, 12 - tp_count]
else:
row += ['', '', '']
WR(ws, rn, row)
# 美化:先刷底色(交替色),再叠加特殊色
for ci in range(1, len(headers) + 1):
ws.cell(row=rn, column=ci).fill = row_stripe
# 车辆信息列灰底(覆盖条纹)
for ci in range(1, info_cols + 1):
ws.cell(row=rn, column=ci).fill = grey_fill if vehicle_idx % 2 == 0 else PatternFill(start_color='E8E8E8', end_color='E8E8E8', fill_type='solid')
# 月度达标列着色
for m in range(1, settle_month + 1):
col_base = info_cols + 1 + (m - 1) * 3 # 业务员列后面
qual_col = col_base + 3 # 达标列
cell = ws.cell(row=rn, column=qual_col)
cell.alignment = center_top
val = cell.value
if val and '' in str(val) and '' not in str(val):
cell.fill = green_fill; cell.font = green_font
elif val and '' in str(val):
cell.fill = red_fill; cell.font = red_font
# 累计达标着色
if settle_month >= 2:
cum_qual_col = info_cols + 1 + settle_month * 3 + 3 # 累计达标列
cell = ws.cell(row=rn, column=cum_qual_col)
if cell.value == '':
cell.fill = green_fill; cell.font = Font(bold=True, color='006100')
elif cell.value == '':
cell.fill = red_fill; cell.font = Font(bold=True, color='9C0006')
# 发放金底
pay_col = info_cols + 1 + settle_month * 3 + (3 if settle_month >= 2 else 0) + 1
if tp:
ws.cell(row=rn, column=pay_col).fill = gold_fill
# 奖金池蓝底
if pi == 0 and tp_count > 0:
pool_col = pay_col + 2
ws.cell(row=rn, column=pool_col).fill = blue_fill
ws.cell(row=rn, column=pool_col).font = Font(bold=True)
rn += 1
# 合并车辆信息列(如果多人)
if len(persons) > 1:
for ci in range(1, info_cols + 1):
ws.merge_cells(start_row=start_rn, start_column=ci,
end_row=start_rn + len(persons) - 1, end_column=ci)
ws.cell(row=start_rn, column=ci).alignment = Alignment(vertical='center', wrap_text=True)
# 奖金池列也合并
for offset in [2, 3, 4]: # 已发期数/已发金额/剩余期数
pool_col = pay_col + offset
ws.merge_cells(start_row=start_rn, start_column=pool_col,
end_row=start_rn + len(persons) - 1, end_column=pool_col)
vehicle_idx += 1
# 列宽
# 隐藏BCD列车架号/归属公司/车型)
for cl in ['B','C','D']:
ws.column_dimensions[cl].hidden = True
col_widths = {'A':12,'B':18,'C':18,'D':10,'E':18,'F':8,'G':14}
for cl, w in col_widths.items():
ws.column_dimensions[cl].width = w
# 月度列
start = ord('H')
for m in range(settle_month):
for i in range(3):
cl = chr(start + m*3 + i)
if cl <= 'Z': ws.column_dimensions[cl].width = 12
# 剩余列
rem = start + settle_month * 3
for i in range(10):
cl = chr(rem + i)
if cl <= 'Z': ws.column_dimensions[cl].width = 14