Files
mileage-bonus/outputs/hydrogen-history-20260916/recalculate_min_hydrogen.py
T
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

164 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.
from pathlib import Path
import sys,json,copy,re,hashlib
from collections import defaultdict,Counter
from decimal import Decimal,ROUND_HALF_UP
from datetime import date,timedelta
import openpyxl
OUT=Path(__file__).resolve().parent
REPO=OUT.parents[1]
sys.path.insert(0,str(REPO))
import calc_engine as e
SUP=Path('/Users/shishengliang/Documents/羚牛/里程考核核算/8月/资料检查/运行支持')
old=json.loads((SUP/'august_calculation.json').read_text())
source_workbook=SUP.parents[1]/'8月核算结果/里程任务考核_8月核算.xlsx'
sw=openpyxl.load_workbook(source_workbook,read_only=True,data_only=True)
srr=list(sw['8月奖金发放记录'].values)
existing_deductions={(r[0],r[1],r[4]):float(r[8] or 0) for r in srr[1:] if r[0] and r[0]!='合计' and isinstance(r[8],(int,float))}
source_actual_total=sw['8月汇总(新版界面)']['M22'].value
sw.close()
coverage=json.loads((OUT/'source_coverage.json').read_text())
hydrogen_daily={(p,d):v for p,d,v in json.loads((OUT/'hydrogen_daily.json').read_text())}
def money(x):return float(Decimal(str(x)).quantize(Decimal('.01'),rounding=ROUND_HALF_UP))
def dump(name,obj):(OUT/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2,default=str))
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)}
D[8]=copy.deepcopy(old['records'])
G={m:e.grp(D[m],m) for m in D}
e.calc_jan_carryover(G[1]);all_data={}
for m in range(2,9):e.calc_month(m,G,all_data,G[m-1],G[m])
# Verify exact historical monthly baseline against the authoritative August data.
for x in old['groups']:
k=(x['plate'],x['sales'])
for m in range(1,9):
expected=[money(G[m][k]['应考核']),money(G[m][k]['实际'])] if k in G[m] else [None,None]
assert expected==x['history'][m-1],(k,m,expected,x['history'][m-1])
by=defaultdict(list)
for r in coverage['records']:by[(r['month'],r['plate'],r['sales'])].append(r)
comparison=[]
for (m,p,s),rs in sorted(by.items()):
g=G[m][(p,s)];a=money(sum(float(r['original_assessment_km'] or 0) for r in rs))
assert a==money(g['实际'])
assert all(r['hydrogen_km'] is None for r in rs) or all(r['hydrogen_km'] is not None for r in rs),('mixed coverage',m,p,s)
dates={str(date.fromisoformat(r['start'])+timedelta(days=i)) for r in rs for i in range(r['days'])}
overlap=sum(r['days'] for r in rs)-len(dates)
h=None if rs[0]['hydrogen_km'] is None else money(sum(hydrogen_daily[p,d] for d in dates))
n=a if h is None else min(a,h)
comparison.append(dict(month=m,plate=p,sales=s,original=a,hydrogen=h,corrected=n,reduction=money(a-n),overlap_days=overlap,note='无纯氢数据,保留原考核里程' if h is None else (f'考核区间重叠{overlap}天,纯氢按日期去重' if overlap else '同考核区间汇总后取小值'),intervals=';'.join(r['start']+'至'+r['end'] for r in rs),source_rows=','.join(str(r['source_row']) for r in rs)))
paid=set();carry_paid=set();paid_rows=[];payment_controls=[]
final_totals=[21212.26,21152.14,56907.10,86848.08,92912.56,93696.88,62616.73]
for m in range(1,8):
path=REPO/f'里程任务考核_{m}月核算.xlsx' if m<7 else REPO/'7月绩效重算_多源考核里程_20260817/里程任务考核_7月核算.xlsx'
w=openpyxl.load_workbook(path,read_only=True,data_only=True);rr=list(w[f'{m}月奖金发放记录'].values);w.close()
subtotal=0
for rn,row in enumerate(rr[1:],2):
r=dict(zip(rr[0],row));p=r.get('车牌号');s=r.get('业务员');cat=r.get('发放类型');amount=r.get('实发金额')
if not p or not isinstance(amount,(int,float)) or amount<=0:continue
if m==7 and (s=='钱婧' or s in {'赵连飞','伍仲文','岑彦'} and cat!='当月'):continue
bm=int(re.search(r'补发(\d+)月',cat).group(1)) if '补发' in cat else m
assert (p,s) in G[bm],('payment owner missing',m,r)
paid.add((bm,p,s))
if cat=='结转':carry_paid.add((m,p,s))
recipient={'赵连飞':'刘念念','伍仲文':'钟祥','岑彦':'钟祥'}.get(s,s) if m>=6 and cat=='当月' else s
paid_rows.append(dict(payment_month=m,assessment_month=bm,plate=p,owner=s,recipient=recipient,category=cat,amount=amount,source=path.name,source_row=rn))
subtotal+=amount
payment_controls.append(dict(month=m,final_total=final_totals[m-1],row_total=money(subtotal),rounding_difference=money(subtotal-final_totals[m-1])))
assert all(abs(x['rounding_difference'])<=.03 for x in payment_controls),payment_controls
# Control: paid-ledger locking with original mileage reproduces the existing August
# non-departed calculation. This separates mileage effects from accounting changes.
B=copy.deepcopy(G)
for m in range(1,8):
for k,g in B[m].items():
prev=B.get(m-1,{}).get(k,{})
used=prev.get('累计已发结转',0)+int((m,*k) in carry_paid)
earned=int(max(0,g['cum_a']-g['cum_t'])//g['目标km']) if g['目标km'] else 0
g['累计已发结转']=used;g['可结转']=max(0,earned-used);g[f'{m}月已发']=(m,*k) in paid
for bm in range(1,m):g.pop(f'补发{bm}月',None)
B[8]=e.grp(D[8],8);bd={};e.calc_month(8,B,bd,B[7],B[8])
bp={}
for cat,items in bd[8].items():
for item in items:
k=(item['车牌'],item['销售'])
if k[1] in old['departed']['people']:continue
value=item['额']
if B[8][k].get('虚拟'):value=next(x['reward'] for x in old['groups'] if (x['plate'],x['sales'])==k)
bp[(*k,cat)]=money(value)
assert bp=={(l['plate'],l['sales'],l['category']):l['amount'] for l in old['ledger']},'Original mileage plus locked actuals failed baseline reproduction'
# Preserve historical assessment scope and paid virtual periods. Recompute cumulative mileage,
# but never regenerate Jan-Jul payment flags from revised eligibility.
N=copy.deepcopy(G)
for x in comparison:N[x['month']][(x['plate'],x['sales'])]['实际']=x['corrected']
states=[]
for m in range(1,8):
for k,g in N[m].items():
prev=N.get(m-1,{}).get(k,{})
g['cum_t']=prev.get('cum_t',0)+g['应考核'];g['cum_a']=prev.get('cum_a',0)+g['实际']
g['cum_q']=g['cum_t']>0 and g['cum_a']>=g['cum_t']
used=prev.get('累计已发结转',0)+int((m,*k) in carry_paid)
earned=int(max(0,g['cum_a']-g['cum_t'])//g['目标km']) if g['目标km']>0 else 0
g['累计已发结转']=used;g['可结转']=max(0,earned-used)
g[f'{m}月已发']=(m,*k) in paid
for bm in range(1,m):g.pop(f'补发{bm}月',None)
states.append(dict(month=m,plate=k[0],sales=k[1],cum_target=money(g['cum_t']),cum_actual=money(g['cum_a']),earned=earned,used=used,available=g['可结转'],paid=(m,*k) in paid,virtual=bool(g.get('虚拟'))))
N[8]=e.grp(D[8],8);new_data={};e.calc_month(8,N,new_data,N[7],N[8])
# August policy reward for virtual groups comes from authoritative existing August group.
oldgroups={(x['plate'],x['sales']):x for x in old['groups']}
for k,g in N[8].items():
if g.get('虚拟'):
g['结转']=oldgroups[k]['reward']
for f in ['奖励达标','奖励结转','奖励补发']:g[f]=oldgroups[k]['reward']
for item in new_data[8]['结转']:
if (item['车牌'],item['销售'])==k:item['额']=g['结转']
ledger=[];departed=set(old['departed']['people'])
for cat in old['categories']:
for item in new_data[8].get(cat,[]):
k=(item['车牌'],item['销售']);g=N[8][k]
if k[1] in departed:continue
m=int(re.search(r'补发(\d+)月',cat).group(1)) if '补发' in cat else 8
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),31
else:base,days,den=N[m][k]['奖励补发'],N[m][k]['天数'],e.DAYS[m]
assert m==8 or (m,*k) not in paid,('duplicate backpay',m,k)
exact=base*days/den;assert abs(exact-item['额'])<1e-7
ledger.append(dict(id=len(ledger)+1,plate=k[0],sales=k[1],owner_sales=k[1],department=g['部门'],source_department=g['部门'],assessment_month=m,category=cat,base=base,days=days,month_days=den,gross_amount=money(exact),amount=money(exact),withheld=0,unrounded=exact,unrounded_net=exact,virtual=bool(g.get('虚拟')),payment_status='released',payment_status_text='正常发放',withhold_reason='',transfer=False,receiver='',transfer_from=''))
for l in ledger:
deduction=existing_deductions.get((l['plate'],l['sales'],l['category']),0)
l['withheld']=deduction;l['amount']=money(l['gross_amount']-deduction);l['unrounded_net']=l['unrounded']-deduction
if deduction:l['withhold_reason']='保留8月原表现有扣减';l['payment_status_text']='原表扣减保留'
for l in old['ledger']:
l['amount']=money(l['gross_amount']-existing_deductions.get((l['plate'],l['sales'],l['category']),0))
old['stats']['amount']=money(sum(l['amount'] for l in old['ledger']))
assert old['stats']['amount']==source_actual_total
d=copy.deepcopy(old);d['ledger']=ledger;groups=[]
for k,g in sorted(N[8].items(),key=lambda kv:(kv[0][1],kv[0][0])):
pays=[l for l in ledger if (l['plate'],l['sales'])==k];total=money(sum(l['amount'] for l in pays))
groups.append(dict(plate=k[0],sales=k[1],department=g['部门'],rows=len(g['recs']),days=g['天数'],target=money(g['应考核']),actual=money(g['实际']),qualified=g['有达标'],cum_target=money(g['cum_t']),cum_actual=money(g['cum_a']),cum_qualified=g['cum_q'],previous_carry=N[7].get(k,{}).get('可结转',0),carry_left=g['可结转'],virtual=bool(g.get('虚拟')),reward=g['奖励达标'],total=total,gross_total=total,withheld_total=0,withhold_reason=oldgroups.get(k,{}).get('withhold_reason',''),history=[[money(N[m][k]['应考核']),money(N[m][k]['实际'])] if k in N[m] else [None,None] for m in range(1,9)]))
d['groups']=groups
for g in groups:
pays=[l for l in ledger if (l['plate'],l['sales'])==(g['plate'],g['sales'])]
g['gross_total']=money(sum(l['gross_amount'] for l in pays));g['withheld_total']=money(sum(l['withheld'] for l in pays))
for r in d['person_summary']:
for i,cat in enumerate(d['categories'],2):r[i]=money(sum(l['amount'] for l in ledger if l['sales']==r[0] and l['category']==cat))
r[-1]=money(sum(r[2:-1]))
for r in d['person_audit']:
if r[0] not in departed:r[2]=r[4]=money(sum(l['amount'] for l in ledger if l['sales']==r[0]))
total=money(sum(l['amount'] for l in ledger));unrounded=money(sum(l['unrounded'] for l in ledger))
d['stats'].update(amount=total,gross_amount=total,withheld_amount=0,unrounded_total=unrounded,unrounded_net_total=unrounded,rounding_difference=money(total-unrounded),net_rounding_difference=money(total-unrounded),display_groups=sum(g['sales'] not in departed for g in groups),virtual_carry_records=sum(g['virtual'] for g in groups))
d['withholding']['net_amount']=total
gross=money(sum(l['gross_amount'] for l in ledger));withheld=money(sum(l['withheld'] for l in ledger))
d['stats'].update(gross_amount=gross,withheld_amount=withheld,withheld_records=sum(l['withheld']>0 for l in ledger),unrounded_net_total=money(sum(l['unrounded_net'] for l in ledger)))
d['stats']['rounding_difference']=money(gross-unrounded)
d['historical_revision']=dict(paid_locked=True,old_august_amount=old['stats']['amount'],refresh='纯氢来源或取数口径变更后需重新运行核算;表内金额及汇总公式自动计算。')
changes=[]
oldpay={(l['plate'],l['sales'],l['category']):l['amount'] for l in old['ledger']};newpay={(l['plate'],l['sales'],l['category']):l['amount'] for l in ledger}
for k in sorted(oldpay.keys()|newpay.keys()):
a,b=oldpay.get(k,0),newpay.get(k,0)
if a!=b:changes.append(dict(plate=k[0],sales=k[1],category=k[2],old=a,new=b,difference=money(b-a)))
summary=dict(old_amount=old['stats']['amount'],new_amount=total,difference=money(total-old['stats']['amount']),comparison_groups=len(comparison),reduced_groups=sum(x['reduction']>0 for x in comparison),missing_groups=sum(x['hydrogen'] is None for x in comparison),reduced_km=money(sum(x['reduction'] for x in comparison)),categories={cat:dict(old=money(sum(l['amount'] for l in old['ledger'] if l['category']==cat)),new=money(sum(l['amount'] for l in ledger if l['category']==cat))) for cat in d['categories']},payment_controls=payment_controls,changes=len(changes))
assert total==money(sum(r[-1] for r in d['person_summary']))
assert len(newpay)==len(ledger)
assert money(sum(x['difference'] for x in changes))==summary['difference']
dump('august_calculation.json',d);dump('comparison.json',comparison);dump('historical_paid.json',paid_rows);dump('historical_states.json',states);dump('bonus_changes.json',changes);dump('recalculation_summary.json',summary)
dump('confirmed_rules.json',dict(scope='1—6月',group_by=['车辆','月份','业务负责人'],mileage='按原考核区间汇总纯氢里程,与8月原表对应月考核里程取小值',missing_hydrogen='保留原考核里程,并注明无纯氢数据',historical_payment='1—7月最终表作为实际已发,保留不追扣;已发月份不重复补发;扣除实际已使用结转',july_august='7—8月考核里程沿用原表',gps_meter='不再取数或判定测试车辆'))
print(json.dumps(summary,ensure_ascii=False,indent=2))