Files
mileage-bonus/outputs/hydrogen-history-20260916/build_revision.mjs
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

181 lines
31 KiB
JavaScript
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 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)+'.');