Files
shishengliang 65904c8270 feat: add assessment workbench and September reconciliation
Preserve August hydrogen revision and September audit evidence; calculate interrupted vehicle history across all prior months. Record remaining carry-policy and layout-baseline discrepancies.
2026-10-09 18:21:45 +08:00

111 lines
11 KiB
Python
Raw Permalink 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.
"""September mileage selection and awards, with January-August settlements locked."""
from pathlib import Path
from collections import defaultdict,Counter
from decimal import Decimal,ROUND_HALF_UP
import json,sys,copy,re,hashlib
import openpyxl
P=Path(__file__).resolve().parent;REPO=P.parents[1];H=P.parent/'hydrogen-history-20260916'
sys.path.insert(0,str(REPO));import calc_engine as e
def money(v):return float(Decimal(str(v)).quantize(Decimal('.01'),rounding=ROUND_HALF_UP))
def load(p):return json.loads(p.read_text())
def dump(name,x):(P/name).write_text(json.dumps(x,ensure_ascii=False,indent=2,default=str))
prepared=load(P/'prepared_records.json');aug=load(H/'august_calculation.json')
confirmed=dict(test_rule='沿用8月:名单内每日仪表减GPS严格大于50公里为测试日,取GPS;非测试日优先纯氢(含0),无纯氢取仪表;名单内无法判定且无纯氢时兜底GPS',test_threshold_km=50,august_actual_paid=45065.47,overlaps='交接日计前段,后段次日起算',new_policy='沿用上月原始39辆名单,按9月车型档位匹配',missing='沿用8月按已有里程累计并单列缺口;不将缺失解释为实测0',history='1—6月修正后的里程继续承接;1—8月已发及扣减锁定,不重复补发、不追扣')
assert aug['stats']['amount']==45065.47
ap=H/'里程任务考核_8月核算_纯氢修正版.xlsx'
wb=openpyxl.load_workbook(ap,read_only=True,data_only=True)
assert wb['8月汇总(新版界面)']['M22'].value==45065.47
rr=list(wb['8月奖金发放记录'].values);wb.close()
aug_rows={(r[0],r[1],r[4]):r for r in rr[1:] if r[0] and r[0]!='合计' and isinstance(r[9],(int,float))}
for l in aug['ledger']:
r=aug_rows[l['plate'],l['sales'],l['category']];assert r[9]==l['amount'] and r[8]==l['withheld']
daily=prepared['daily'];basis=Counter();gaps=[];byrow=defaultdict(list)
for x in daily:
t,g,h=x['meter'],x['gps'],x['hydrogen'];flag=False
if x['test_candidate']:flag=(Decimal(str(t))-Decimal(str(g))>50) if t is not None and g is not None else None
if flag is True:mode,v='测试日GPS',g
elif h is not None:mode,v='非测试日纯氢',h
elif x['test_candidate'] and flag is None:mode,v='测试名单无纯氢GPS',g
elif t is not None:mode,v='非测试日仪表',t
else:mode,v='缺失',None
needed='GPS' if x['test_candidate'] and h is None and flag is None else '仪表'
x.update(test_flag=flag,comparison=money(t-g) if t is not None and g is not None else None,selection=mode if v is not None else '缺失',included=v,needed=needed if v is None else '',reason=('测试候选无纯氢且无法判定,需GPS' if needed=='GPS' else '无纯氢且无仪表,普通车辆不自动以GPS替代') if v is None else '')
if v is None:gaps.append(x)
basis[x['selection']]+=1;byrow[x['source_row']].append(x)
records=prepared['records']
for r in records:
if r['销售经理']=='刘念念':r['部门名称']='业务一部'
ds=byrow[r['源表行']];km=money(sum(Decimal(str(x['included'])) for x in ds if x['included'] is not None));gap=sum(x['included'] is None for x in ds)
target=r['应考核里程(km)'];reward=r['月奖励'];q=km>=target and target>0
r.update(月份=9,实际行驶里程_km=km,是否达标='达标' if q else '未达标',奖励基准_达标=reward,奖励基准_未达标=0,奖励基准_结转=reward,奖励基准_补发=reward,奖金=reward*r['考核天数']/30 if q else 0,多跑=max(0,km-target),可结转=int(max(0,km-target)//r['月度目标里程']),缺口天数=gap,状态='待补里程' if gap else '数据齐全',取数方式=';'.join(f'{k}{v}天' for k,v in Counter(x['selection'] for x in ds).items()))
r['实际行驶里程(km)']=km;r['完成率(%)']=money(km/target*100)
# Rebuild original history only for unchanged monthly target/reward/day metadata;
# then apply the saved corrected mileage and actual-settlement state.
D={m:e.read_file(REPO/f'租赁任务考核_2026年{m}月.xlsx' if m<7 else REPO/'7月绩效重算_多源考核里程_20260817/租赁任务考核_2026年7月.xlsx',m) for m in range(1,8)}
G={m:e.grp(D[m],m) for m in D};ad={};e.calc_jan_carryover(G[1])
for m in range(2,8):e.calc_month(m,G,ad,G[m-1],G[m])
for x in load(H/'comparison.json'):G[x['month']][(x['plate'],x['sales'])]['实际']=x['corrected']
states=load(H/'historical_states.json')
for s in states:
g=G[s['month']][(s['plate'],s['sales'])]
g.update(cum_t=s['cum_target'],cum_a=s['cum_actual'],cum_q=s['cum_actual']>=s['cum_target'] and s['cum_target']>0,累计已发结转=s['used'],可结转=s['available'])
settled={(r['assessment_month'],r['plate'],r['owner']) for r in load(H/'historical_paid.json')}
settled.update((l['assessment_month'],l['plate'],l['sales']) for l in aug['ledger'] if l['gross_amount']>0)
aug_carry={(l['plate'],l['sales']) for l in aug['ledger'] if l['category']=='结转' and l['amount']>0}
G[8]=e.grp(aug['records'],8)
for x in aug['groups']:
k=x['plate'],x['sales'];old=G[7].get(k,{})
if k not in G[8]:
assert x['virtual'];G[8][k]=dict(recs=[],应考核=x['target'],实际=x['actual'],奖金=0,天数=x['days'],有达标=False,目标km=old['目标km'],奖励达标=x['reward'],奖励未达标=0,奖励结转=x['reward'],奖励补发=x['reward'],部门=x['department'],销售=x['sales'],车牌=x['plate'],虚拟=True)
g=G[8][k]
g.update(cum_t=x['cum_target'],cum_a=x['cum_actual'],cum_q=x['cum_qualified'],可结转=x['carry_left'],累计已发结转=old.get('累计已发结转',0)+int(k in aug_carry))
for m in range(1,9):
for k,g in G[m].items():
g[f'{m}月已发']=(m,*k) in settled
for bm in range(1,m):g.pop(f'补发{bm}月',None)
before=copy.deepcopy(G)
G[9]=e.grp(records,9);data={};e.calc_month(9,G,data,G[8],G[9],cumulative_history=True)
policy=set(prepared['policy_plates']);price=aug['rules']['new_policy']['award_by_target']
for k,g in G[9].items():
if k[1]=='刘念念':g['部门']='业务一部'
if g.get('虚拟') and k[0] in policy:
last=next((r for m in range(8,0,-1) for r in (aug['records'] if m==8 else D[m]) if (r['车牌号'],r['销售经理'])==k),None)
assert last is not None;reward=price[last['考核目标']]
g.update(奖励达标=reward,奖励结转=reward,奖励补发=reward,结转=reward)
for item in data[9]['结转']:
if (item['车牌'],item['销售'])==k:item['额']=reward
departed=set(aug['departed']['people']);ledger=[];excluded=[]
cats=['当月','结转']+[f'补发{m}月' for m in range(1,9)]+['累计补发9月']
for cat in cats:
for item in data[9].get(cat,[]):
k=(item['车牌'],item['销售']);g=G[9][k];m=int(re.search(r'补发(\d+)月',cat).group(1)) if '补发' in cat else 9
if k[1] in departed:excluded.append(dict(plate=k[0],sales=k[1],category=cat,amount=money(item['额'])));continue
if cat=='结转':base,days,den=g['奖励结转'],1,1
elif cat=='当月':
rs=[r for r in g['recs'] if r['是否达标']=='达标'];assert len({r['奖励基准_达标'] for r in rs})==1
base,days,den=rs[0]['奖励基准_达标'],sum(r['考核天数'] for r in rs),30
else:base,days,den=G[m][k]['奖励补发'],G[m][k]['天数'],e.DAYS[m]
assert m==9 or (m,*k) not in settled
exact=base*days/den;assert abs(exact-item['额'])<1e-7
gap=sum(r['缺口天数'] for r in g['recs']);history_gap=sum(r.get('缺口天数',0) for r in aug['records'] if (r['车牌号'],r['销售经理'])==k)
ledger.append(dict(id=len(ledger)+1,plate=k[0],sales=k[1],department=g['部门'],category=cat,assessment_month=m,base=base,days=days,month_days=den,amount=money(exact),gross_amount=money(exact),withheld=0,unrounded=exact,virtual=bool(g.get('虚拟')),gap_days=gap,history_gap_days=history_gap,status='按现有里程计算' if gap or history_gap else '数据齐全'))
groups=[]
for k,g in sorted(G[9].items(),key=lambda kv:(kv[0][1],kv[0][0])):
if k[1] in departed:continue
prev=G[8].get(k,{});pays=[l for l in ledger if (l['plate'],l['sales'])==k]
groups.append(dict(plate=k[0],sales=k[1],department=g['部门'],days=g['天数'],target=money(g['应考核']),actual=money(g['实际']),monthly_target=g['目标km'],reward=g['奖励达标'],qualified=g['有达标'],cum_target=money(g['cum_t']),cum_actual=money(g['cum_a']),cum_qualified=g['cum_q'],previous_carry=prev.get('可结转',0),carry_left=g['可结转'],used_carry=g['累计已发结转'],virtual=bool(g.get('虚拟')),total=money(sum(l['amount'] for l in pays)),gap_days=sum(r['缺口天数'] for r in g['recs']),history_gap_days=sum(r.get('缺口天数',0) for r in aug['records'] if (r['车牌号'],r['销售经理'])==k),history=[[money(G[m][k]['应考核']),money(G[m][k]['实际'])] if k in G[m] else [None,None] for m in range(1,10)]))
people=sorted({g['sales'] for g in groups});total=money(sum(l['amount'] for l in ledger))
summary=dict(amount=total,unrounded_total=money(sum(l['unrounded'] for l in ledger)),records=len(records),vehicles=len({r['车牌号'] for r in records}),groups=len(groups),ledger_items=len(ledger),people=len(people),daily=len(daily),gap_days=len(gaps),gap_records=sum(r['缺口天数']>0 for r in records),gap_vehicles=len({x['plate'] for x in gaps}),wholly_missing_records=sum(r['缺口天数']==r['考核天数'] for r in records),basis_days=dict(basis),test_days=sum(x['test_flag'] is True for x in daily),unknown_test_days=sum(x['test_candidate'] and x['test_flag'] is None for x in daily),new_policy_vehicles=len(policy&{r['车牌号'] for r in records}),categories={c:money(sum(l['amount'] for l in ledger if l['category']==c)) for c in cats},excluded_departed_amount=money(sum(x['amount'] for x in excluded)))
assert len({(l['plate'],l['sales'],l['category']) for l in ledger})==len(ledger)
assert abs(sum(g['total'] for g in groups)-total)<1e-7
for l in ledger:
if l['category']=='结转':assert not any(y['plate']==l['plate'] and y['sales']==l['sales'] and y['category'] in ('当月','累计补发9月') for y in ledger)
for s in prepared['sources'].values():assert hashlib.sha256(Path(s['snapshot']).read_bytes()).hexdigest()==s['sha256']
paid_schedule=[dict(month=m,plate=k[0],sales=k[1],days=g['天数'],reward=g['奖励补发'],settled=(m,*k) in settled,target=money(g['应考核']),actual=money(g['实际'])) for m,gs in before.items() for k,g in gs.items()]
display_categories=['当月','结转','累计补发9月']+[f'补发{m}月' for m in range(8,0,-1)]
confirmed['september_department_overrides']={'刘念念':'业务一部'}
result={**prepared,**dict(confirmed_rules=confirmed,daily=daily,records=records,gaps=gaps,groups=groups,ledger=ledger,people=people,categories=display_categories,summary=summary,historical_months=paid_schedule,august_ledger=aug['ledger'],excluded=excluded)}
dump('september_calculation.json',result);dump('confirmed_rules.json',confirmed);dump('calculation_summary.json',summary)
print(json.dumps(summary,ensure_ascii=False,indent=2))
print('Missing groups:',json.dumps([dict(plate=r['车牌号'],sales=r['销售经理'],days=r['缺口天数'],known=r['实际行驶里程(km)']) for r in records if r['缺口天数']],ensure_ascii=False))