8 Commits

Author SHA1 Message Date
kkfluous
f2de5d5500 feat: 车辆考核追踪sheet移到业务员sheet前面
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:01:13 +08:00
kkfluous
54ecbc352f feat: 车辆考核追踪sheet业务员名加部门前缀
考核明细和发放明细中的业务员统一显示为"X部-姓名"格式。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:00:27 +08:00
kkfluous
cdc4cec2ff feat: 业务员sheet命名加部门前缀(如"二部-刘念念")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:53:03 +08:00
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
kkfluous
e47cc00b3d V2.3.0 车辆考核追踪:多人多条用单元格内换行显示
同车多个销售经理在同一单元格内换行展示:
- X月考核明细: "刘念念: 4834/2903 达标\n董剑煜: 1294/2710 未达标"
- 本月发放明细: "赵连飞: 150(结转)\n董剑煜: 260(当月达标)"
设置wrap_text自动换行,一车一行不拆分。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:51:16 +08:00
kkfluous
4273592160 V2.2.0 业务员sheet重写为对账单风格
每辆车展示:
- 第1行:车辆信息 + 各月里程/目标 + 累计 + 达标状态
- 下方:历史已发(哪月发的、金额、来源)+ 本月发放明细 + 奖金池
- "历史已发"列清楚展示防重复证据
- 发放说明包含关键数字(累计里程≥目标等)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:33:04 +08:00
kkfluous
1b7d25c821 fix: 业务员sheet奖金池期数按当前核算月过滤
之前vehicle_payments是Q1全量,导致1月文件显示了3个月的累计期数。
改为只统计截至settle_month的发放记录。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:41:34 +08:00
kkfluous
95f1685612 V2.1.0 业务员sheet增加"发放说明"列
将技术化的"发放类型"改为动态生成的"发放说明",用一句话解释为什么发/不发:
- 当月达标 → "当月达标"
- 结转 → "1月多跑9578≥3000,结转(完整月奖金)"
- 累计补发 → "1-2月累计10620≥10000,累计达标补发"
- 未达标 → "未达标(实际1486<目标5000)"
- 补发过去月 → "1-3月累计14427≥10161,补发1月"

包含关键数字,非技术人员一眼看懂计算依据。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:37:13 +08:00
3 changed files with 184 additions and 79 deletions

View File

@@ -169,6 +169,16 @@ def collect_vehicle_payments(G, feb_data, mar_data):
for i, r in enumerate(records): r['期数'] = i + 1 for i, r in enumerate(records): r['期数'] = i + 1
return payments, info return payments, info
# (归属公司+车型) → 考核目标 映射,用于补全无考核记录的车辆
VEHICLE_TARGET_MAP = {
('广州开发区交投氢能运营管理有限公司', '4.5吨冷链车'): ('交投190辆4.5T冷链车', 3000, 150),
('广州开发区交投氢能运营管理有限公司', '4.5吨货车'): ('交投40辆4.5T普货', 3000, 150),
('羚牛氢能科技(广东)有限公司', '4.5吨冷链车'): ('羚牛136辆4.5T冷链车', 5000, 260),
('羚牛氢能科技(广东)有限公司', '18吨双飞翼货车'): ('羚牛100辆18T', 6000, 1000),
('现代氢能科技(广州)有限公司', '4.5吨货车'): ('恒运50辆4.5T普货', 5000, 260),
('现代氢能科技(广州)有限公司', '4.5吨货车'): ('恒运50辆4.5T普货', 5000, 260),
}
def read_master_vehicles(fp='里程任务考核_Q1汇总.xlsx'): def read_master_vehicles(fp='里程任务考核_Q1汇总.xlsx'):
"""从现有Q1汇总文件读取全量车辆台账""" """从现有Q1汇总文件读取全量车辆台账"""
wb = openpyxl.load_workbook(fp, data_only=True) wb = openpyxl.load_workbook(fp, data_only=True)

View File

@@ -186,88 +186,150 @@ def write_summary_month(wb, month, month_data, section_names):
# ============================================================ # ============================================================
def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments): def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, vehicle_payments):
ws = wb.create_sheet(f'业务员_{person}') short_dept = dept.replace('业务','') if '业务' in dept else dept
ws.cell(row=1,column=1,value=f'{person} | {dept} | {settle_month}月考核').font=Font(bold=True,size=14) 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)
# 收集该人在1-settle_month中有记录的所有车牌
plates = set() plates = set()
for m in range(1, settle_month+1): for m in range(1, settle_month+1):
for k in G.get(m, {}): for k in G.get(m, {}):
if k[1] == person: plates.add(k[0]) 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) 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) ws.cell(row=2,column=1,value=f'本月考核车辆: {len(plates)}辆 | 本月发放合计: {R(person_total)}').font=Font(bold=True,size=11)
rn = 4 # 表头:车辆信息 + 各月里程/目标 + 累计 + 奖金池
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): for plate in sorted(plates):
# 车辆信息取最近月的group
g_cur = None g_cur = None
for m in range(settle_month, 0, -1): for m in range(settle_month, 0, -1):
g_cur = G.get(m, {}).get((plate, person)) g_cur = G.get(m, {}).get((plate, person))
if g_cur: break if g_cur: break
if not g_cur: continue if not g_cur: continue
first = g_cur['recs'][0] first = g_cur['recs'][0]
mkm = g_cur['目标km']
ws.cell(row=rn,column=1,value=plate).font=Font(bold=True,size=11) # 第1行车辆信息 + 各月里程/目标
ws.cell(row=rn,column=2,value=first.get('合同编号','')) row = [plate, first.get('考核目标',''), g_cur['奖励额']]
ws.cell(row=rn,column=3,value=first.get('客户名称',''))
ws.cell(row=rn,column=4,value=first.get('考核目标',''))
ws.cell(row=rn,column=5,value=f'月度目标{g_cur["目标km"]}km | 月奖励{g_cur["奖励额"]}')
rn += 1
# 表头
headers = ['月份','考核天数','应考核里程','实际里程','完成率','达标','本月奖金','发放类型']
if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标']
WH(ws, headers, rn); rn += 1
# 每月一行
cum_t = 0; cum_a = 0 cum_t = 0; cum_a = 0
for m in range(1, settle_month+1): for m in range(1, settle_month+1):
gm = G.get(m, {}).get((plate, person)) gm = G.get(m, {}).get((plate, person))
if not gm: if gm:
WR(ws, rn, [f'{m}','','','','','无记录']); rn+=1; continue
t=gm['应考核']; a=gm['实际']; cum_t+=t; cum_a+=a t=gm['应考核']; a=gm['实际']; cum_t+=t; cum_a+=a
qual='达标' if gm['有达标'] else '未达标' row.append(f'{R(a,0)}/{R(t,0)}')
rate=R(a/t*100,0) if t>0 else 0
# 发放类型和金额
pay_type=''; pay_amt=0
if m == settle_month:
carry=gm.get('结转',0); bonus=gm.get('当月奖金',0)
cum_bp=gm.get(f'累计补发{m}',0)
if carry>0: pay_type='结转'; pay_amt=carry
elif bonus>0: pay_type='当月达标'; pay_amt=bonus
elif cum_bp>0: pay_type=f'累计补发'; pay_amt=cum_bp
else: else:
if gm['有达标']: pay_type='(已发)'; pay_amt=gm.get('奖金',0) row.append('-')
else: pay_type='(未达标)'
row_data=[f'{m}',gm['天数'],R(t),R(a),f'{rate}%',qual,R(pay_amt) if pay_amt>0 else 0,pay_type]
if settle_month >= 2: if settle_month >= 2:
cum_q='达标' if (cum_a>=cum_t and cum_t>0) else '未达标' row.append(f'{R(cum_a,0)}/{R(cum_t,0)}')
row_data+=[R(cum_t),R(cum_a),cum_q] cum_q = cum_a >= cum_t and cum_t > 0
WR(ws,rn,row_data); rn+=1 # 达标判断取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行发放明细历史已发 + 本月发放
if settle_month >= 2: 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), {}) 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): for prev_m in range(1, settle_month):
bp_key = f'补发{prev_m}' bp_key = f'补发{prev_m}'
bp_amt = g_s.get(bp_key, 0) if isinstance(g_s, dict) else 0 bp_amt = g_s.get(bp_key, 0)
if bp_amt > 0: if bp_amt > 0:
row_data = [f'补发{prev_m}','','','','','',R(bp_amt),f'补发{prev_m}'] WR(ws, rn, ['', f'补发{prev_m}', R(bp_amt), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{prev_m}', ''])
if settle_month>=2: row_data += ['','',''] plate_this_month += bp_amt; rn += 1
WR(ws,rn,row_data); rn+=1
# 奖金池 # 当月
pays = vehicle_payments.get(plate, []) bonus = g_s.get('当月奖金', 0)
total_periods = len(pays) if bonus > 0:
plate_this = sum(p['金额'] for p in pays if p['结算月']==settle_month and p['业务员']==person) excess = sum(r['多跑'] for r in gm_s['recs'] if r['是否达标'] == '达标') if gm_s else 0
ws.cell(row=rn,column=1,value=f'小计: {R(plate_this)}元 | 奖金池: 已发{total_periods}期/共12期, 剩余{12-total_periods}').font=Font(italic=True) desc = '当月达标'
rn += 2 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) ws.cell(row=rn,column=1,value=f'{person} {settle_month}月合计: {len(plates)}辆车, 发放 {R(person_total)}').font=Font(bold=True,size=12)
@@ -279,46 +341,79 @@ def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, ve
def write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info): def write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info):
ws = wb.create_sheet('车辆考核追踪') ws = wb.create_sheet('车辆考核追踪')
wrap_align = Alignment(wrap_text=True, vertical='top')
# 表头:每月用一列"业务员/里程/目标/达标"(多人换行)
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励金额'] headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励金额']
for m in range(1, settle_month+1): for m in range(1, settle_month+1):
headers += [f'{m}月业务员',f'{m}考核',f'{m}月实际',f'{m}月达标'] headers.append(f'{m}月考核明细')
if settle_month >= 2: if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标'] headers += ['累计里程/目标','累计达标']
headers += ['本月发放金额','发放给谁','发放类型','累计已发期数','累计已发金额','剩余可发期数'] headers += ['本月发放明细','累计已发期数','累计已发金额','剩余可发期数']
WH(ws, headers) WH(ws, headers)
from calc_engine import VEHICLE_TARGET_MAP, RULES
rn=2 rn=2
for mv in master_vehicles: for mv in master_vehicles:
plate=mv['车牌号'] plate=mv['车牌号']
info=vehicle_info.get(plate, {}) info=vehicle_info.get(plate, {})
pays=vehicle_payments.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('车型确定',''), row=[plate,mv.get('车架号',''),mv.get('归属公司',''),mv.get('车型确定',''),
info.get('考核目标',''),info.get('月度奖励',0) or ''] target_name or '',monthly_bonus or '']
cum_t=0; cum_a=0 cum_t=0; cum_a=0
for m in range(1, settle_month+1): for m in range(1, settle_month+1):
mgs=[(k,g) for k,g in G.get(m,{}).items() if k[0]==plate] mgs=[(k,g) for k,g in G.get(m,{}).items() if k[0]==plate]
if mgs: if mgs:
persons=', '.join(sorted(set(g['销售'] for _,g in mgs))) # 每个(车牌,销售)组一行,换行显示
ts=sum(g['应考核'] for _,g in mgs); As=sum(g['实际'] for _,g in mgs) lines = []
q='达标' if any(g['有达标'] for _,g in mgs) else '未达标' for _,g in sorted(mgs, key=lambda x: x[0][1]):
cum_t+=ts; cum_a+=As t=g['应考核']; a=g['实际']
row+=[persons,R(ts),R(As),q] cum_t+=t; cum_a+=a
q='达标' if g['有达标'] else '未达标'
sd = g['部门'].replace('业务','') if '业务' in g.get('部门','') else g.get('部门','')
lines.append(f"{sd}-{g['销售']}: {R(a,0)}/{R(t,0)} {q}")
row.append('\n'.join(lines))
else: else:
row+=['','','',''] row.append('')
if settle_month>=2:
row+=[R(cum_t),R(cum_a),'达标' if (cum_a>=cum_t and cum_t>0) else '未达标']
# 本月发放(截至settle_month 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] tp=[p for p in pays if p['结算月']==settle_month]
if tp: if tp:
row+=[R(sum(p['金额'] for p in tp)),', '.join(sorted(set(p['业务员'] for p in tp))), pay_lines = []
', '.join(sorted(set(p['类型'] for p in tp)))] for p in sorted(tp, key=lambda x: x['业务员']):
pd = p.get('部门','').replace('业务','') if '业务' in p.get('部门','') else p.get('部门','')
pay_lines.append(f"{pd}-{p['业务员']}: {R(p['金额'])}({p['类型']})")
row.append('\n'.join(pay_lines))
else: else:
row+=[0,'',''] row.append('')
# 奖金池截至settle_month # 奖金池截至settle_month
pays_to_date=[p for p in pays if p['结算月']<=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) 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] row+=[tp_count,R(tp_amt) if tp_amt>0 else 0,12-tp_count]
WR(ws,rn,row); rn+=1
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) AW(ws)

View File

@@ -74,13 +74,13 @@ for settle_month in [1, 2, 3]:
else: else:
write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月']) write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月'])
# Sheet 5-16: 业务员 # Sheet 5: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info)
# Sheet 6-17: 业务员
for person in sorted(all_persons.keys()): for person in sorted(all_persons.keys()):
write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments) write_salesperson_sheet(wb, person, all_persons[person], settle_month, D, G, month_data, vehicle_payments)
# Sheet 17: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info)
# 删除默认空sheet # 删除默认空sheet
if 'Sheet' in wb.sheetnames: if 'Sheet' in wb.sheetnames:
del wb['Sheet'] del wb['Sheet']