370 lines
14 KiB
Python
370 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
6月 18T 绩效考核 — 方案3 (800/300 当月, 800结转, 500补发/累计)
|
||
"""
|
||
|
||
import os, openpyxl, unicodedata
|
||
from collections import defaultdict
|
||
|
||
os.chdir('/Users/kkfluous/Downloads')
|
||
|
||
DAYS = {1:31, 2:28, 3:31, 4:30, 5:31, 6:30}
|
||
TARGET_KM = 6000
|
||
BASE_GOOD = 800 # 当月达标
|
||
BASE_BAD = 300 # 当月未达标
|
||
BASE_CARRY = 800 # 结转
|
||
BASE_OTHER = 500 # 补发/累计补发
|
||
OUTPUT_DIR = '/Users/kkfluous/Downloads/6月18T测算'
|
||
|
||
# ========== 数据读取 ==========
|
||
|
||
def read_18t(filepath, month):
|
||
wb = openpyxl.load_workbook(filepath, data_only=True)
|
||
ws = wb['业务考核视图']
|
||
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
|
||
recs = []
|
||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||
r = dict(zip(h, row))
|
||
if r.get('考核目标') != '羚牛100辆18T':
|
||
continue
|
||
r['考核天数'] = float(r['考核天数'] or 0)
|
||
r['应考核里程(km)'] = float(r['应考核里程(km)'] or 0)
|
||
r['实际行驶里程(km)'] = float(r['实际行驶里程(km)'] or 0)
|
||
r['月份'] = month
|
||
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
|
||
q = a >= t and t > 0
|
||
r['是否达标'] = '达标' if q else '未达标'
|
||
r['多跑'] = max(0, a - t) if q else 0
|
||
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
|
||
recs.append(r)
|
||
wb.close()
|
||
return recs
|
||
|
||
|
||
def grp(recs):
|
||
gs = defaultdict(lambda: {
|
||
'recs': [], '应考核': 0, '实际': 0, '奖金': 0, '天数': 0,
|
||
'有达标': False, '部门': '', '销售': '', '车牌': ''
|
||
})
|
||
for r in recs:
|
||
k = (r['车牌号'], r['销售经理'])
|
||
g = gs[k]
|
||
g['recs'].append(r)
|
||
g['应考核'] += r['应考核里程(km)']
|
||
g['实际'] += r['实际行驶里程(km)']
|
||
g['奖金'] += r.get('奖金', 0)
|
||
g['天数'] += r['考核天数']
|
||
if r['是否达标'] == '达标':
|
||
g['有达标'] = True
|
||
g['部门'] = r['部门名称']
|
||
g['销售'] = r['销售经理']
|
||
g['车牌'] = r['车牌号']
|
||
return dict(gs)
|
||
|
||
|
||
# ========== 计算 ==========
|
||
|
||
def calc():
|
||
D = {}
|
||
for m in range(1, 7):
|
||
fp = f'租赁任务考核_2026年{m}月.xlsx'
|
||
if os.path.exists(fp):
|
||
D[m] = read_18t(fp, m)
|
||
|
||
# 给每条记录算奖金
|
||
for m in D:
|
||
for r in D[m]:
|
||
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
|
||
q = a >= t and t > 0
|
||
r['是否达标'] = '达标' if q else '未达标'
|
||
if q:
|
||
r['奖金'] = BASE_GOOD * (r['考核天数'] / DAYS[m])
|
||
else:
|
||
r['奖金'] = BASE_BAD * (r['考核天数'] / DAYS[m])
|
||
r['多跑'] = max(0, a - t) if q else 0
|
||
r['可结转'] = int(r['多跑'] // TARGET_KM) if q else 0
|
||
|
||
G = {m: grp(D[m]) for m in D}
|
||
|
||
# 1月: 初始化
|
||
for k, g in G[1].items():
|
||
cum_q = g['实际'] >= g['应考核'] and g['应考核'] > 0
|
||
cum_over = max(0, g['实际'] - g['应考核']) if cum_q else 0
|
||
g['可结转'] = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
|
||
g['累计已发结转'] = 0
|
||
g['1月已发'] = g['有达标']
|
||
g['cum_t'] = g['应考核']
|
||
g['cum_a'] = g['实际']
|
||
g['cum_q'] = cum_q
|
||
|
||
# 逐月计算 2→6
|
||
for month in range(2, 7):
|
||
if month not in G:
|
||
continue
|
||
prev_m = month - 1
|
||
days_m = DAYS[month]
|
||
|
||
for k, gm in G[month].items():
|
||
gp = G[prev_m].get(k)
|
||
|
||
cum_t = (gp['cum_t'] if gp else 0) + gm['应考核']
|
||
cum_a = (gp['cum_a'] if gp else 0) + gm['实际']
|
||
cum_q = cum_a >= cum_t and cum_t > 0
|
||
|
||
# 结转: 800
|
||
carry = 0
|
||
p_carry = gp['可结转'] if gp else 0
|
||
if gp and p_carry >= 1:
|
||
carry = BASE_CARRY
|
||
|
||
# 当月奖金
|
||
gm['当月奖金'] = gm['奖金'] if carry == 0 else 0
|
||
|
||
# 补发历史月: 500
|
||
for back_m in range(1, month):
|
||
g_bm = G[back_m].get(k)
|
||
if not g_bm:
|
||
continue
|
||
bm_paid = g_bm.get(f'{back_m}月已发', g_bm['有达标'])
|
||
if bm_paid:
|
||
continue
|
||
bm_paid_later = False
|
||
for later_m in range(back_m + 1, month):
|
||
gl = G[later_m].get(k)
|
||
if gl and gl.get(f'补发{back_m}月', 0) > 0:
|
||
bm_paid_later = True
|
||
break
|
||
if bm_paid_later:
|
||
continue
|
||
if cum_q:
|
||
bj = BASE_OTHER * (g_bm['天数'] / DAYS[back_m])
|
||
gm[f'补发{back_m}月'] = bj
|
||
|
||
# 累计补发当月: 500
|
||
cbp = 0
|
||
if not gm['有达标'] and carry == 0 and cum_q:
|
||
cbp = BASE_OTHER * (gm['天数'] / days_m)
|
||
gm['累计补发当月'] = cbp
|
||
|
||
gm['cum_t'] = cum_t
|
||
gm['cum_a'] = cum_a
|
||
gm['cum_q'] = cum_q
|
||
gm['结转'] = carry
|
||
gm[f'{month}月已发'] = carry > 0 or gm['有达标'] or cbp > 0
|
||
|
||
# 更新可结转
|
||
cum_over = max(0, cum_a - cum_t) if cum_q else 0
|
||
earned = int(cum_over // TARGET_KM) if TARGET_KM > 0 else 0
|
||
prev_used = gp['累计已发结转'] if gp else 0
|
||
cur_used = prev_used + (1 if carry > 0 else 0)
|
||
gm['累计已发结转'] = cur_used
|
||
gm['可结转'] = max(0, earned - cur_used)
|
||
|
||
# 补充: 上月有结转但本月无考核记录的车
|
||
for k, gp in G[prev_m].items():
|
||
if k not in G[month] and gp.get('可结转', 0) >= 1:
|
||
G[month][k] = {
|
||
'recs': [], '应考核': TARGET_KM, '实际': 0, '奖金': 0, '天数': days_m,
|
||
'有达标': False, '部门': gp['部门'], '销售': gp['销售'], '车牌': k[0],
|
||
'cum_t': gp.get('cum_t', 0) + TARGET_KM, 'cum_a': gp.get('cum_a', 0),
|
||
'cum_q': False,
|
||
'结转': BASE_CARRY, '当月奖金': 0,
|
||
f'{month}月已发': True,
|
||
'可结转': max(0, gp['可结转'] - 1),
|
||
'累计已发结转': gp.get('累计已发结转', 0) + 1,
|
||
'虚拟': True,
|
||
}
|
||
|
||
# 收集6月发放记录
|
||
pay_records = collect_payments(G, 6)
|
||
|
||
return D[6], G[6], pay_records
|
||
|
||
|
||
def collect_payments(G, month):
|
||
G_m = G[month]
|
||
records = []
|
||
for k, g in sorted(G_m.items()):
|
||
if g.get('虚拟'):
|
||
continue
|
||
carry = g.get('结转', 0)
|
||
if carry > 0:
|
||
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
|
||
'金额': carry, '类型': '结转(800)'})
|
||
|
||
bonus = g.get('当月奖金', 0)
|
||
if bonus > 0:
|
||
ptype = '当月达标(800)' if g['有达标'] else '当月未达标(300)'
|
||
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
|
||
'金额': bonus, '类型': ptype})
|
||
|
||
cbp = g.get('累计补发当月', 0)
|
||
if cbp > 0:
|
||
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
|
||
'金额': cbp, '类型': f'累计补发{month}月(500)'})
|
||
|
||
for bm in range(1, month):
|
||
bv = g.get(f'补发{bm}月', 0)
|
||
if bv > 0:
|
||
records.append({'车牌': k[0], '销售': k[1], '部门': g['部门'],
|
||
'金额': bv, '类型': f'补发{bm}月(500)'})
|
||
|
||
records.sort(key=lambda x: (x['车牌'], x['类型']))
|
||
return records
|
||
|
||
|
||
# ========== Excel输出 ==========
|
||
|
||
def set_widths(ws, widths):
|
||
for i, w in enumerate(widths, 1):
|
||
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
|
||
|
||
|
||
def write_xlsx(recs_6, G6, pay_records, filepath):
|
||
wb = openpyxl.Workbook()
|
||
|
||
# Sheet 1: 规则
|
||
ws = wb.active
|
||
ws.title = '考核规则'
|
||
ws.append(['6月 18T 绩效考核 — 方案3'])
|
||
ws.append(['考核目标', '羚牛100辆18T'])
|
||
ws.append(['月度目标里程', '6000km'])
|
||
ws.append(['当月达标', '800元 × (考核天数/30天)'])
|
||
ws.append(['当月未达标', '300元 × (考核天数/30天)'])
|
||
ws.append(['结转奖励', '800元/次 (消耗1个可结转月)'])
|
||
ws.append(['补发历史月', '500元 × (历史月天数/历史总天数)'])
|
||
ws.append(['累计补发当月', '500元 × (当月天数/30天)'])
|
||
ws.append(['规则', '当月达标800/未达标300 → 结转800 → 补发500 → 累计补发500'])
|
||
ws.append(['亏损筛选', '6月无盈亏表,不管亏损全发'])
|
||
set_widths(ws, [30, 60])
|
||
|
||
# Sheet 2: 里程明细
|
||
ws2 = wb.create_sheet('6月里程明细')
|
||
ws2.append(['车牌号', '销售经理', '部门', '客户', '考核天数', '应考核里程', '实际里程', '是否达标', '月度目标'])
|
||
for r in recs_6:
|
||
ws2.append([r['车牌号'], r['销售经理'], r.get('部门名称', ''),
|
||
r.get('客户名称', ''), r['考核天数'], r['应考核里程(km)'],
|
||
r['实际行驶里程(km)'], r['是否达标'], TARGET_KM])
|
||
set_widths(ws2, [14, 10, 30, 35, 10, 12, 12, 10, 10])
|
||
|
||
# Sheet 3: 计算过程
|
||
ws3 = wb.create_sheet('6月计算过程')
|
||
ws3.append(['车牌号', '销售经理', '部门', '6月应考核', '6月实际', '6月达标',
|
||
'累计应考核', '累计实际', '累计达标', '可结转', '结转发放(800)',
|
||
'当月奖金', '累计补发6月(500)', '补发1月', '补发2月', '补发3月', '补发4月', '补发5月',
|
||
'6月总发放'])
|
||
for k, g in sorted(G6.items()):
|
||
if g.get('虚拟'):
|
||
continue
|
||
cum_q_str = '是' if g.get('cum_q') else '否'
|
||
dq_str = '是' if g['有达标'] else '否'
|
||
total = (g.get('结转', 0) + g.get('当月奖金', 0) + g.get('累计补发当月', 0) +
|
||
sum(g.get(f'补发{m}月', 0) for m in range(1, 6)))
|
||
ws3.append([k[0], k[1], g['部门'], g['应考核'], g['实际'], dq_str,
|
||
g.get('cum_t', ''), g.get('cum_a', ''), cum_q_str,
|
||
g.get('可结转', 0), g.get('结转', 0),
|
||
g.get('当月奖金', 0), g.get('累计补发当月', 0),
|
||
g.get('补发1月', 0), g.get('补发2月', 0), g.get('补发3月', 0),
|
||
g.get('补发4月', 0), g.get('补发5月', 0), total])
|
||
set_widths(ws3, [14, 10, 30, 12, 12, 10, 12, 12, 10, 10, 14, 12, 16, 10, 10, 10, 10, 10, 14])
|
||
|
||
# Sheet 4: 发放明细
|
||
ws4 = wb.create_sheet('6月发放明细')
|
||
ws4.append(['车牌号', '销售经理', '部门', '金额', '发放类型'])
|
||
for r in pay_records:
|
||
ws4.append([r['车牌'], r['销售'], r['部门'], round(r['金额'], 2), r['类型']])
|
||
total = sum(r['金额'] for r in pay_records)
|
||
ws4.append([])
|
||
ws4.append(['', '', '合计', round(total, 2), ''])
|
||
set_widths(ws4, [14, 10, 30, 12, 22])
|
||
|
||
# Sheet 5: 汇总
|
||
ws5 = wb.create_sheet('汇总')
|
||
ws5.append(['6月18T 方案3 汇总 (800/300, 结转800, 补发500)'])
|
||
ws5.append(['发放类型', '辆数', '金额合计'])
|
||
cat_sum, cat_cnt = defaultdict(float), defaultdict(int)
|
||
for r in pay_records:
|
||
cat_sum[r['类型']] += r['金额']
|
||
cat_cnt[r['类型']] += 1
|
||
all_cats = ['当月达标(800)', '当月未达标(300)', '结转(800)', '累计补发6月(500)',
|
||
'补发1月(500)', '补发2月(500)', '补发3月(500)', '补发4月(500)', '补发5月(500)']
|
||
for cat in all_cats:
|
||
if cat in cat_sum:
|
||
ws5.append([cat, cat_cnt[cat], round(cat_sum[cat], 2)])
|
||
ws5.append(['合计', sum(cat_cnt.values()), round(sum(cat_sum.values()), 2)])
|
||
set_widths(ws5, [22, 10, 15])
|
||
|
||
wb.save(filepath)
|
||
print(f' ✅ {filepath}')
|
||
|
||
|
||
# ========== 主流程 ==========
|
||
|
||
def main():
|
||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||
|
||
print('计算6月18T方案3...')
|
||
recs_6, G6, pay_records = calc()
|
||
|
||
# 统计
|
||
total_groups = sum(1 for g in G6.values() if not g.get('虚拟'))
|
||
good_groups = sum(1 for g in G6.values() if g['有达标'] and not g.get('虚拟'))
|
||
bad_groups = sum(1 for g in G6.values() if not g['有达标'] and not g.get('虚拟'))
|
||
carry_groups = sum(1 for g in G6.values() if g.get('结转', 0) > 0)
|
||
|
||
print(f' 6月18T组数: {total_groups}, 达标: {good_groups}, 未达标: {bad_groups}, 结转: {carry_groups}')
|
||
|
||
# 分类汇总
|
||
def cats(dl, *keys):
|
||
cnt = sum(sum(1 for r in dl if r['类型']==k) for k in keys)
|
||
amt = sum(sum(r['金额'] for r in dl if r['类型']==k) for k in keys)
|
||
return cnt, amt
|
||
|
||
def cell(amt, cnt):
|
||
if cnt == 0: return '—'
|
||
return f'{amt:,.2f} ({cnt}辆)'
|
||
|
||
rows = [
|
||
('当月达标(800)', cats(pay_records, '当月达标(800)')),
|
||
('当月未达标(300)', cats(pay_records, '当月未达标(300)')),
|
||
('结转(800)', cats(pay_records, '结转(800)')),
|
||
('累计补发6月(500)', cats(pay_records, '累计补发6月(500)')),
|
||
('补发1月(500)', cats(pay_records, '补发1月(500)')),
|
||
('补发2月(500)', cats(pay_records, '补发2月(500)')),
|
||
('补发3月(500)', cats(pay_records, '补发3月(500)')),
|
||
('补发4月(500)', cats(pay_records, '补发4月(500)')),
|
||
('补发5月(500)', cats(pay_records, '补发5月(500)')),
|
||
]
|
||
grand_total = sum(r['金额'] for r in pay_records)
|
||
|
||
def vw(s):
|
||
return sum(2 if unicodedata.east_asian_width(c) in ('F','W') else 1 for c in str(s))
|
||
def padr(s, w):
|
||
return ' ' * (w - vw(s)) + str(s)
|
||
def padl(s, w):
|
||
return str(s) + ' ' * (w - vw(s))
|
||
|
||
W0, W1 = 17, 26
|
||
print()
|
||
print(' 6月 18T 绩效考核 — 方案3 (800/300, 结转800, 补发500)')
|
||
print(f" ┌{'─'*W0}┬{'─'*W1}┐")
|
||
for label, (cnt, amt) in rows:
|
||
if amt > 0 or ('累计补发' in label) or ('补发' in label):
|
||
c = cell(amt, cnt)
|
||
else:
|
||
c = cell(amt, cnt)
|
||
if amt > 0 or cnt > 0:
|
||
print(f" │{padl(label,W0)}│{padr(c,W1)}│")
|
||
# separator
|
||
print(f" ├{'─'*W0}┼{'─'*W1}┤")
|
||
print(f" │{padl('合计',W0)}│{padr(f'{grand_total:,.2f}',W1)}│")
|
||
print(f" └{'─'*W0}┴{'─'*W1}┘")
|
||
|
||
# 写Excel
|
||
write_xlsx(recs_6, G6, pay_records, os.path.join(OUTPUT_DIR, '6月18T_方案3.xlsx'))
|
||
print(f"\n文件输出到: {OUTPUT_DIR}/")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|