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.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Read-only source coverage audit. Does not calculate or overwrite awards."""
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
import json, re, warnings
|
||||
import openpyxl
|
||||
|
||||
warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
|
||||
OUT = Path(__file__).resolve().parent
|
||||
REPO = OUT.parents[1]
|
||||
HROOT = Path('/tmp/mileage-hydrogen-review-0916/氢能源行驶里程')
|
||||
METER = Path('/Users/shishengliang/Downloads/车辆里程查询_20260101-20260731_1024辆.xlsx')
|
||||
JAN = Path('/Users/shishengliang/Code/lingniu/氢能车用氢行驶里程统计 1.17.xlsx')
|
||||
|
||||
def read(p, sheet=None):
|
||||
w = openpyxl.load_workbook(p, read_only=True, data_only=True)
|
||||
s = w[sheet] if sheet else w.worksheets[0]
|
||||
s.reset_dimensions()
|
||||
r = list(s.values)
|
||||
w.close()
|
||||
return r
|
||||
|
||||
def norm(x):
|
||||
return re.sub(r'\s+', '', str(x or '')).upper()
|
||||
|
||||
h = {}
|
||||
bad_files = []
|
||||
for p in HROOT.rglob('*.xlsx'):
|
||||
if int(p.parent.name[:-1]) > 6:
|
||||
continue
|
||||
rr = read(p)
|
||||
headers = list(rr[0])
|
||||
if '车牌号码' not in headers:
|
||||
bad_files.append(str(p))
|
||||
continue
|
||||
for row in rr[1:]:
|
||||
r = dict(zip(headers,row))
|
||||
plate = norm(r.get('车牌号码'))
|
||||
if not plate:
|
||||
continue
|
||||
d = str(r['日期范围'])[:10]
|
||||
val = r.get('氢能源行驶里程(km)')
|
||||
if val is not None and str(val).strip():
|
||||
key = (plate,d)
|
||||
assert key not in h, ('duplicate hydrogen',key)
|
||||
h[key] = float(val)
|
||||
jan_status = 'not read'
|
||||
try:
|
||||
rr=read(JAN)
|
||||
assert '车牌号码' in rr[0]
|
||||
for row in rr[1:]:
|
||||
r=dict(zip(rr[0],row)); plate=norm(r.get('车牌号码'))
|
||||
if plate and r.get('氢能源行驶里程(km)') is not None:
|
||||
h[(plate,str(r['日期范围'])[:10])] = float(r['氢能源行驶里程(km)'])
|
||||
jan_status='read'
|
||||
except (OSError,AssertionError) as e:
|
||||
jan_status=f'{type(e).__name__}: {e}'
|
||||
|
||||
(OUT/'hydrogen_daily.json').write_text(json.dumps([[p,d,v] for (p,d),v in sorted(h.items())],ensure_ascii=False))
|
||||
# GPS and meter acquisition superseded by the user's monthly minimum rule.
|
||||
t={};meter_months=Counter()
|
||||
|
||||
records=[]; summary=[]
|
||||
for m in range(1,7):
|
||||
rr=read(REPO/f'租赁任务考核_2026年{m}月.xlsx','业务考核视图')
|
||||
month=[]
|
||||
for rownum,row in enumerate(rr[1:],2):
|
||||
r=dict(zip(rr[0],row)); plate=norm(r.get('车牌号'))
|
||||
if not plate:
|
||||
continue
|
||||
def as_date(v):
|
||||
return v.date() if isinstance(v,datetime) else datetime.fromisoformat(str(v)).date()
|
||||
start,end=as_date(r['考核开始日期']),as_date(r['考核结束日期'])
|
||||
dates=[(start+timedelta(days=i)).isoformat() for i in range((end-start).days+1)]
|
||||
mh=[d for d in dates if (plate,d) not in h]
|
||||
mt=[d for d in dates if (plate,d) not in t]
|
||||
both=[d for d in mh if (plate,d) not in t]
|
||||
x=dict(month=m,source_row=rownum,plate=plate,sales=r.get('销售经理'),start=str(start),end=str(end),days=len(dates),hydrogen_missing_dates=mh,meter_missing_dates=mt,both_missing_dates=both,original_assessment_km=r.get('实际行驶里程(km)'),test_decision='not applicable before April' if m<4 else 'pending GPS and complete same-period meter basis')
|
||||
assert len(mh) in (0,len(dates)), 'Partial hydrogen needs a daily fallback decision'
|
||||
x['hydrogen_km'] = round(sum(h[(plate,d)] for d in dates), 2) if not mh else None
|
||||
x['candidate_non_test_km'] = x['hydrogen_km'] if not mh else x['original_assessment_km']
|
||||
x['meter_comparison_km'] = x['original_assessment_km']
|
||||
x['meter_basis'] = '用户确认历史考核源表实际里程即完整仪表区间合计'
|
||||
x['test_decision'] = '不再采用测试车辆判定;按同组月合计取小值'
|
||||
month.append(x);records.append(x)
|
||||
summary.append(dict(month=m,records=len(month),records_with_hydrogen_gaps=sum(bool(x['hydrogen_missing_dates']) for x in month),records_with_meter_gaps=sum(bool(x['meter_missing_dates']) for x in month),records_with_both_gaps=sum(bool(x['both_missing_dates']) for x in month),both_missing_record_days=sum(len(x['both_missing_dates']) for x in month)))
|
||||
|
||||
result=dict(jan17_status=jan_status,meter_nonblank_vehicle_days_by_month=dict(sorted(meter_months.items())),summary=summary,records=records,invalid_archive_files=bad_files)
|
||||
(OUT/'source_coverage.json').write_text(json.dumps(result,ensure_ascii=False,indent=2,default=str))
|
||||
print(json.dumps({'jan17_status':jan_status,'summary':summary},ensure_ascii=False,indent=2))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
[
|
||||
{
|
||||
"plate": "粤A01013F",
|
||||
"sales": "刘念念",
|
||||
"category": "累计补发8月",
|
||||
"old": 1000.0,
|
||||
"new": 0,
|
||||
"difference": -1000.0
|
||||
},
|
||||
{
|
||||
"plate": "粤A02298F",
|
||||
"sales": "刘念念",
|
||||
"category": "累计补发8月",
|
||||
"old": 1000.0,
|
||||
"new": 0,
|
||||
"difference": -1000.0
|
||||
},
|
||||
{
|
||||
"plate": "粤A05378F",
|
||||
"sales": "秦挺",
|
||||
"category": "补发6月",
|
||||
"old": 1000.0,
|
||||
"new": 0,
|
||||
"difference": -1000.0
|
||||
},
|
||||
{
|
||||
"plate": "粤A05378F",
|
||||
"sales": "秦挺",
|
||||
"category": "补发7月",
|
||||
"old": 677.42,
|
||||
"new": 0,
|
||||
"difference": -677.42
|
||||
},
|
||||
{
|
||||
"plate": "粤A05809F",
|
||||
"sales": "刘念念",
|
||||
"category": "累计补发8月",
|
||||
"old": 1000.0,
|
||||
"new": 0,
|
||||
"difference": -1000.0
|
||||
},
|
||||
{
|
||||
"plate": "粤A05968F",
|
||||
"sales": "秦挺",
|
||||
"category": "累计补发8月",
|
||||
"old": 1000.0,
|
||||
"new": 0,
|
||||
"difference": -1000.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP2011",
|
||||
"sales": "钟祥",
|
||||
"category": "累计补发8月",
|
||||
"old": 0,
|
||||
"new": 260.0,
|
||||
"difference": 260.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP2011",
|
||||
"sales": "钟祥",
|
||||
"category": "结转",
|
||||
"old": 260.0,
|
||||
"new": 0,
|
||||
"difference": -260.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3607",
|
||||
"sales": "刘念念",
|
||||
"category": "当月",
|
||||
"old": 0,
|
||||
"new": 234.84,
|
||||
"difference": 234.84
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP3607",
|
||||
"sales": "刘念念",
|
||||
"category": "结转",
|
||||
"old": 260.0,
|
||||
"new": 0,
|
||||
"difference": -260.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP5378",
|
||||
"sales": "刘念念",
|
||||
"category": "累计补发8月",
|
||||
"old": 0,
|
||||
"new": 101.61,
|
||||
"difference": 101.61
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP5378",
|
||||
"sales": "刘念念",
|
||||
"category": "结转",
|
||||
"old": 150.0,
|
||||
"new": 0,
|
||||
"difference": -150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP6639",
|
||||
"sales": "尚建华",
|
||||
"category": "当月",
|
||||
"old": 0,
|
||||
"new": 111.29,
|
||||
"difference": 111.29
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP6639",
|
||||
"sales": "尚建华",
|
||||
"category": "结转",
|
||||
"old": 150.0,
|
||||
"new": 0,
|
||||
"difference": -150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP9706",
|
||||
"sales": "尚建华",
|
||||
"category": "累计补发8月",
|
||||
"old": 0,
|
||||
"new": 117.42,
|
||||
"difference": 117.42
|
||||
},
|
||||
{
|
||||
"plate": "粤AGP9706",
|
||||
"sales": "尚建华",
|
||||
"category": "结转",
|
||||
"old": 260.0,
|
||||
"new": 0,
|
||||
"difference": -260.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ5819",
|
||||
"sales": "吴纬涛",
|
||||
"category": "当月",
|
||||
"old": 0,
|
||||
"new": 150.0,
|
||||
"difference": 150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ5819",
|
||||
"sales": "吴纬涛",
|
||||
"category": "结转",
|
||||
"old": 150.0,
|
||||
"new": 0,
|
||||
"difference": -150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ5838",
|
||||
"sales": "刘念念",
|
||||
"category": "当月",
|
||||
"old": 0,
|
||||
"new": 150.0,
|
||||
"difference": 150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGQ5838",
|
||||
"sales": "刘念念",
|
||||
"category": "结转",
|
||||
"old": 150.0,
|
||||
"new": 0,
|
||||
"difference": -150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGR5098",
|
||||
"sales": "尚建华",
|
||||
"category": "当月",
|
||||
"old": 0,
|
||||
"new": 150.0,
|
||||
"difference": 150.0
|
||||
},
|
||||
{
|
||||
"plate": "粤AGR5098",
|
||||
"sales": "尚建华",
|
||||
"category": "结转",
|
||||
"old": 150.0,
|
||||
"new": 0,
|
||||
"difference": -150.0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,180 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import {Workbook,SpreadsheetFile,FileBlob} from '@oai/artifact-tool';
|
||||
const root='/Users/shishengliang/Documents/羚牛/里程考核核算/8月';
|
||||
const sup='/Users/shishengliang/Code/lingniu/mileage-bonus/outputs/hydrogen-history-20260916', file=sup+'/里程任务考核_8月核算_纯氢修正版.xlsx';
|
||||
const july='/Users/shishengliang/Code/lingniu/mileage-bonus/7月绩效重算_多源考核里程_20260817/里程任务考核_7月核算.xlsx';
|
||||
if(globalThis.process.argv.includes('--preview')){
|
||||
for(const [p,names,prefix]of [[july,['考核奖励规则','7月汇总','一部-何文斌'],'july'],[file,['8月汇总'],'aug_before']]){
|
||||
const w=await SpreadsheetFile.importXlsx(await FileBlob.load(p));
|
||||
for(const name of names){const b=await w.render({sheetName:name,range:name.includes('汇总')?'A1:F16':'A1:M14',scale:1});await fs.writeFile(`${sup}/${prefix}_${name}.png`,new Uint8Array(await b.arrayBuffer()));}
|
||||
}
|
||||
globalThis.process.exit(0);
|
||||
}
|
||||
const d=JSON.parse(await fs.readFile(sup+'/august_calculation.json','utf8'));
|
||||
const meta=JSON.parse(await fs.readFile(sup+'/july_style_metadata.json','utf8'));
|
||||
const w=Workbook.create(), previews=[], controls={people:{},ledgerRows:{}};
|
||||
const c=n=>{let s='';for(;n;n=Math.floor((n-1)/26))s=String.fromCharCode(65+(n-1)%26)+s;return s;};
|
||||
const money='#,##0.00;[Red](#,##0.00);0.00';
|
||||
const dataEnd=d.ledger.length+1;
|
||||
const L=col=>`'8月奖金发放记录'!$${col}$2:$${col}$${dataEnd}`;
|
||||
const sum=(sales,cat,plate)=>`SUMIFS(${L('J')},${L('B')},${sales}${cat?`,${L('E')},"${cat}"`:''}${plate?`,${L('A')},${plate}`:''})`;
|
||||
const sumG=(sales,cat,plate)=>`SUMIFS(${L('G')},${L('B')},${sales}${cat?`,${L('E')},"${cat}"`:''}${plate?`,${L('A')},${plate}`:''})`;
|
||||
const departed=d.departed||{people:[],transfers:[],transfer_map:{},legacy_items:[],reason:''};
|
||||
const departedSet=new Set(departed.people||[]);
|
||||
// 主表按原负责人过滤:仅剔除四名离职人员的 records/分组,同车其他负责人的区间保留,不按车牌整删。
|
||||
const dr=[...d.records].filter(r=>!departedSet.has(r['销售经理'])).sort((a,b)=>(a['销售经理']+a['车牌号']).localeCompare(b['销售经理']+b['车牌号'],'zh'));
|
||||
const groups=d.groups.filter(g=>!departedSet.has(g.sales));
|
||||
const dispVehicles=new Set(dr.map(r=>r['车牌号'])).size;
|
||||
const dispQualified=dr.filter(r=>r['是否达标']==='达标').length;
|
||||
const dispGapDays=dr.reduce((a,r)=>a+(r['缺口天数']||0),0);
|
||||
const transferByReceiver=new Map();
|
||||
for(const t of (departed.transfers||[])){const a=transferByReceiver.get(t.receiver)||[];a.push(t);transferByReceiver.set(t.receiver,a);}
|
||||
const transferLedgerId=t=>{const l=d.ledger.find(x=>x.transfer&&x.plate===t.plate&&x.sales===t.receiver);return l?l.id:null;};
|
||||
const clean=v=>v===undefined?null:typeof v==='string'&&v.startsWith('=')?"'"+v:v;
|
||||
function sheet(name,n=13,end=30){
|
||||
const s=w.worksheets.add(name),r=s.getRange(`A1:${c(n)}${end}`);
|
||||
r.format={font:{name:'宋体',size:11,color:'#000000'},rowHeight:19,columnWidth:14,verticalAlignment:'center'};
|
||||
s.showGridLines=true;return s;
|
||||
}
|
||||
function write(s,row,vals){s.getRange(`A${row}:${c(vals.length)}${row}`).values=[vals.map(clean)];}
|
||||
function grid(s,a){s.getRange(a).format.borders={preset:'all',style:'thin',color:'#000000'};}
|
||||
function header(s,row,vals){write(s,row,vals);const a=`A${row}:${c(vals.length)}${row}`;s.getRange(a).format={font:{name:'宋体',size:11,bold:true,color:'#000000'},fill:'#D9E1F2',horizontalAlignment:'center',wrapText:true,rowHeight:34};grid(s,a);}
|
||||
function widths(s,ws,end=100){for(const [col,v]of Object.entries(ws))s.getRange(`${col}1:${col}${end}`).format.columnWidth=v;}
|
||||
function bold(s,row,n=3){s.getRange(`A${row}:${c(n)}${row}`).format.font={name:'宋体',bold:true,size:11,color:'#000000'};}
|
||||
function table(s,h,rows,ws={},numbers=[]){header(s,1,h);if(rows.length)s.getRange(`A2:${c(h.length)}${rows.length+1}`).values=rows.map(r=>r.map(clean));grid(s,`A1:${c(h.length)}${rows.length+1}`);widths(s,ws,rows.length+1);for(const col of numbers)s.getRange(`${col}2:${col}${rows.length+1}`).setNumberFormat(money);}
|
||||
const current=new Map(d.records.map(r=>[r['车牌号']+'|'+r['销售经理'],r]));
|
||||
const rec=g=>current.get(g.plate+'|'+g.sales)||meta.last_records[g.plate+'|'+g.sales]||{};
|
||||
const personalName=p=>(d.departments[p].includes('、')?'跨部门':d.departments[p].replace('业务',''))+'-'+p;
|
||||
// 与7月相同的工作表次序,新版汇总紧邻旧版汇总。
|
||||
const rules=sheet('考核奖励规则',6,68), detail=sheet('里程明细8月',23,dr.length+1), process=sheet('8月计算过程',25,groups.length+1), tracking=sheet('车辆考核追踪',40,groups.length+1), ledger=sheet('8月奖金发放记录',14,dataEnd+2), old=sheet('8月汇总',3,170), modern=sheet('8月汇总(新版界面)',13,36);
|
||||
const ps={};for(const p of d.people)ps[p]=sheet(personalName(p),13,groups.filter(g=>g.sales===p).length*8+10);
|
||||
|
||||
table(rules,['考核目标','月度目标里程(km)','当月达标','当月未达标','结转','补发/累计补发'],[
|
||||
['交投40辆4.5T普货',3000,'150元×天数折算','不发','150元整月','150元×折算'],['交投190辆4.5T冷链车',3000,'150元×天数折算','不发','150元整月','150元×折算'],['羚牛136辆4.5T冷链车',5000,'260元×天数折算','不发','260元整月','260元×折算'],['恒运50辆4.5T普货',5000,'260元×天数折算','不发','260元整月','260元×折算'],['羚牛100辆18T',6000,'1000元×天数折算','不发','1000元整月','1000元×折算']
|
||||
],{A:38,B:18,C:22,D:20,E:20,F:24});
|
||||
header(rules,9,['8月名单对应考核目标','月目标不变','名单当月奖励','未达标','名单结转','8月补发基准']);
|
||||
let rn=10;for(const [name,amount]of Object.entries(d.rules.new_policy.award_by_target)){const km=name.includes('18T')?6000:name.startsWith('交投')?3000:5000;write(rules,rn++,[name,km,`${amount}元×天数折算`,'不发',`${amount}元整月`,`${amount}元×折算`]);}grid(rules,'A9:F14');
|
||||
rules.mergeCells('A16:F18');write(rules,16,['名单标9月,本次适用于8月;名单外沿用原金额。折算、结转、补发规则不变,历史月补发仍使用历史基准。亏损暂不处理。']);rules.getRange('A16:F18').format.wrapText=true;
|
||||
rules.mergeCells('A20:F22');write(rules,20,[`本表按每条发放项取分:应发合计${d.stats.gross_amount.toFixed(2)}元,实发合计${d.stats.amount.toFixed(2)}元,保留原表扣减9.68元。应发未逐项取分总和为${d.stats.unrounded_total.toFixed(2)}元,与应发相差${Math.abs(d.stats.rounding_difference).toFixed(2)}元。1—7月实际已发保留,不追扣、不重复补发。1—6月按纯氢与原考核里程取小值,无纯氢则保留原里程。来源为输入数据目录及项目1—7月历史源表,7月采用20260817重算版本。`]);rules.getRange('A20:F22').format.wrapText=true;
|
||||
header(rules,24,['名单车牌','原考核表行号','8月月奖励(元)','业务员','部门','考核目标']);
|
||||
for(const [i,v]of d.rules.new_policy.matched.entries()){const r=d.records.find(r=>r['车牌号']===v.plate);write(rules,i+25,[v.plate,v.source_rows.join('、'),v.monthly_bonus,r['销售经理'],r['部门名称'],v.target]);}grid(rules,'A24:F55');rules.getRange('F25:F55').format.wrapText=true;rules.getRange('A25:F55').format.rowHeight=34;
|
||||
previews.push(['考核奖励规则','A1:F18']);
|
||||
|
||||
table(detail,['车牌号','部门名称','销售经理','客户名称','合同编号','考核目标','月度目标里程','月奖励金额','考核天数','8月应考核里程','8月实际行驶里程','完成率','8月是否达标','8月奖金(天数折算)','多跑里程','可结转月数','考核状态','源表行','取数方式','新政策','考核开始日期','考核结束日期','说明'],dr.map(r=>[r['车牌号'],r['部门名称'],r['销售经理'],r['客户名称'],r['合同编号'],r['考核目标'],r['月度目标里程'],r['奖励基准_达标'],r['考核天数'],r['应考核里程(km)'],r['实际行驶里程(km)'],r['实际行驶里程(km)']/r['应考核里程(km)'],r['是否达标'],null,r['多跑'],r['可结转'],r['考核状态'],r['源表行'],r['取数方式'],r['新政策'],new Date(r['考核开始日期']+'T00:00:00Z'),new Date(r['考核结束日期']+'T00:00:00Z'),r['说明']]),{A:16,B:14,C:12,D:34,E:28,F:26,G:16,H:14,I:12,J:18,K:18,L:12,M:14,N:20,O:16,P:14,Q:14,R:10,S:25,T:12,U:16,V:16,W:65},['H','J','K','N','O']);
|
||||
detail.getRange(`N2:N${dr.length+1}`).formulas=dr.map((_,i)=>[`=IF(K${i+2}>=J${i+2},ROUND(H${i+2}*I${i+2}/31,2),0)`]);detail.getRange(`L2:L${dr.length+1}`).setNumberFormat('0.00%');detail.getRange(`U2:V${dr.length+1}`).setNumberFormat('yyyy-mm-dd');previews.push(['里程明细8月','A1:H12']);
|
||||
|
||||
table(ledger,['车牌号','业务员','部门','客户名称','发放类型','对应考核月','考核应发','客户盈亏','不发/扣减','实发金额','奖励基准','折算分子','折算分母','原发放编号','说明'],d.ledger.map(l=>{const r=rec(l);const note=l.withhold_reason||'';return[l.plate,l.sales,l.department,r['客户名称']||meta.vehicles[l.plate]?.[7]||'',l.category,l.assessment_month,null,'暂不处理',null,null,l.base,l.days,l.month_days,l.id,note];}),{A:16,B:12,C:16,D:34,E:22,F:14,G:16,H:16,I:14,J:16,K:14,L:14,M:14,N:14,O:36},['G','I','J','K']);
|
||||
ledger.getRange(`G2:G${dataEnd}`).formulas=d.ledger.map((_,i)=>[`=ROUND(K${i+2}*L${i+2}/M${i+2},2)`]);ledger.getRange(`I2:I${dataEnd}`).values=d.ledger.map(l=>[l.withheld]);ledger.getRange(`J2:J${dataEnd}`).formulas=d.ledger.map((_,i)=>[`=G${i+2}-I${i+2}`]);ledger.getRange(`F2:F${dataEnd}`).setNumberFormat('0"月"');write(ledger,dataEnd+1,['合计']);for(const col of ['G','I','J'])ledger.getRange(`${col}${dataEnd+1}`).formulas=[[`=SUM(${col}2:${col}${dataEnd})`]];bold(ledger,dataEnd+1,15);ledger.getRange(`G${dataEnd+1}:K${dataEnd+1}`).setNumberFormat(money);ledger.mergeCells(`A${dataEnd+3}:O${dataEnd+4}`);ledger.getRange(`A${dataEnd+3}`).values=[[`金额为实发:考核应发(G)−不发/扣减(I)=实发(J)。保留原表扣减9.68元,亏损暂不处理。`]];ledger.getRange(`A${dataEnd+3}:O${dataEnd+4}`).format.wrapText=true;ledger.getRange(`A${dataEnd+3}:O${dataEnd+4}`).format.font={name:'宋体',size:10,color:'#C00000'};previews.push(['8月奖金发放记录','A1:O12']);
|
||||
|
||||
const cats=['结转',...Array.from({length:7},(_,i)=>`补发${i+1}月`),'当月','累计补发8月'];
|
||||
table(process,['车牌号','销售经理','部门','8月记录数','8月总天数','8月应考核','8月实际','8月有达标','8月奖金','累计应考核','累计实际','累计达标','可结转',...cats.map(x=>x==='当月'?'当月奖金':x==='结转'?'结转发放':x),'8月发放合计','无本月考核记录'],groups.map(g=>[g.plate,g.sales,g.department,g.rows,g.days,g.target,g.actual,g.qualified?'是':'否',d.records.filter(r=>r['车牌号']===g.plate&&r['销售经理']===g.sales).reduce((a,r)=>a+r['奖金'],0),g.cum_target,g.cum_actual,g.cum_qualified?'达标':'未达标',g.carry_left,...cats.map(()=>null),null,g.virtual?'是':'否']),{A:16,B:12,C:16,D:14,E:14,F:18,G:18,H:14,I:16,J:18,K:18,L:14,M:12,X:18,Y:22},['F','G','I','J','K',...Array.from({length:11},(_,i)=>c(i+14))]);
|
||||
process.getRange(`N2:W${groups.length+1}`).formulas=groups.map((g,i)=>cats.map(cat=>`=${sum(`B${i+2}`,cat,`A${i+2}`)}`));process.getRange(`X2:X${groups.length+1}`).formulas=groups.map((g,i)=>[`=SUM(N${i+2}:W${i+2})`]);previews.push(['8月计算过程','A1:M12']);
|
||||
const th=['车牌号','车架号','归属公司','车型','考核目标','月度奖励','业务员',...Array.from({length:8},(_,i)=>[`${i+1}月应考核`,`${i+1}月实际`,`${i+1}月达标`]).flat(),'累计应完成','累计实际','累计达标','客户名称','客户是否亏损','考核应发','最终发放','未发放原因','本月无考核记录'];
|
||||
table(tracking,th,groups.map(g=>{const r=rec(g),v=meta.vehicles[g.plate]||[];return[g.plate,v[1]||'',v[2]||'',v[3]||'',r['考核目标']||v[4]||'',g.reward,g.sales,...g.history.flatMap(([t,a])=>[t,a,t===null?'':a>=t?'✓':'✗']),g.cum_target,g.cum_actual,g.cum_qualified?'✓':'✗',r['客户名称']||v[7]||'','暂不处理',null,null,g.withhold_reason?g.withhold_reason:(g.total?'':g.virtual?'本月无新增发放':'未满足本月奖励、结转或补发条件'),g.virtual?'是':'否'];}),{A:16,B:24,C:38,D:18,E:28,F:14,G:14,AI:36,AJ:18,AK:16,AL:16,AM:40,AN:22},['F','AF','AG','AK','AL']);
|
||||
tracking.getRange(`AK2:AL${groups.length+1}`).formulas=groups.map((g,i)=>[`=${sumG(`G${i+2}`,null,`A${i+2}`)}`,`=${sum(`G${i+2}`,null,`A${i+2}`)}`]);for(let i=0;i<groups.length;i++)tracking.getRange(`A${i+2}:AN${i+2}`).format.fill=i%2?'#E8E8E8':'#F2F2F2';tracking.freezePanes.freezeRows(1);tracking.freezePanes.freezeColumns(7);previews.push(['车辆考核追踪','A1:G12']);
|
||||
|
||||
// 7月的纵向分类汇总格式。离职人员不列入汇总;接收人的转入项已在其名下计入。
|
||||
const summaryPeople=d.people.filter(p=>!departedSet.has(p));
|
||||
const summaryItems=(p,cat)=>d.ledger.filter(l=>l.sales===p&&(!cat||l.category===cat));
|
||||
const summaryFormula=(p,cell,cat)=>sum(cell,cat);
|
||||
widths(old,{A:28,B:26,C:20},170);write(old,1,['一、实发明细(按发放类别)']);bold(old,1);rn=3;
|
||||
function section(title,cat){write(old,rn++,[title]);bold(old,rn-1);header(old,rn++,['销售人员','车辆数','金额']);const start=rn;const people=summaryPeople.filter(p=>summaryItems(p,cat).length);
|
||||
for(const p of people){write(old,rn,[p,null,null]);old.getRange(`B${rn}`).formulas=[[`=COUNTIFS(${L('B')},A${rn}${cat?`,${L('E')},"${cat}"`:''})`]];old.getRange(`C${rn}`).formulas=[[`=${summaryFormula(p,`A${rn}`,cat)}`]];rn++;}
|
||||
write(old,rn,['总计',null,null]);for(const col of ['B','C'])old.getRange(`${col}${rn}`).formulas=[[`=SUM(${col}${start}:${col}${rn-1})`]];grid(old,`A${start}:C${rn}`);bold(old,rn);rn+=2;
|
||||
}
|
||||
for(const cat of d.categories.filter(cat=>d.ledger.some(l=>l.category===cat)))section('实发-'+cat,cat);
|
||||
write(old,rn,['考核应发合计',null,null]);old.getRange(`C${rn}`).formulas=[[`=SUM(${L('G')})`]];bold(old,rn);rn+=2;
|
||||
write(old,rn++,['二、亏损处理']);bold(old,rn-1);write(old,rn++,['亏损暂不处理。']);rn++;
|
||||
write(old,rn++,['三、最终发放']);bold(old,rn-1);rn++;section('最终发放明细',null);
|
||||
write(old,rn++,['8月最终发放(按销售人员)']);bold(old,rn-1);header(old,rn++,['销售人员','部门名称','最终发放']);let start=rn;
|
||||
for(const p of summaryPeople){write(old,rn,[p,d.departments[p],null]);old.getRange(`C${rn}`).formulas=[[`=${summaryFormula(p,`A${rn}`)}`]];rn++;}grid(old,`A${start}:C${rn-1}`);write(old,rn,['合计',null,null]);old.getRange(`C${rn}`).formulas=[[`=SUM(C${start}:C${rn-1})`]];bold(old,rn);rn+=2;
|
||||
write(old,rn++,['8月最终发放(按部门)']);bold(old,rn-1);header(old,rn++,['部门名称','最终发放']);const depts=[...new Set([...groups.map(g=>g.department),...d.ledger.map(l=>l.department)])].sort();start=rn;
|
||||
for(const dep of depts){write(old,rn,[dep,null]);old.getRange(`B${rn}`).formulas=[[`=SUMIF(${L('C')},A${rn},${L('J')})`]];rn++;}grid(old,`A${start}:B${rn-1}`);write(old,rn,['合计',null]);old.getRange(`B${rn}`).formulas=[[`=SUM(B${start}:B${rn-1})`]];bold(old,rn);rn+=2;
|
||||
write(old,rn++,['总览']);bold(old,rn-1);for(const [label,col]of [['考核应发','G'],['不发/扣减','I'],['最终发放','J']]){write(old,rn,[label,null]);old.getRange(`B${rn}`).formulas=[[`=SUM(${L(col)})`]];rn++;}bold(old,rn-1);old.getRange(`C1:C${rn}`).setNumberFormat(money);old.getRange(`B${start}:B${rn}`).setNumberFormat(money);controls.oldTotal=`B${rn-1}`;previews.push(['8月汇总','A1:C19'],['8月汇总',`A${rn-7}:C${rn-1}`]);
|
||||
|
||||
// 保留上版深色表头、人员横向分类和部门汇总界面。
|
||||
modern.showGridLines=false;modern.getRange('A1:M36').format={font:{name:'PingFang SC',size:11,color:'#183D50'},columnWidth:16,rowHeight:24};widths(modern,{A:14,B:26},36);
|
||||
modern.mergeCells('A1:H1');write(modern,1,['2026年8月里程绩效核算']);modern.getRange('A1:H1').format.font={size:18,bold:true};modern.getRange('A1').format.rowHeight=36;
|
||||
modern.mergeCells('A2:H3');write(modern,2,['金额为实发,按每条发放记录四舍五入到分后汇总。亏损暂不处理。部门汇总按发放记录归属。']);modern.getRange('A2').values=[['1—6月历史里程已修正,1—7月实际已发保留。修正前后差额见8月调整对照。金额按发放项取分后汇总。']];modern.getRange('A2:H3').format={font:{size:10,color:'#526671'},wrapText:true};
|
||||
modern.mergeCells('J1:M1');modern.getRange('J1').values=[['8月核算总额(元)']];modern.getRange('J1:M1').format.font={size:12,bold:true,color:'#167D8D'};modern.mergeCells('J2:M3');modern.getRange('J2').formulas=[['=M22']];modern.getRange('J2:M3').format.font={size:26,bold:true};modern.getRange('J2').setNumberFormat(money);
|
||||
write(modern,5,['业务员','部门',...d.categories.map(cat=>cat+'(元)'),'合计(元)']);modern.getRange('A5:M5').format={fill:'#183D50',font:{color:'#FFFFFF',bold:true},rowHeight:44,wrapText:true};
|
||||
for(const [i,p]of summaryPeople.entries()){const row=i+6;write(modern,row,[p,d.departments[p],...Array(11).fill(null)]);modern.getRange(`C${row}:L${row}`).formulas=[d.categories.map(cat=>`=${summaryFormula(p,`A${row}`,cat)}`)];modern.getRange(`M${row}`).formulas=[[`=SUM(C${row}:L${row})`]];modern.getRange(`A${row}:M${row}`).format.fill=i%2?'#FFFFFF':'#BFE5F2';}
|
||||
write(modern,22,['合计']);modern.getRange('C22:M22').formulas=[Array.from({length:11},(_,i)=>`=SUM(${c(i+3)}6:${c(i+3)}21)`)];modern.getRange('A22:M22').format={fill:'#EAF3F5',font:{bold:true},rowHeight:30};modern.getRange('C6:M22').setNumberFormat(money);
|
||||
modern.getRange('A26:B26').values=[['部门','发放金额(元)']];modern.getRange('A26:B26').format={fill:'#183D50',font:{bold:true,color:'#FFFFFF'},rowHeight:30};for(const [i,dep]of depts.entries()){modern.getRange(`A${27+i}`).values=[[dep]];modern.getRange(`B${27+i}`).formulas=[[`=SUMIF(${L('C')},A${27+i},${L('J')})`]];}modern.getRange(`B27:B${26+depts.length}`).setNumberFormat(money);
|
||||
modern.mergeCells('A35:M36');modern.getRange('A35').values=[[`本月${dispVehicles}辆、${dr.length}条考核记录,其中${dispQualified}条达标;新口径下${dispGapDays}个车辆日所需来源缺失,按已有记录累计并单列缺口,补数后应重算累计、结转及补发。本次实发${d.stats.amount.toFixed(2)}元,保留原表扣减9.68元。`]];modern.getRange('A35:M36').format.wrapText=true;modern.freezePanes.freezeRows(5);modern.freezePanes.freezeColumns(2);previews.push(['8月汇总(新版界面)','A1:M36']);
|
||||
const modernTable=modern.tables.add(`A5:M${5+summaryPeople.length}`,true,'AugustModernSummary');modernTable.showFilterButton=true;
|
||||
|
||||
// 个人对账单按7月方式逐车展开,增加8月列,发放项链接本次8月账单。
|
||||
// 接收人:本人车辆 + 接收转入的离职人员8月当月达标车辆与奖金;离职人员:原计算额审计、本人实发0。
|
||||
for(const p of d.people){const s=ps[p],ownGroups=groups.filter(g=>g.sales===p),audit=d.person_audit.find(r=>r[0]===p)||[p,'',0,0,0,''];
|
||||
const inTransfers=transferByReceiver.get(p)||[],isDeparted=departedSet.has(p);const legacyByPlate=new Map();
|
||||
if(isDeparted){for(const l of (departed.legacy_items||[])){if(l.sales!==p)continue;const a=legacyByPlate.get(l.plate)||[];a.push(l);legacyByPlate.set(l.plate,a);}}
|
||||
widths(s,{A:22,B:28,C:14,D:22,E:22,F:22,G:22,H:22,I:22,J:22,K:22,L:25,M:12},(ownGroups.length+inTransfers.length)*8+12);
|
||||
s.mergeCells('A1:M1');write(s,1,[`${p} | ${d.departments[p]} | 8月绩效对账单${isDeparted?'(离职审计,实发0)':''}`]);s.getRange('A1:M1').format.font={name:'宋体',bold:true,size:14};s.getRange('A1').format.rowHeight=22;
|
||||
s.mergeCells('A2:D2');write(s,2,[`本月考核及结转车辆:${ownGroups.length}辆${inTransfers.length?`;接收转入车辆:${inTransfers.length}辆`:''}`]);bold(s,2,4);s.mergeCells('E2:G2');s.getRange('E2').values=[['本月实发合计(元)']];s.getRange('H2').formulas=[[`=${sum(`"${p}"`)}`]];s.getRange('H2').setNumberFormat(money);bold(s,2,13);
|
||||
if(isDeparted){s.mergeCells('A3:M3');s.getRange('A3').values=[[`已离职:原计算额${audit[2].toFixed(2)}元(含当月、结转及历史补发)不再按旧口径支付,本人实发0元;8月当月达标奖励已按新规则转交接收人,原计算项与接收人追溯见离职人员处理表。`]];s.getRange('A3:M3').format={font:{name:'宋体',size:10,bold:true,color:'#C00000'},wrapText:true,rowHeight:30};}
|
||||
else if(inTransfers.length){const sumT=inTransfers.reduce((a,t)=>a+t.amount,0);s.mergeCells('A3:M3');s.getRange('A3').values=[[`本月含接收转入的离职人员8月当月达标奖励${sumT.toFixed(2)}元,已并入本月实发合计;转入车辆与源考核明细见离职人员处理表。`]];s.getRange('A3:M3').format={font:{name:'宋体',size:10,bold:true,color:'#C00000'},wrapText:true,rowHeight:30};}
|
||||
header(s,4,['车牌号','考核目标','月奖励',...Array.from({length:8},(_,i)=>`${i+1}月里程/目标`),'累计里程/目标','达标']);let row=5;
|
||||
for(const g of ownGroups){const r=rec(g),items=isDeparted?(legacyByPlate.get(g.plate)||[]):d.ledger.filter(l=>l.plate===g.plate&&l.sales===p);write(s,row,[g.plate,r['考核目标']||meta.vehicles[g.plate]?.[4]||'',g.reward,...g.history.map(([t,a])=>t===null?'-':`${a.toFixed(2)}/${t.toFixed(2)}`),`${g.cum_actual.toFixed(2)}/${g.cum_target.toFixed(2)}`,g.cum_qualified?'达标':'未达标']);grid(s,`A${row}:M${row}`);s.getRange(`A${row}`).format.font={bold:true};s.getRange(`B${row}`).format.wrapText=true;s.getRange(`A${row}:M${row}`).format.rowHeight=34;row++;
|
||||
header(s,row,['','发放项','金额','说明','结转余额']);s.mergeCells(`E${row}:G${row}`);row++;
|
||||
if(items.length){for(const l of items){if(isDeparted){const note=l.category==='当月'?'旧当月口径,已由8月达标记录重新生成后转交接收人':'离职人员结转/历史补发取消,本人不发';write(s,row,['',l.category,l.gross_amount,note,null]);}else{const bnote=l.transfer?'8月当月达标奖励,接收转入':(l.virtual?'历史结转,本月无考核记录':l.category==='结转'?'整月结转,不与当月奖励重复':'奖励基准×考核天数/对应月天数');write(s,row,['',l.category,null,bnote,null]);s.getRange(`C${row}`).formulas=[[`='8月奖金发放记录'!J${l.id+1}`]];}s.mergeCells(`D${row}:G${row}`);grid(s,`A${row}:G${row}`);row++;}}
|
||||
else{write(s,row,['','本月不发',0,isDeparted?'本人无发放项':'未满足本月奖励、结转或补发条件',null]);s.mergeCells(`D${row}:G${row}`);grid(s,`A${row}:G${row}`);row++;}
|
||||
write(s,row,['','本月合计',null,isDeparted?'实发0,原计算额不再支付':'已计入个人汇总',`上期${g.previous_carry}期,本期剩余${g.carry_left}期`]);s.getRange(`C${row}`).formulas=[[isDeparted?'=0':`=${sum(`"${p}"`,null,`"${g.plate}"`)}`]];s.mergeCells(`E${row}:G${row}`);grid(s,`A${row}:G${row}`);bold(s,row,3);row+=2;
|
||||
}
|
||||
if(inTransfers.length){header(s,row,['车牌号','原考核目标','8月里程/目标','发放项','转入金额','说明']);s.mergeCells(`F${row}:M${row}`);row++;
|
||||
for(const t of inTransfers){const g=d.groups.find(x=>x.plate===t.plate&&x.sales===t.owner_sales)||{},r=rec({plate:t.plate,sales:t.owner_sales}),lid=transferLedgerId(t);write(s,row,[t.plate,r['考核目标']||meta.vehicles[t.plate]?.[4]||'',`${(g.actual||0).toFixed(2)}/${(g.target||0).toFixed(2)}`,t.category,null,'离职人员当月达标转入(接收)']);s.getRange(`E${row}`).formulas=[[`='8月奖金发放记录'!J${lid+1}`]];s.getRange(`E${row}`).setNumberFormat(money);s.mergeCells(`F${row}:M${row}`);grid(s,`A${row}:E${row}`);row++;}
|
||||
write(s,row,['转入合计',null,null,null,null,null]);s.getRange(`E${row}`).formulas=[[`=SUM(E${row-inTransfers.length}:E${row-1})`]];s.getRange(`E${row}`).setNumberFormat(money);bold(s,row,5);row+=2;
|
||||
}
|
||||
write(s,row,['合计',null,null]);s.getRange(`C${row}`).formulas=[['=H2']];bold(s,row);s.getRange(`C5:C${row}`).setNumberFormat(money);controls.people[p]={sheet:personalName(p),total:`C${row}`,groups:ownGroups.length};previews.push([personalName(p),`A1:M${Math.min(13,row)}`]);
|
||||
}
|
||||
// 客观缺失日:全部所需来源缺失的记录保留“待核实”,不写成实测0,也不使用“暂算”表述。
|
||||
const empty=new Set(d.records.filter(r=>r['缺口天数']===r['考核天数']).map(r=>r['源表行']));
|
||||
dr.forEach((r,i)=>{if(empty.has(r['源表行'])){detail.getRange(`M${i+2}`).values=[['待核实']];detail.getRange(`W${i+2}`).values=[['全部所需来源缺失,已有计入0为累计值而非实测0,补数后重算。']];}});
|
||||
|
||||
const comparison=JSON.parse(await fs.readFile(sup+'/comparison.json','utf8'));
|
||||
const changes=JSON.parse(await fs.readFile(sup+'/bonus_changes.json','utf8'));
|
||||
const summary=JSON.parse(await fs.readFile(sup+'/recalculation_summary.json','utf8'));
|
||||
const paid=JSON.parse(await fs.readFile(sup+'/historical_paid.json','utf8'));
|
||||
const states=JSON.parse(await fs.readFile(sup+'/historical_states.json','utf8'));
|
||||
const rev=sheet('历史里程修正',10,comparison.length+1);
|
||||
table(rev,['月份','车牌号','业务负责人','原考核里程(km)','纯氢里程(km)','修正里程(km)','减少里程(km)','取数说明','考核区间','历史源表行'],comparison.map(x=>[x.month,x.plate,x.sales,x.original,x.hydrogen,null,null,x.note,x.intervals,x.source_rows]),{A:9,B:17,C:14,D:20,E:20,F:20,G:20,H:36,I:66,J:16},['D','E','F','G']);
|
||||
rev.getRange(`F2:G${comparison.length+1}`).formulas=comparison.map((_,i)=>{let r=i+2;return[`=IF(ISNUMBER(E${r}),MIN(D${r},E${r}),D${r})`,`=D${r}-F${r}`];});
|
||||
rev.freezePanes.freezeRows(1);rev.freezePanes.freezeColumns(3);rev.tables.add(`A1:J${comparison.length+1}`,true,'HydrogenRevision');
|
||||
rev.getRange('L1').values=[['来源及取数口径']];rev.getRange('L2').values=[['纯氢:氢能源行驶里程9.14.rar,1月17日由补充同名文件替换。']];rev.getRange('L3').values=[['原里程:8月原表历史月份数据,已逐车逐人逐月核对历史考核源表。']];rev.getRange('L4').values=[['按原考核区间合计,同车同月同负责人先汇总后取小值。真实0按0。']];rev.getRange('L5').values=[['7—8月里程沿用原表。纯氢源数据变更后需重新运行核算。']];rev.getRange('L1:L5').format.columnWidth=88;
|
||||
const delta=sheet('8月调整对照',7,70);widths(delta,{A:19,B:16,C:22,D:18,E:18,F:18,G:65},70);
|
||||
write(delta,1,['8月纯氢里程修正结果']);bold(delta,1,7);
|
||||
header(delta,3,['项目','原表实发(元)','修正实发(元)','差额(元)']);
|
||||
let rr=4;for(const [cat,v] of Object.entries(summary.categories)){write(delta,rr,[cat,v.old,null,null]);delta.getRange(`C${rr}`).formulas=[[`=SUMIFS(${L('J')},${L('E')},A${rr})`]];delta.getRange(`D${rr}`).formulas=[[`=C${rr}-B${rr}`]];rr++;}
|
||||
write(delta,14,['合计']);delta.getRange('B14:D14').formulas=[['=SUM(B4:B13)','=SUM(C4:C13)','=C14-B14']];bold(delta,14,4);delta.getRange('B4:D14').setNumberFormat(money);
|
||||
write(delta,16,['1—7月已发保留,不追扣;已支付的考核月份不重复补发。']);
|
||||
write(delta,17,['保留原表何文斌粤AGP5613当月奖励扣减9.68元。']);
|
||||
write(delta,18,['结转取消后,按8月当月达标或累计达标条件重新判定本期奖励类别。']);
|
||||
header(delta,20,['车牌号','业务负责人','发放类别','原实发(元)','新实发(元)','差额(元)','变化原因']);
|
||||
changes.forEach((x,i)=>{const row=21+i;write(delta,row,[x.plate,x.sales,x.category,x.old,x.new,null,x.category==='结转'?'修正后上期可用结转不足':x.new===0?'修正后累计里程未达标,取消本次补发':'结转取消后,重新判定本期达标奖励']);delta.getRange(`F${row}`).formulas=[[`=E${row}-D${row}`]];});
|
||||
delta.getRange(`D21:F${20+changes.length}`).setNumberFormat(money);grid(delta,`A20:G${20+changes.length}`);
|
||||
const hist=sheet('历史已发台账',9,paid.length+16);
|
||||
table(hist,['发放月','考核月','车牌号','考核负责人','实际接收人','发放类别','已发金额(元)','历史最终表','源表行'],paid.map(x=>[x.payment_month,x.assessment_month,x.plate,x.owner,x.recipient,x.category,x.amount,x.source,x.source_row]),{A:12,B:12,C:18,D:16,E:16,F:21,G:20,H:45,I:12},['G']);hist.freezePanes.freezeRows(1);
|
||||
header(hist,paid.length+4,['月份','历史最终合计','逐项金额合计','取分差额']);
|
||||
summary.payment_controls.forEach((x,i)=>write(hist,paid.length+5+i,[x.month,x.final_total,x.row_total,x.rounding_difference]));hist.getRange(`B${paid.length+5}:D${paid.length+11}`).setNumberFormat(money);
|
||||
write(hist,paid.length+13,['历史最终合计原样保留。逐项取分与总额取分差0.01—0.03元不作追调;不影响已发资格。']);
|
||||
const st=sheet('历史结转状态',10,states.length+1);
|
||||
table(st,['月份','车牌号','业务负责人','累计应考核(km)','修正累计实际(km)','累计获得期数','实际已用期数','可用结转期数','本考核月已发','历史无考核结转'],states.map(x=>[x.month,x.plate,x.sales,x.cum_target,x.cum_actual,x.earned,x.used,null,x.paid?'是':'否',x.virtual?'是':'否']),{A:10,B:18,C:15,D:23,E:23,F:19,G:19,H:19,I:19,J:23},['D','E']);
|
||||
st.getRange(`H2:H${states.length+1}`).formulas=states.map((_,i)=>[`=MAX(0,F${i+2}-G${i+2})`]);st.freezePanes.freezeRows(1);
|
||||
previews.push(['历史里程修正','A1:H12'],['8月调整对照','A1:D18'],['8月调整对照','A20:G32'],['历史已发台账','A1:I12'],['历史结转状态','A1:J12']);
|
||||
w.recalculate();
|
||||
comparison.forEach((x,i)=>{if(Math.abs(rev.getRange(`F${i+2}`).values[0][0]-x.corrected)>1e-6)throw Error('Mileage min mismatch');});
|
||||
if(Math.abs(delta.getRange('D14').values[0][0]-summary.difference)>1e-6)throw Error('Revision difference mismatch');
|
||||
|
||||
w.recalculate();
|
||||
if(Math.abs(modern.getRange('M22').values[0][0]-d.stats.amount)>1e-6)throw Error('Total changed');
|
||||
if(Math.abs(ledger.getRange(`G${dataEnd+1}`).values[0][0]-d.stats.gross_amount)>1e-6||Math.abs(ledger.getRange(`I${dataEnd+1}`).values[0][0]-d.stats.withheld_amount)>1e-6||Math.abs(ledger.getRange(`J${dataEnd+1}`).values[0][0]-d.stats.amount)>1e-6)throw Error('Ledger totals mismatch');
|
||||
d.ledger.forEach((l,i)=>{const g=ledger.getRange(`G${i+2}`).values[0][0],ii=ledger.getRange(`I${i+2}`).values[0][0],j=ledger.getRange(`J${i+2}`).values[0][0];if(Math.abs(g-l.gross_amount)>1e-6||Math.abs(ii-l.withheld)>1e-6||Math.abs(j-l.amount)>1e-6)throw Error('Ledger row mismatch '+l.id);});
|
||||
// 离职四人姓名不得出现在两版汇总与整张发放表可见文字中。
|
||||
for(const nm of (departed.people||[])){for(const [sh,rng] of [['8月汇总','A1:C210'],['8月汇总(新版界面)','A1:M40'],['8月奖金发放记录',`A1:O${dataEnd+5}`]]){const vals=(w.worksheets.getItem(sh).getRange(rng).values||[]).flat().join('|');if(vals.includes(nm))throw Error(`离职姓名残留: ${nm} @ ${sh}`);}}
|
||||
const probeIndex=d.ledger.findIndex(l=>l.days===l.month_days);
|
||||
const probeCell=`K${probeIndex+2}`;
|
||||
const probe=ledger.getRange(probeCell).values[0][0];ledger.getRange(probeCell).values=[[probe+1]];w.recalculate();if(Math.abs(modern.getRange('M22').values[0][0]-(d.stats.amount+1))>1e-6)throw Error('Formula update failed');ledger.getRange(probeCell).values=[[probe]];w.recalculate();
|
||||
const errors=await w.inspect({kind:'match',searchTerm:'#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A|#NUM!|#NULL!',options:{useRegex:true,maxResults:20},maxChars:2000});await fs.writeFile(sup+'/restyle_formula_scan.json',errors.ndjson);
|
||||
for(const [i,[name,range]]of previews.entries()){const img=await w.render({sheetName:name,range,scale:1});await fs.writeFile(`${sup}/restyle_${i}.png`,new Uint8Array(await img.arrayBuffer()));}
|
||||
await fs.writeFile(sup+'/restyle_controls.json',JSON.stringify({...controls,previews},null,2));
|
||||
const output=await SpreadsheetFile.exportXlsx(w);await output.save(file);console.log('Saved restyled workbook; total '+d.stats.amount.toFixed(2)+'.');
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"scope": "1—6月",
|
||||
"group_by": [
|
||||
"车辆",
|
||||
"月份",
|
||||
"业务负责人"
|
||||
],
|
||||
"mileage": "按原考核区间汇总纯氢里程,与8月原表对应月考核里程取小值",
|
||||
"missing_hydrogen": "保留原考核里程,并注明无纯氢数据",
|
||||
"historical_payment": "1—7月最终表作为实际已发,保留不追扣;已发月份不重复补发;扣除实际已使用结转",
|
||||
"july_august": "7—8月考核里程沿用原表",
|
||||
"gps_meter": "不再取数或判定测试车辆"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
O=Path(__file__).resolve().parent
|
||||
S=Path('/Users/shishengliang/Documents/羚牛/里程考核核算/8月/资料检查/运行支持')
|
||||
s=(S/'restyle_august.mjs').read_text()
|
||||
s=s.replace("const sup=root+'/资料检查/运行支持', file=root+'/8月核算结果/里程任务考核_8月核算.xlsx';",f"const sup={str(O)!r}, file=sup+'/里程任务考核_8月核算_纯氢修正版.xlsx';")
|
||||
s=s.replace("d.ledger.map(()=>[0])","d.ledger.map(l=>[l.withheld])")
|
||||
s=s.replace("Math.abs(ii)>1e-6","Math.abs(ii-l.withheld)>1e-6")
|
||||
s=s.replace('本次无停发扣减','保留原表扣减9.68元').replace('无停发扣减','保留原表扣减9.68元')
|
||||
s=s.replace('本次应发=实发','本次实发')
|
||||
s=s.replace('7月复算62,616.73元与历史一致。','1—7月实际已发保留,不追扣、不重复补发。1—6月按纯氢与原考核里程取小值,无纯氢则保留原里程。')
|
||||
s=s.replace("const note='';return[l.plate", "const note=l.withhold_reason||'';return[l.plate")
|
||||
s=s.replace("modern.getRange('A2:H3').format=", "modern.getRange('A2').values=[['1—6月历史里程已修正,1—7月实际已发保留。修正前后差额见8月调整对照。金额按发放项取分后汇总。']];modern.getRange('A2:H3').format=")
|
||||
extra=r'''
|
||||
const comparison=JSON.parse(await fs.readFile(sup+'/comparison.json','utf8'));
|
||||
const changes=JSON.parse(await fs.readFile(sup+'/bonus_changes.json','utf8'));
|
||||
const summary=JSON.parse(await fs.readFile(sup+'/recalculation_summary.json','utf8'));
|
||||
const paid=JSON.parse(await fs.readFile(sup+'/historical_paid.json','utf8'));
|
||||
const states=JSON.parse(await fs.readFile(sup+'/historical_states.json','utf8'));
|
||||
const rev=sheet('历史里程修正',10,comparison.length+1);
|
||||
table(rev,['月份','车牌号','业务负责人','原考核里程(km)','纯氢里程(km)','修正里程(km)','减少里程(km)','取数说明','考核区间','历史源表行'],comparison.map(x=>[x.month,x.plate,x.sales,x.original,x.hydrogen,null,null,x.note,x.intervals,x.source_rows]),{A:9,B:17,C:14,D:20,E:20,F:20,G:20,H:36,I:66,J:16},['D','E','F','G']);
|
||||
rev.getRange(`F2:G${comparison.length+1}`).formulas=comparison.map((_,i)=>{let r=i+2;return[`=IF(ISNUMBER(E${r}),MIN(D${r},E${r}),D${r})`,`=D${r}-F${r}`];});
|
||||
rev.freezePanes.freezeRows(1);rev.freezePanes.freezeColumns(3);rev.tables.add(`A1:J${comparison.length+1}`,true,'HydrogenRevision');
|
||||
rev.getRange('L1').values=[['来源及取数口径']];rev.getRange('L2').values=[['纯氢:氢能源行驶里程9.14.rar,1月17日由补充同名文件替换。']];rev.getRange('L3').values=[['原里程:8月原表历史月份数据,已逐车逐人逐月核对历史考核源表。']];rev.getRange('L4').values=[['按原考核区间合计,同车同月同负责人先汇总后取小值。真实0按0。']];rev.getRange('L5').values=[['7—8月里程沿用原表。纯氢源数据变更后需重新运行核算。']];rev.getRange('L1:L5').format.columnWidth=88;
|
||||
const delta=sheet('8月调整对照',7,70);widths(delta,{A:19,B:16,C:22,D:18,E:18,F:18,G:65},70);
|
||||
write(delta,1,['8月纯氢里程修正结果']);bold(delta,1,7);
|
||||
header(delta,3,['项目','原表实发(元)','修正实发(元)','差额(元)']);
|
||||
let rr=4;for(const [cat,v] of Object.entries(summary.categories)){write(delta,rr,[cat,v.old,null,null]);delta.getRange(`C${rr}`).formulas=[[`=SUMIFS(${L('J')},${L('E')},A${rr})`]];delta.getRange(`D${rr}`).formulas=[[`=C${rr}-B${rr}`]];rr++;}
|
||||
write(delta,14,['合计']);delta.getRange('B14:D14').formulas=[['=SUM(B4:B13)','=SUM(C4:C13)','=C14-B14']];bold(delta,14,4);delta.getRange('B4:D14').setNumberFormat(money);
|
||||
write(delta,16,['1—7月已发保留,不追扣;已支付的考核月份不重复补发。']);
|
||||
write(delta,17,['保留原表何文斌粤AGP5613当月奖励扣减9.68元。']);
|
||||
write(delta,18,['结转取消后,按8月当月达标或累计达标条件重新判定本期奖励类别。']);
|
||||
header(delta,20,['车牌号','业务负责人','发放类别','原实发(元)','新实发(元)','差额(元)','变化原因']);
|
||||
changes.forEach((x,i)=>{const row=21+i;write(delta,row,[x.plate,x.sales,x.category,x.old,x.new,null,x.category==='结转'?'修正后上期可用结转不足':x.new===0?'修正后累计里程未达标,取消本次补发':'结转取消后,重新判定本期达标奖励']);delta.getRange(`F${row}`).formulas=[[`=E${row}-D${row}`]];});
|
||||
delta.getRange(`D21:F${20+changes.length}`).setNumberFormat(money);grid(delta,`A20:G${20+changes.length}`);
|
||||
const hist=sheet('历史已发台账',9,paid.length+16);
|
||||
table(hist,['发放月','考核月','车牌号','考核负责人','实际接收人','发放类别','已发金额(元)','历史最终表','源表行'],paid.map(x=>[x.payment_month,x.assessment_month,x.plate,x.owner,x.recipient,x.category,x.amount,x.source,x.source_row]),{A:12,B:12,C:18,D:16,E:16,F:21,G:20,H:45,I:12},['G']);hist.freezePanes.freezeRows(1);
|
||||
header(hist,paid.length+4,['月份','历史最终合计','逐项金额合计','取分差额']);
|
||||
summary.payment_controls.forEach((x,i)=>write(hist,paid.length+5+i,[x.month,x.final_total,x.row_total,x.rounding_difference]));hist.getRange(`B${paid.length+5}:D${paid.length+11}`).setNumberFormat(money);
|
||||
write(hist,paid.length+13,['历史最终合计原样保留。逐项取分与总额取分差0.01—0.03元不作追调;不影响已发资格。']);
|
||||
const st=sheet('历史结转状态',10,states.length+1);
|
||||
table(st,['月份','车牌号','业务负责人','累计应考核(km)','修正累计实际(km)','累计获得期数','实际已用期数','可用结转期数','本考核月已发','历史无考核结转'],states.map(x=>[x.month,x.plate,x.sales,x.cum_target,x.cum_actual,x.earned,x.used,null,x.paid?'是':'否',x.virtual?'是':'否']),{A:10,B:18,C:15,D:23,E:23,F:19,G:19,H:19,I:19,J:23},['D','E']);
|
||||
st.getRange(`H2:H${states.length+1}`).formulas=states.map((_,i)=>[`=MAX(0,F${i+2}-G${i+2})`]);st.freezePanes.freezeRows(1);
|
||||
previews.push(['历史里程修正','A1:H12'],['8月调整对照','A1:D18'],['8月调整对照','A20:G32'],['历史已发台账','A1:I12'],['历史结转状态','A1:J12']);
|
||||
w.recalculate();
|
||||
comparison.forEach((x,i)=>{if(Math.abs(rev.getRange(`F${i+2}`).values[0][0]-x.corrected)>1e-6)throw Error('Mileage min mismatch');});
|
||||
if(Math.abs(delta.getRange('D14').values[0][0]-summary.difference)>1e-6)throw Error('Revision difference mismatch');
|
||||
'''
|
||||
s=s.replace('w.recalculate();\nif(Math.abs(modern',extra+'\nw.recalculate();\nif(Math.abs(modern',1)
|
||||
(O/'build_revision.mjs').write_text(s)
|
||||
shutil.copy2(S/'july_style_metadata.json',O/'july_style_metadata.json')
|
||||
@@ -0,0 +1,163 @@
|
||||
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))
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"old_amount": 50997.73,
|
||||
"new_amount": 45065.47,
|
||||
"difference": -5932.26,
|
||||
"comparison_groups": 2308,
|
||||
"reduced_groups": 1594,
|
||||
"missing_groups": 302,
|
||||
"reduced_km": 450477.23,
|
||||
"categories": {
|
||||
"当月": {
|
||||
"old": 26723.21,
|
||||
"new": 27519.34
|
||||
},
|
||||
"结转": {
|
||||
"old": 12990.0,
|
||||
"new": 11460.0
|
||||
},
|
||||
"补发1月": {
|
||||
"old": 0.0,
|
||||
"new": 0.0
|
||||
},
|
||||
"补发2月": {
|
||||
"old": 0.0,
|
||||
"new": 0.0
|
||||
},
|
||||
"补发3月": {
|
||||
"old": 0.0,
|
||||
"new": 0.0
|
||||
},
|
||||
"补发4月": {
|
||||
"old": 0.0,
|
||||
"new": 0.0
|
||||
},
|
||||
"补发5月": {
|
||||
"old": 0.0,
|
||||
"new": 0.0
|
||||
},
|
||||
"补发6月": {
|
||||
"old": 1150.0,
|
||||
"new": 150.0
|
||||
},
|
||||
"补发7月": {
|
||||
"old": 827.42,
|
||||
"new": 150.0
|
||||
},
|
||||
"累计补发8月": {
|
||||
"old": 9307.1,
|
||||
"new": 5786.13
|
||||
}
|
||||
},
|
||||
"payment_controls": [
|
||||
{
|
||||
"month": 1,
|
||||
"final_total": 21212.26,
|
||||
"row_total": 21212.27,
|
||||
"rounding_difference": 0.01
|
||||
},
|
||||
{
|
||||
"month": 2,
|
||||
"final_total": 21152.14,
|
||||
"row_total": 21152.15,
|
||||
"rounding_difference": 0.01
|
||||
},
|
||||
{
|
||||
"month": 3,
|
||||
"final_total": 56907.1,
|
||||
"row_total": 56907.08,
|
||||
"rounding_difference": -0.02
|
||||
},
|
||||
{
|
||||
"month": 4,
|
||||
"final_total": 86848.08,
|
||||
"row_total": 86848.09,
|
||||
"rounding_difference": 0.01
|
||||
},
|
||||
{
|
||||
"month": 5,
|
||||
"final_total": 92912.56,
|
||||
"row_total": 92912.54,
|
||||
"rounding_difference": -0.02
|
||||
},
|
||||
{
|
||||
"month": 6,
|
||||
"final_total": 93696.88,
|
||||
"row_total": 93696.88,
|
||||
"rounding_difference": -0.0
|
||||
},
|
||||
{
|
||||
"month": 7,
|
||||
"final_total": 62616.73,
|
||||
"row_total": 62616.76,
|
||||
"rounding_difference": 0.03
|
||||
}
|
||||
],
|
||||
"changes": 22
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"people": {
|
||||
"何文斌": {
|
||||
"sheet": "一部-何文斌",
|
||||
"total": "C20",
|
||||
"groups": 3
|
||||
},
|
||||
"刘念念": {
|
||||
"sheet": "二部-刘念念",
|
||||
"total": "C235",
|
||||
"groups": 46
|
||||
},
|
||||
"吴纬涛": {
|
||||
"sheet": "三部-吴纬涛",
|
||||
"total": "C360",
|
||||
"groups": 71
|
||||
},
|
||||
"尚建华": {
|
||||
"sheet": "二部-尚建华",
|
||||
"total": "C270",
|
||||
"groups": 53
|
||||
},
|
||||
"秦挺": {
|
||||
"sheet": "五部-秦挺",
|
||||
"total": "C145",
|
||||
"groups": 28
|
||||
},
|
||||
"童军林": {
|
||||
"sheet": "综合部-童军林",
|
||||
"total": "C10",
|
||||
"groups": 1
|
||||
},
|
||||
"董剑煜": {
|
||||
"sheet": "二部-董剑煜",
|
||||
"total": "C125",
|
||||
"groups": 24
|
||||
},
|
||||
"谯云": {
|
||||
"sheet": "二部-谯云",
|
||||
"total": "C95",
|
||||
"groups": 18
|
||||
},
|
||||
"赵伟军": {
|
||||
"sheet": "综合部-赵伟军",
|
||||
"total": "C25",
|
||||
"groups": 4
|
||||
},
|
||||
"金可鹏": {
|
||||
"sheet": "三部-金可鹏",
|
||||
"total": "C235",
|
||||
"groups": 46
|
||||
},
|
||||
"钟祥": {
|
||||
"sheet": "跨部门-钟祥",
|
||||
"total": "C322",
|
||||
"groups": 63
|
||||
},
|
||||
"陈高伟": {
|
||||
"sheet": "一部-陈高伟",
|
||||
"total": "C20",
|
||||
"groups": 3
|
||||
}
|
||||
},
|
||||
"ledgerRows": {},
|
||||
"oldTotal": "B94",
|
||||
"previews": [
|
||||
[
|
||||
"考核奖励规则",
|
||||
"A1:F18"
|
||||
],
|
||||
[
|
||||
"里程明细8月",
|
||||
"A1:H12"
|
||||
],
|
||||
[
|
||||
"8月奖金发放记录",
|
||||
"A1:O12"
|
||||
],
|
||||
[
|
||||
"8月计算过程",
|
||||
"A1:M12"
|
||||
],
|
||||
[
|
||||
"车辆考核追踪",
|
||||
"A1:G12"
|
||||
],
|
||||
[
|
||||
"8月汇总",
|
||||
"A1:C19"
|
||||
],
|
||||
[
|
||||
"8月汇总",
|
||||
"A88:C94"
|
||||
],
|
||||
[
|
||||
"8月汇总(新版界面)",
|
||||
"A1:M36"
|
||||
],
|
||||
[
|
||||
"一部-何文斌",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"二部-刘念念",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"三部-吴纬涛",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"二部-尚建华",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"五部-秦挺",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"综合部-童军林",
|
||||
"A1:M10"
|
||||
],
|
||||
[
|
||||
"二部-董剑煜",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"二部-谯云",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"综合部-赵伟军",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"三部-金可鹏",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"跨部门-钟祥",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"一部-陈高伟",
|
||||
"A1:M13"
|
||||
],
|
||||
[
|
||||
"历史里程修正",
|
||||
"A1:H12"
|
||||
],
|
||||
[
|
||||
"8月调整对照",
|
||||
"A1:D18"
|
||||
],
|
||||
[
|
||||
"8月调整对照",
|
||||
"A20:G32"
|
||||
],
|
||||
[
|
||||
"历史已发台账",
|
||||
"A1:I12"
|
||||
],
|
||||
[
|
||||
"历史结转状态",
|
||||
"A1:J12"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"kind":"notice","message":"Cell search matched 0 entries."}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"total": 45065.47,
|
||||
"difference": -5932.26,
|
||||
"sheets": 23,
|
||||
"ledger_items": 170,
|
||||
"missing_hydrogen_groups": 302,
|
||||
"overlap_days_deduplicated": 14,
|
||||
"formula_errors": [],
|
||||
"august_detail_unchanged": true,
|
||||
"historical_paid_no_duplicate": true,
|
||||
"monthly_baseline_reproduced": true
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
from pathlib import Path
|
||||
import json,openpyxl
|
||||
from collections import Counter
|
||||
P=Path(__file__).resolve().parent
|
||||
d=json.loads((P/'august_calculation.json').read_text());s=json.loads((P/'recalculation_summary.json').read_text())
|
||||
comp=json.loads((P/'comparison.json').read_text());paid=json.loads((P/'historical_paid.json').read_text())
|
||||
w=openpyxl.load_workbook(P/'里程任务考核_8月核算_纯氢修正版.xlsx',data_only=True)
|
||||
orig=openpyxl.load_workbook('/Users/shishengliang/Documents/羚牛/里程考核核算/8月/8月核算结果/里程任务考核_8月核算.xlsx',data_only=True)
|
||||
assert w.sheetnames[:len(orig.sheetnames)]==orig.sheetnames
|
||||
def same(a,b):
|
||||
if a in (None,'') and b in (None,''):return True
|
||||
if isinstance(a,(int,float)) and isinstance(b,(int,float)):return abs(a-b)<1e-8
|
||||
return a==b
|
||||
for row in w['里程明细8月']:
|
||||
for c in row:assert same(c.value,orig['里程明细8月'][c.coordinate].value),('August assessment records changed',c.coordinate,c.value,orig['里程明细8月'][c.coordinate].value)
|
||||
assert abs(w['8月汇总(新版界面)']['M22'].value-s['new_amount'])<1e-7
|
||||
rr=list(w['8月奖金发放记录'].values)
|
||||
for row,l in zip(rr[1:],d['ledger']):
|
||||
assert (row[0],row[1],row[4])==(l['plate'],l['sales'],l['category'])
|
||||
assert row[6]==l['gross_amount'] and row[8]==l['withheld'] and row[9]==l['amount']
|
||||
assert abs(w['8月调整对照']['D14'].value-s['difference'])<1e-7
|
||||
for row,x in zip(list(w['历史里程修正'].values)[1:],comp):
|
||||
assert row[5]==x['corrected']
|
||||
assert x['corrected']<=x['original']
|
||||
if x['hydrogen'] is None:assert row[4] is None and row[5]==x['original']
|
||||
if x['hydrogen']==0:assert row[5]==min(0,x['original'])
|
||||
for l in d['ledger']:
|
||||
if l['assessment_month']<8:
|
||||
assert not any(r['plate']==l['plate'] and r['owner']==l['sales'] and r['assessment_month']==l['assessment_month'] for r in paid)
|
||||
errors=[]
|
||||
for sh in w:
|
||||
for row in sh:
|
||||
for c in row:
|
||||
if c.data_type=='e':errors.append((sh.title,c.coordinate,c.value))
|
||||
assert not errors,errors[:10]
|
||||
# Independent cumulative check: same contiguous monthly chain as the established engine;
|
||||
# every historical correction reduces only the matching owner and interval.
|
||||
for g in d['groups']:
|
||||
target=actual=0
|
||||
for t,a in g['history']:
|
||||
if t is None:target=actual=0
|
||||
else:target+=t;actual+=a
|
||||
assert abs(g['cum_target']-target)<.03 and abs(g['cum_actual']-actual)<.03,(g['plate'],g['sales'])
|
||||
result=dict(total=s['new_amount'],difference=s['difference'],sheets=len(w.sheetnames),ledger_items=len(d['ledger']),missing_hydrogen_groups=s['missing_groups'],overlap_days_deduplicated=sum(x['overlap_days'] for x in comp),formula_errors=errors,august_detail_unchanged=True,historical_paid_no_duplicate=True,monthly_baseline_reproduced=True)
|
||||
(P/'verification.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
|
||||
print(json.dumps(result,ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
P=Path(__file__).resolve().parent
|
||||
s=json.loads((P/'recalculation_summary.json').read_text())
|
||||
text=f'''# 8月纯氢里程修正
|
||||
|
||||
已完成。8月原表实发{s['old_amount']:,.2f}元,修正后{s['new_amount']:,.2f}元,减少{-s['difference']:,.2f}元。原正式文件未覆盖,新版为本目录“里程任务考核_8月核算_纯氢修正版.xlsx”。
|
||||
|
||||
## 已执行口径
|
||||
|
||||
- 仅修正1—6月历史里程,按车辆、月份、业务负责人匹配8月原表历史数据。
|
||||
- 纯氢按原考核区间日期集合汇总,同组重叠日期去重,再与原月考核合计取小值。14个重叠车人日期已去重。
|
||||
- 无纯氢数据的302组保留原里程,并标注“无纯氢数据”;真实0参与取小值。
|
||||
- 1—7月最终核算表视为实际已发,不追扣、不重复补发;实际已消耗结转从后续余额中扣除。
|
||||
- 7—8月考核里程沿用原表。本次不再获取GPS或仪表、不再做测试车辆判定。
|
||||
- 保留当前Excel何文斌粤AGP5613的9.68元扣减。旧JSON金额51,007.41元未反映此扣减,本次对照以当前Excel50,997.73元为准。
|
||||
- 四名离职人员8月全部不发、不转交,沿用原表。
|
||||
|
||||
## 结果在8月表中的体现
|
||||
|
||||
- 原有计算过程、车辆追踪、个人对账单及两版汇总均已更新。
|
||||
- “历史里程修正”:原里程、纯氢里程、取小值结果、减少公里及缺失说明。
|
||||
- “8月调整对照”:分类前后金额与22条变化项,其中包括同车奖励类别改列。
|
||||
- “历史已发台账”“历史结转状态”:已发资格和已用结转的追溯依据。
|
||||
|
||||
## 核对与数据边界
|
||||
|
||||
- 原里程结合实际已发台账可逐项复现旧8月金额,确保本次差异来自里程修正。
|
||||
- 已核对导出后汇总、170条发放记录、无重复历史补发、缺失与0的区别,公式错误0处。
|
||||
- 8月考核明细保持原样,纯氢1月17日已使用项目目录补充文件。
|
||||
- 原8月仍有121个车辆日来源缺口,此为原表已有事项,本次未替换7—8月数据。如补齐,应另行重算受影响的8月里程和奖金。
|
||||
- 原历史表5月粤AGR3208(尚建华)里程为-0.1公里,纯氢0公里;按用户取小值规则保留-0.1,未擅自改写原数。
|
||||
- 历史明细逐项取分与最终合计取分差0.01—0.03元,最终已发合计原样保留,不作追调。
|
||||
- 本文件为本次核算结果。更换纯氢源数据后需重新运行核算流程;表内金额及汇总公式可自动计算,但修改对照页不会自动重新判定全部奖金资格。
|
||||
|
||||
按本次已确认规则,不再缺少阻碍1—6月修正的数据。
|
||||
'''
|
||||
(P/'资料核对与待补项.md').write_text(text)
|
||||
# Remove superseded GPS/meter claims from the reusable source auditor.
|
||||
a=P/'audit_sources.py';t=a.read_text();start=t.index("lines=['# 历史纯氢里程修正")
|
||||
t=t[:start]+"print(json.dumps({'jan17_status':jan_status,'summary':summary},ensure_ascii=False,indent=2))\n"
|
||||
t=t.replace(" x['test_decision'] = '1—3月不做测试判定' if m<4 else '待同区间GPS,仪表合计已确认'"," x['test_decision'] = '不再采用测试车辆判定;按同组月合计取小值'")
|
||||
a.write_text(t)
|
||||
@@ -0,0 +1,32 @@
|
||||
# 8月纯氢里程修正
|
||||
|
||||
已完成。8月原表实发50,997.73元,修正后45,065.47元,减少5,932.26元。原正式文件未覆盖,新版为本目录“里程任务考核_8月核算_纯氢修正版.xlsx”。
|
||||
|
||||
## 已执行口径
|
||||
|
||||
- 仅修正1—6月历史里程,按车辆、月份、业务负责人匹配8月原表历史数据。
|
||||
- 纯氢按原考核区间日期集合汇总,同组重叠日期去重,再与原月考核合计取小值。14个重叠车人日期已去重。
|
||||
- 无纯氢数据的302组保留原里程,并标注“无纯氢数据”;真实0参与取小值。
|
||||
- 1—7月最终核算表视为实际已发,不追扣、不重复补发;实际已消耗结转从后续余额中扣除。
|
||||
- 7—8月考核里程沿用原表。本次不再获取GPS或仪表、不再做测试车辆判定。
|
||||
- 保留当前Excel何文斌粤AGP5613的9.68元扣减。旧JSON金额51,007.41元未反映此扣减,本次对照以当前Excel50,997.73元为准。
|
||||
- 四名离职人员8月全部不发、不转交,沿用原表。
|
||||
|
||||
## 结果在8月表中的体现
|
||||
|
||||
- 原有计算过程、车辆追踪、个人对账单及两版汇总均已更新。
|
||||
- “历史里程修正”:原里程、纯氢里程、取小值结果、减少公里及缺失说明。
|
||||
- “8月调整对照”:分类前后金额与22条变化项,其中包括同车奖励类别改列。
|
||||
- “历史已发台账”“历史结转状态”:已发资格和已用结转的追溯依据。
|
||||
|
||||
## 核对与数据边界
|
||||
|
||||
- 原里程结合实际已发台账可逐项复现旧8月金额,确保本次差异来自里程修正。
|
||||
- 已核对导出后汇总、170条发放记录、无重复历史补发、缺失与0的区别,公式错误0处。
|
||||
- 8月考核明细保持原样,纯氢1月17日已使用项目目录补充文件。
|
||||
- 原8月仍有121个车辆日来源缺口,此为原表已有事项,本次未替换7—8月数据。如补齐,应另行重算受影响的8月里程和奖金。
|
||||
- 原历史表5月粤AGR3208(尚建华)里程为-0.1公里,纯氢0公里;按用户取小值规则保留-0.1,未擅自改写原数。
|
||||
- 历史明细逐项取分与最终合计取分差0.01—0.03元,最终已发合计原样保留,不作追调。
|
||||
- 本文件为本次核算结果。更换纯氢源数据后需重新运行核算流程;表内金额及汇总公式可自动计算,但修改对照页不会自动重新判定全部奖金资格。
|
||||
|
||||
按本次已确认规则,不再缺少阻碍1—6月修正的数据。
|
||||
Binary file not shown.
Reference in New Issue
Block a user