Files
mileage-bonus/calc_engine.py
T

304 lines
13 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.
"""计算引擎:规则、读取、分组、结转/补发/累计逻辑"""
import openpyxl
from collections import defaultdict
# RULES: 18T用(1000/0/1000/1000), 非18T保持原逻辑(未达标不发, 结转补发=原奖励额)
RULES = {
'交投40辆4.5T普货': {'km': 3000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150},
'交投190辆4.5T冷链车': {'km': 3000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150},
'羚牛136辆4.5T冷链车': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'恒运50辆4.5T普货': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'恒运50辆4.5T 普货': {'km': 5000, 'good': 260, 'bad': 0, 'carry': 260, 'other': 260},
'羚牛100辆18T': {'km': 6000, 'good': 1000, 'bad': 0, 'carry': 1000, 'other': 1000},
}
# 2026年7月经业务确认的4辆18T执行下半年新政策:
# 月目标5000公里,达标150元,未达标不发,结转/补发按150元基准。
# 其余18T继续使用上面的原口径。
JULY_SCHEME3_PLATES = {'粤AGR0772', '粤AGQ0178', '粤AGP6675', '粤AGP9757'}
JULY_SCHEME3_RULE = {'km': 5000, 'good': 150, 'bad': 0, 'carry': 150, 'other': 150}
DAYS = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
def read_file(fp, month):
wb = openpyxl.load_workbook(fp, 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))
r['考核天数'] = float(r.get('考核天数') or 0)
r['应考核里程(km)'] = float(r.get('应考核里程(km)') or 0)
r['实际行驶里程(km)'] = float(r.get('实际行驶里程(km)') or 0)
r['月份'] = month
if month == 7 and str(r.get('车牌号') or '').strip().upper() in JULY_SCHEME3_PLATES:
rule = JULY_SCHEME3_RULE
else:
rule = RULES.get(r.get('考核目标', ''), {'km': 0, 'good': 0, 'bad': 0, 'carry': 0, 'other': 0})
r['月度目标里程'] = rule.get('km', 0)
r['奖励基准_达标'] = rule.get('good', 0)
r['奖励基准_未达标'] = rule.get('bad', 0)
r['奖励基准_结转'] = rule.get('carry', 0)
r['奖励基准_补发'] = rule.get('other', 0)
t, a = r['应考核里程(km)'], r['实际行驶里程(km)']
q = a >= t and t > 0
r['是否达标'] = '达标' if q else '未达标'
if q:
r['奖金'] = r['奖励基准_达标'] * (r['考核天数'] / DAYS[month])
else:
r['奖金'] = r['奖励基准_未达标'] * (r['考核天数'] / DAYS[month])
r['多跑'] = max(0, a - t) if q else 0
r['可结转'] = int(r['多跑'] // r['月度目标里程']) if r['月度目标里程'] > 0 and q else 0
recs.append(r)
wb.close()
return recs
def grp(recs, month):
gs = defaultdict(lambda: {'recs':[], '应考核':0, '实际':0, '奖金':0, '天数':0,
'有达标':False, '目标km':0, '奖励达标':0, '奖励未达标':0,
'奖励结转':0, '奖励补发':0})
for r in recs:
k = (r['车牌号'], r['销售经理'])
g = gs[k]
g['recs'].append(r)
g['应考核'] += r['应考核里程(km)']
g['实际'] += r['实际行驶里程(km)']
g['奖金'] += r['奖金']
g['天数'] += r['考核天数']
if r['是否达标'] == '达标': g['有达标'] = True
g['目标km'] = max(g['目标km'], r['月度目标里程'])
g['奖励达标'] = max(g['奖励达标'], r['奖励基准_达标'])
g['奖励未达标'] = max(g['奖励未达标'], r['奖励基准_未达标'])
g['奖励结转'] = max(g['奖励结转'], r['奖励基准_结转'])
g['奖励补发'] = max(g['奖励补发'], r['奖励基准_补发'])
g['部门'] = r['部门名称']
g['销售'] = r['销售经理']
g['车牌'] = r['车牌号']
return dict(gs)
def calc_jan_carryover(G1):
for k, g in G1.items():
tc = 0
for r in g['recs']:
if r['是否达标'] == '达标': tc += r['可结转']
g['可结转'] = tc
g['累计已发结转'] = 0
g['1月已发'] = g['有达标'] # 当月有达标 → 已发
g['cum_t'] = g['应考核']
g['cum_a'] = g['实际']
g['cum_q'] = g['实际'] >= g['应考核'] and g['应考核'] > 0
def calc_month(month, G_all, all_data, G_prev, G_cur):
"""通用月计算: 结转/补发/累计补发/当月"""
prev_m = month - 1
days_m = DAYS[month]
data_m = {'结转': [], '当月': []}
# 向后兼容的key: 当月达标/当月未达标 用于18T区分
data_18t = {'当月达标': [], '当月未达标': []}
use_18t = True # 统一使用拆分标签
for k, gm in G_cur.items():
gp = G_prev.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
# 结转: carry × 1 (整月, 不折算)
carry = 0
p_carry = gp['可结转'] if gp else 0
if gp and p_carry >= 1:
carry = gm['奖励结转']
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gm['部门'], '额': carry})
# 当月奖金
is_18t = gm['奖励达标'] == 1000 # 18T特征
bonus = 0
if carry == 0 and gm['奖金'] > 0:
bonus = gm['奖金']
data_m['当月'].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '额': bonus})
# 补发历史月: other × 折算
for back_m in range(1, month):
label = f'补发{back_m}月'
if label not in data_m:
data_m[label] = []
g_bm = G_all[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_all[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 = g_bm['奖励补发'] * (g_bm['天数'] / DAYS[back_m])
data_m[label].append({'车牌': k[0], '销售': g_bm['销售'], '部门': gm['部门'], '额': bj})
gm[f'补发{back_m}月'] = bj
# 累计补发当月: other × 折算
label_cbp = f'累计补发{month}月'
if label_cbp not in data_m:
data_m[label_cbp] = []
cbp = 0
if not gm['有达标'] and carry == 0 and cum_q:
cbp = gm['奖励补发'] * (gm['天数'] / days_m)
data_m[label_cbp].append({'车牌': k[0], '销售': gm['销售'], '部门': gm['部门'], '额': cbp})
gm['cum_t'] = cum_t
gm['cum_a'] = cum_a
gm['cum_q'] = cum_q
gm['结转'] = carry
gm['当月奖金'] = bonus
gm['累计补发当月'] = cbp
gm[f'{month}月已发'] = carry > 0 or gm['奖金'] > 0 or cbp > 0
# 更新可结转
cum_over = max(0, cum_a - cum_t) if cum_q else 0
earned = int(cum_over // gm['目标km']) if gm['目标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.items():
if k not in G_cur and gp.get('可结转', 0) >= 1:
carry = gp['奖励结转']
data_m['结转'].append({'车牌': k[0], '销售': gp['销售'], '部门': gp['部门'], '额': carry})
G_cur[k] = {
'recs': [], '应考核': gp['目标km'], '实际': 0, '奖金': 0, '天数': days_m,
'有达标': False, '目标km': gp['目标km'],
'奖励达标': gp['奖励达标'], '奖励未达标': gp['奖励未达标'],
'奖励结转': gp['奖励结转'], '奖励补发': gp['奖励补发'],
'部门': gp['部门'], '销售': gp['销售'], '车牌': k[0],
'cum_t': gp.get('cum_t', 0) + gp['目标km'], 'cum_a': gp.get('cum_a', 0),
'cum_q': False,
'结转': carry, '当月奖金': 0,
f'{month}月已发': True,
'可结转': max(0, gp['可结转'] - 1),
'累计已发结转': gp.get('累计已发结转', 0) + 1,
'虚拟': True,
}
all_data[month] = data_m
return data_m, G_cur
def collect_vehicle_payments(G, all_data):
"""收集所有月的付款记录"""
payments = defaultdict(list)
info = {}
# 1月
for k, g in G[1].items():
plate = k[0]
if not info.get(plate):
first = g['recs'][0]
info[plate] = {'考核目标': first.get('考核目标', ''), '月度奖励': g['奖励达标'], '目标km': g['目标km']}
if g['奖金'] > 0:
payments[plate].append({'结算月': 1, '对应考核月': 1, '业务员': g['销售'],
'金额': g['奖金'], '类型': '当月达标', '部门': g['部门']})
# 2月-7月
cat_map = {}
for month in range(2, 8):
cat_map[('结转', month)] = month
for bm in range(1, month):
cat_map[(f'补发{bm}月', month)] = bm
cat_map[('当月', month)] = month
cat_map[(f'累计补发{month}月', month)] = month
for month in range(2, 8):
mdata = all_data.get(month, {})
for cat, dl in mdata.items():
assess_month = cat_map.get((cat, month), month)
for d in dl:
payments[d['车牌']].append({'结算月': month, '对应考核月': assess_month,
'业务员': d['销售'], '金额': d['额'], '类型': cat, '部门': d['部门']})
for m in G:
for k, g in G[m].items():
if not info.get(k[0]):
first = g['recs'][0] if g['recs'] else None
if first:
info[k[0]] = {'考核目标': first.get('考核目标', ''), '月度奖励': g['奖励达标'], '目标km': g['目标km']}
for plate in payments:
records = sorted(payments[plate], key=lambda x: (x['对应考核月'], x['结算月']))
for i, r in enumerate(records): r['期数'] = i + 1
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))]
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'):
"""从现有Q1汇总文件读取全量车辆台账"""
wb = openpyxl.load_workbook(fp, data_only=True)
ws = wb['车辆奖金池总览']
h = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
vehicles = []
for row in ws.iter_rows(min_row=2, values_only=True):
r = dict(zip(h, row))
if r.get('车牌号'):
vehicles.append({'车牌号': r['车牌号'], '车架号': r.get('车架号', ''),
'归属公司': r.get('归属公司', ''), '车型确定': r.get('车型', '')})
wb.close()
return vehicles