14 Commits

Author SHA1 Message Date
kkfluous
e814013320 V3.0.0 叠加客户亏损筛选,生成最终发放记录
新增功能:
- 读取1月/2月亏损表,按客户名称匹配考核数据
- 车辆考核追踪新增列:客户名称、客户是否亏损、考核应发、最终发放、未发放原因
- 月汇总新增亏损筛选section:亏损拦截/未匹配/最终发放/汇总
- 3月无亏损表,全部正常发放
- 亏损拦截不补发

规则:客户亏损→该客户下所有车不发;未匹配→标注待人工确认

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:14:37 +08:00
kkfluous
26361f3a95 fix: 交替行底色改为白/浅蓝,对比更明显
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:27:21 +08:00
kkfluous
bdbc3e4273 feat: 车辆追踪交替行底色(按车牌分组换色)
奇数车牌白底,偶数车牌浅灰底,多人同车保持同色。
车辆信息列用更深的灰色区分。达标/发放等特殊颜色覆盖底色。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:26:40 +08:00
kkfluous
1ca2657f75 feat: 车辆追踪隐藏BCD列(车架号/归属公司/车型)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:15:58 +08:00
kkfluous
5a69ef2993 V2.5.0 车辆追踪:多业务员拆行+车牌合并单元格
- 每个(车牌+业务员)独立一行,各自有独立的月度里程和累计
- 同车多人时车牌/车架号/归属公司等信息列合并单元格
- 奖金池(已发期数/金额/剩余)也合并(整车共享)
- 月度数据拆为应考核/实际/达标三列,不再挤在一个单元格
- 同人多条记录在达标列内换行显示各条

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:13:00 +08:00
kkfluous
04b6035d52 fix: 车辆追踪同人多条记录逐条显示,不合并里程
之前同人多条记录合并为一个总里程/总目标,但达标标记取"任一达标",
导致总里程<总目标却显示✓(如粤AGE4080: 1824/1839 ✓)。
改为每条记录独立显示里程/目标/达标,避免误导。
月度颜色也改为"全部达标才绿,否则红"。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:09:21 +08:00
kkfluous
4c43c00e73 feat: 车辆考核追踪sheet美化
- 达标月份绿底✓,未达标红底✗
- 累计达标/未达标加粗+颜色
- 有发放的行金底高亮
- 奖金池已发期数蓝底加粗,显示为"N/12"格式
- 车辆基本信息列浅灰底色区分
- 冻结首行+首列,支持滚动查看
- 开启自动筛选
- 优化列宽

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:02:50 +08:00
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
3 changed files with 474 additions and 128 deletions

View File

@@ -169,6 +169,57 @@ 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_loss_data(month):
"""读取亏损表,返回 {客户名称: ''/''} 字典。无亏损表返回None"""
import os
fp = f'{month}月.xlsx'
if not os.path.exists(fp):
return None
wb = openpyxl.load_workbook(fp, data_only=True)
ws = wb[wb.sheetnames[0]]
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
# 1月: 列名"项目","1月是否亏损"
# 2月: 列名"客户名称","是否亏损"
client_col = None; loss_col = None
for i, col_name in enumerate(h):
if col_name and ('项目' in str(col_name) or '客户' in str(col_name)):
client_col = i
if col_name and '亏损' in str(col_name):
loss_col = i
if client_col is None or loss_col is None:
wb.close()
return None
result = {}
for row in ws.iter_rows(min_row=2, values_only=True):
client = row[client_col]
loss = row[loss_col]
if client:
result[str(client).strip()] = str(loss).strip() if loss else ''
wb.close()
return result
def get_vehicle_client_map(D):
"""从考核源数据构建 {车牌号: 客户名称} 映射(取最新月的客户名)"""
plate_client = {}
for m in sorted(D.keys()):
for r in D[m]:
plate_client[r['车牌号']] = r.get('客户名称', '')
return plate_client
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

@@ -162,14 +162,44 @@ def write_calc_process_mar(wb, G1, G2, G3, feb_data):
c5,R(cbp3) if cbp3>0 else 0,R(total) if total>0 else 0]); rn+=1 c5,R(cbp3) if cbp3>0 else 0,R(total) if total>0 else 0]); rn+=1
AW(ws) AW(ws)
def write_summary_jan(wb, records): def write_summary_jan(wb, records, loss_data=None, plate_client=None):
ws = wb.create_sheet('1月汇总') ws = wb.create_sheet('1月汇总')
jan_dl=[{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']} for r in records if r['是否达标']=='达标'] jan_dl=[{'车牌':r['车牌号'],'销售':r['销售经理'],'部门':r['部门名称'],'':r['奖金']} for r in records if r['是否达标']=='达标']
rn=write_sec(ws,1,'1月达标奖励',jan_dl) 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}) write_total(ws,rn,1,{'达标':jan_dl})
AW(ws) AW(ws)
def write_summary_month(wb, month, month_data, section_names): def write_summary_month(wb, month, month_data, section_names, loss_data=None, plate_client=None):
ws = wb.create_sheet(f'{month}月汇总') ws = wb.create_sheet(f'{month}月汇总')
rn=1 rn=1
for i,cat in enumerate(section_names): for i,cat in enumerate(section_names):
@@ -178,7 +208,47 @@ def write_summary_month(wb, month, month_data, section_names):
'当月':f'{"" if month==2 else ""}{month}月当月奖励', '当月':f'{"" if month==2 else ""}{month}月当月奖励',
f'累计补发{month}':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,[])) rn=write_sec(ws,rn,label_map.get(cat,cat),month_data.get(cat,[]))
# 考核应发合计
write_total(ws,rn,month,month_data) 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) AW(ws)
# ============================================================ # ============================================================
@@ -186,111 +256,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
mkm=gm['目标km']
# 发放金额和说明(动态生成,包含关键数字)
pay_amt=0; desc=''
if m == settle_month:
carry=gm.get('结转',0); bonus=gm.get('当月奖金',0)
cum_bp=gm.get(f'累计补发{m}',0)
excess=sum(r['多跑'] for r in gm['recs'] if r['是否达标']=='达标')
if carry>0:
# 结转:上月多跑够整月
prev=m-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
desc=f'{prev}月多跑{R(prev_excess,0)}{mkm},结转(完整月奖金)'
pay_amt=carry
elif bonus>0:
desc='当月达标'
if excess>0 and int(excess//mkm)>=1:
desc+=f',多跑{R(excess,0)}{mkm}可结转'
pay_amt=bonus
elif cum_bp>0:
desc=f'1-{m}月累计{R(cum_a,0)}{R(cum_t,0)},累计达标补发'
pay_amt=cum_bp
elif gm.get('结转占位'):
desc='结转占位,本月不另发'
else: else:
desc=f'未达标(实际{R(a,0)}<目标{R(t,0)})' row.append('-')
else:
# 历史月份
if gm['有达标']:
pay_amt=gm.get('奖金',0)
desc=f'({m}月已发{R(pay_amt)})'
else:
desc=f'未达标(实际{R(a,0)}<目标{R(t,0)})'
row_data=[f'{m}',gm['天数'],R(t),R(a),f'{rate}%',qual,R(pay_amt) if pay_amt>0 else 0,desc]
if settle_month>=2:
cum_q='达标' if (cum_a>=cum_t and cum_t>0) else '未达标'
row_data+=[R(cum_t),R(cum_a),cum_q]
WR(ws,rn,row_data); rn+=1
# 补发行(当前月补发之前月份)
if settle_month >= 2: 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), {}) 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:
desc=f'1-{settle_month}累计{R(cum_a,0)}{R(cum_t,0)},补发{prev_m}' WR(ws, rn, ['', f'补发{prev_m}', R(bp_amt), f'累计{R(cum_a,0)}{R(cum_t,0)}达标,补发{prev_m}', ''])
row_data = [f'→补发{prev_m}','','','','','',R(bp_amt),desc] plate_this_month += bp_amt; rn += 1
if settle_month>=2: row_data += ['','','']
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)
@@ -300,48 +409,218 @@ def write_salesperson_sheet(wb, person, dept, settle_month, D, G, month_data, ve
# 新增车辆考核追踪sheet # 新增车辆考核追踪sheet
# ============================================================ # ============================================================
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, loss_data=None, plate_client=None):
ws = wb.create_sheet('车辆考核追踪') ws = wb.create_sheet('车辆考核追踪')
headers = ['车牌号','车架号','归属公司','车型','考核目标','月度奖励金额'] 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): for m in range(1, settle_month+1):
headers += [f'{m}业务员',f'{m}应考核',f'{m}月实际',f'{m}月达标'] headers += [f'{m}月应考核', f'{m}月实际', f'{m}月达标']
if settle_month >= 2: if settle_month >= 2:
headers += ['累计应完成','累计实际','累计达标'] headers += ['累计应完成','累计实际','累计达标']
headers += ['本月发放金额','发放给谁','发放类型','累计已发期数','累计已发金额','剩余可发期数'] if loss_data is not None:
headers += ['客户名称','客户是否亏损','考核应发','最终发放','未发放原因']
else:
headers += ['本月发放金额','发放类型']
headers += ['已发期数','已发金额','剩余期数']
WH(ws, headers) WH(ws, headers)
ws.freeze_panes = 'H2' # 冻结车辆信息+业务员列
rn=2 # 交替行底色
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: 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, [])
row=[plate,mv.get('车架号',''),mv.get('归属公司',''),mv.get('车型确定',''),
info.get('考核目标',''),info.get('月度奖励',0) or ''] target_name = info.get('考核目标','')
cum_t=0; cum_a=0 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 m in range(1, settle_month+1):
mgs=[(k,g) for k,g in G.get(m,{}).items() if k[0]==plate] for k,g in G.get(m, {}).items():
if mgs: if k[0] == plate:
persons=', '.join(sorted(set(g['销售'] for _,g in mgs))) person_set[k[1]] = g['部门']
ts=sum(g['应考核'] for _,g in mgs); As=sum(g['实际'] for _,g in mgs) persons = sorted(person_set.keys())
q='达标' if any(g['有达标'] for _,g in mgs) else '未达标' if not persons:
cum_t+=ts; cum_a+=As persons = [''] # 无考核记录也占一行
row+=[persons,R(ts),R(As),q]
else:
row+=['','','','']
if settle_month>=2:
row+=[R(cum_t),R(cum_a),'达标' if (cum_a>=cum_t and cum_t>0) else '未达标']
# 本月发放截至settle_month row_stripe = stripe_b if vehicle_idx % 2 == 1 else stripe_a
tp=[p for p in pays if p['结算月']==settle_month] 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: if tp:
row+=[R(sum(p['金额'] for p in tp)),', '.join(sorted(set(p['业务员'] for p in tp))), ws.cell(row=rn, column=pay_col).fill = gold_fill
', '.join(sorted(set(p['类型'] for p in tp)))]
else:
row+=[0,'','']
# 奖金池截至settle_month # 奖金池蓝底
pays_to_date=[p for p in pays if p['结算月']<=settle_month] if pi == 0 and tp_count > 0:
tp_count=len(pays_to_date); tp_amt=sum(p['金额'] for p in pays_to_date) pool_col = pay_col + 2
row+=[tp_count,R(tp_amt) if tp_amt>0 else 0,12-tp_count] ws.cell(row=rn, column=pool_col).fill = blue_fill
WR(ws,rn,row); rn+=1 ws.cell(row=rn, column=pool_col).font = Font(bold=True)
AW(ws)
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

36
main.py
View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""按月生成独立Excel文件 - 里程考核绩效""" """按月生成独立Excel文件 - 里程考核绩效(含亏损筛选)"""
import os, openpyxl import os, openpyxl
from calc_engine import * from calc_engine import *
from excel_writer import * from excel_writer import *
@@ -23,6 +23,22 @@ mar_data = calc_mar(G[1], G[2], G[3], feb_data)
vehicle_payments, vehicle_info = collect_vehicle_payments(G, feb_data, mar_data) vehicle_payments, vehicle_info = collect_vehicle_payments(G, feb_data, mar_data)
master_vehicles = read_master_vehicles() master_vehicles = read_master_vehicles()
# 亏损表
print("\n读取亏损表...")
loss_data = {}
for m in [1,2,3]:
ld = read_loss_data(m)
if ld:
loss_clients = sum(1 for v in ld.values() if v == '')
print(f" {m}月: {len(ld)}个客户, 其中亏损{loss_clients}")
loss_data[m] = ld
else:
print(f" {m}月: 无亏损表")
loss_data[m] = None
# 车牌→客户名称映射
plate_client = get_vehicle_client_map(D)
# 所有业务员 # 所有业务员
all_persons = {} all_persons = {}
for m in G: for m in G:
@@ -33,8 +49,8 @@ for m in G:
jan_total = sum(r['奖金'] for r in D[1] if r['是否达标']=='达标') jan_total = sum(r['奖金'] for r in D[1] if r['是否达标']=='达标')
feb_total = sum(sum(d[''] for d in v) for v in feb_data.values()) feb_total = sum(sum(d[''] for d in v) for v in feb_data.values())
mar_total = sum(sum(d[''] for d in v) for v in mar_data.values()) mar_total = sum(sum(d[''] for d in v) for v in mar_data.values())
print(f"\n1月: {jan_total:.2f}, 2月: {feb_total:.2f}, 3月: {mar_total:.2f}") print(f"\n考核应发: 1月{jan_total:.2f}, 2月{feb_total:.2f}, 3月{mar_total:.2f}")
print(f"Q1计: {jan_total + feb_total + mar_total:.2f}") print(f"Q1考核应发合计: {jan_total + feb_total + mar_total:.2f}")
# ============================================================ # ============================================================
# 按月生成 # 按月生成
@@ -68,19 +84,19 @@ for settle_month in [1, 2, 3]:
# Sheet 4: 汇总 # Sheet 4: 汇总
if settle_month == 1: if settle_month == 1:
write_summary_jan(wb, D[1]) write_summary_jan(wb, D[1], loss_data[1], plate_client)
elif settle_month == 2: elif settle_month == 2:
write_summary_month(wb, 2, feb_data, ['结转','补发1月','当月','累计补发2月']) write_summary_month(wb, 2, feb_data, ['结转','补发1月','当月','累计补发2月'], loss_data[2], plate_client)
else: else:
write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月']) write_summary_month(wb, 3, mar_data, ['结转','补发1月','补发2月','当月','累计补发3月'], loss_data[3], plate_client)
# Sheet 5-16: 业务员 # Sheet 5: 车辆考核追踪
write_vehicle_tracking_sheet(wb, settle_month, G, master_vehicles, vehicle_payments, vehicle_info, loss_data[settle_month], plate_client)
# 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']