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,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)+'.');
|
||||
Reference in New Issue
Block a user