/* ============ 1. 基础工具 ============ */ function ge(id){return document.getElementById(id);} function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} function fmt(n){n=+n||0;var neg=n<-0.004;if(neg)n=-n;var s=n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,',');return (neg?'-':'')+s;} function fmtN(n){return (+n||0).toFixed(2);} function pnum(v){v=parseFloat(String(v==null?'':v).replace(/[,,\s]/g,''));return isNaN(v)?0:v;} function r2(x){return Math.round((+x+Number.EPSILON)*100)/100;} function roundTo(x,step){if(!step||step<=0)return r2(x);return r2(Math.round(x/step)*step);} function uid(){return Date.now().toString(36)+Math.random().toString(36).slice(2,7);} function pad2(n){return String(n).padStart(2,'0');} function monthEnd(m){var p=m.split('-');return m+'-'+pad2(new Date(+p[0],+p[1],0).getDate());} function nextMonth(m,day){var p=m.split('-');var y=+p[0],mo=+p[1]+1;if(mo>12){mo=1;y++;}return y+'-'+pad2(mo)+'-'+pad2(day);} function nextPeriod(m){var p=m.split('-');var y=+p[0],mo=+p[1]+1;if(mo>12){mo=1;y++;}return y+'-'+pad2(mo);} function curMonth(){var d=new Date();return d.getFullYear()+'-'+pad2(d.getMonth()+1);} function monthIdx(m){var p=m.split('-');return +p[0]*12+(+p[1]-1);} function toast(msg,type){var w=ge('toastWrap');var d=document.createElement('div');d.className='toast '+(type==='ok'?'ok':type==='err'?'err':'');d.innerHTML=(type==='ok'?'✅ ':type==='err'?'❌ ':'💬 ')+esc(msg);w.appendChild(d);setTimeout(function(){d.style.transition='opacity .3s';d.style.opacity='0';setTimeout(function(){d.remove();},320);},3200);} function openDlg(ico,title,bodyHtml,buttons,keep,wide){ge('dlgIco').textContent=ico;ge('dlgTitle').textContent=title;ge('dlgBody').innerHTML=bodyHtml;ge('dlgBox').style.width=wide?'1000px':'640px';var f=ge('dlgFoot');f.innerHTML='';(buttons||[{t:'关闭'}]).forEach(function(b){var btn=document.createElement('button');btn.className='btn '+(b.cls||'');btn.innerHTML=b.t;btn.onclick=function(){if(b.fn){if(b.fn()===false)return;}if(!b.keep)closeDlg();};f.appendChild(btn);});ge('dlgMask').classList.add('show');} function closeDlg(){ge('dlgMask').classList.remove('show');} function askConfirm(msg,fn){openDlg('⚠️','操作确认','
'+esc(msg)+'
',[{t:'取消'},{t:'确认执行',cls:'btn-d',fn:fn}]);} /* ============ 2. Excel 引擎(纯离线 .xlsx 读写:写入带完整样式与界面一致,读取支持模板导入) ============ */ var MiniZip=(function(){ var CRCT=(function(){var t=[],c,n,k;for(n=0;n<256;n++){c=n;for(k=0;k<8;k++)c=c&1?0xEDB88320^(c>>>1):c>>>1;t[n]=c>>>0;}return t;})(); function crc32(u8){var c=0xFFFFFFFF;for(var i=0;i>>8);return (c^0xFFFFFFFF)>>>0;} function strU8(s){return new TextEncoder().encode(s);} function build(files){ // files:[{name,path,data:Uint8Array}] var parts=[],offset=0,central=[]; function push(u8){parts.push(u8);offset+=u8.length;} function hdr(n,len){var b=new Uint8Array(n);var dv=new DataView(b.buffer);return {b:b,dv:dv,n:len};} function u32(v){var b=new Uint8Array(4);new DataView(b.buffer).setUint32(0,v,true);return b;} function u16(v){var b=new Uint8Array(2);new DataView(b.buffer).setUint16(0,v,true);return b;} files.forEach(function(f){ var name=strU8(f.path), crc=crc32(f.data), sz=f.data.length; var loc=new Uint8Array(30+name.length), dv=new DataView(loc.buffer); dv.setUint32(0,0x04034b50,true);dv.setUint16(4,20,true);dv.setUint16(6,0x0800,true);dv.setUint16(8,0,true); dv.setUint16(10,0,true);dv.setUint16(12,0x5281,true);dv.setUint32(14,crc,true); dv.setUint32(18,sz,true);dv.setUint32(22,sz,true);dv.setUint16(26,name.length,true);dv.setUint16(28,0,true); loc.set(name,30); var cd=new Uint8Array(46+name.length), cdv=new DataView(cd.buffer); cdv.setUint32(0,0x02014b50,true);cdv.setUint16(4,20,true);cdv.setUint16(6,20,true);cdv.setUint16(8,0x0800,true);cdv.setUint16(10,0,true); cdv.setUint16(12,0,true);cdv.setUint16(14,0x5281,true);cdv.setUint32(16,crc,true); cdv.setUint32(20,sz,true);cdv.setUint32(24,sz,true);cdv.setUint16(28,name.length,true); cdv.setUint32(42,offset,true);cd.set(name,46); push(loc);push(f.data);central.push(cd); }); var cdStart=offset,cdSize=0; central.forEach(function(c){push(c);cdSize+=c.length;}); var eocd=new Uint8Array(22),ev=new DataView(eocd.buffer); ev.setUint32(0,0x06054b50,true);ev.setUint16(8,files.length,true);ev.setUint16(10,files.length,true); ev.setUint32(12,cdSize,true);ev.setUint32(16,cdStart,true); parts.push(eocd); var total=offset+22,out=new Uint8Array(total),pos=0; /* offset 已含中央目录,EOCD 必须是文件最后 22 字节,否则 Excel 判定损坏 */ parts.forEach(function(p){out.set(p,pos);pos+=p.length;}); return out; } return {build:build}; })(); function xmlEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,'');} function colName(n){var s='';while(n>0){var m=(n-1)%26;s=String.fromCharCode(65+m)+s;n=Math.floor((n-1)/26);}return s;} /* ---- 2.1 样式系统(表头/正文/金额/合计 与软件界面一致) ---- */ var XF={DEF:0,HEAD:1,TXT:2,MONEY:3,MTOTAL:4,TOTAL:5,TITLE:6,INT:7,CENTER:8,NOTE:9,AMT:10}; var STYLE_XML=''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''; function cellXml(ref,v,st){ if(v==null||v==='')return ''; if(typeof v==='number'){if(!isFinite(v))v=0;return ''+v+'';} return ''+xmlEsc(v)+''; } /* sheet:{name,title,note,header,types:'t文本|m金额|i整数|c居中',widths,rows,totalRow,freeze,rowStyles:'title|h|t|note'} */ function buildSheetXml(s){ var types=s.types||[],rows=s.rows||[],rowStyles=s.rowStyles||null; var ncol=types.length; if(s.header&&s.header.length>ncol)ncol=s.header.length; if(s.widths&&s.widths.length>ncol)ncol=s.widths.length; rows.forEach(function(r){if(r&&r.length>ncol)ncol=r.length;}); if(ncol<1)ncol=1; var lead=[]; if(s.title)lead.push({cells:[s.title],st:XF.TITLE}); if(s.note)lead.push({cells:[s.note],st:XF.NOTE}); if(s.header&&s.header.length)lead.push({cells:s.header,st:XF.HEAD}); var freezeAt=lead.length,rowsXml=[],rn=0; function emitRow(cells,stFn){ rn++; var cs=''; for(var c=0;c'+cs+''); } lead.forEach(function(L){emitRow(L.cells,function(){return s.plain?XF.DEF:L.st;});}); rows.forEach(function(r,ri){ var isTotal=s.totalRow&&ri===rows.length-1,rs=rowStyles?rowStyles[ri]:null; emitRow(r||[],function(c){ if(s.plain)return types[c]==='a'?XF.AMT:XF.DEF; var ty=types[c]||'t'; if(rs==='title')return XF.TITLE; if(rs==='h')return XF.HEAD; if(rs==='note')return XF.NOTE; if(rs==='t'||isTotal)return ty==='m'?XF.MTOTAL:XF.TOTAL; return ty==='m'?XF.MONEY:ty==='i'?XF.INT:ty==='c'?XF.CENTER:XF.TXT; }); }); var merges=[]; if(!s.plain){ if(s.title)merges.push('A1:'+colName(ncol)+'1'); if(s.note)merges.push('A2:'+colName(ncol)+'2'); } var sv=''+ (freezeAt>0&&s.freeze!==false&&!s.plain?'':'')+ ''; var colsXml=(s.widths&&s.widths.length)?(''+s.widths.map(function(w,i){return '';}).join('')+''):''; return ''+ ''+ ''+sv+ ''+colsXml+ ''+rowsXml.join('')+''+ (merges.length?''+merges.map(function(m){return '';}).join('')+'':'')+ ''; } function buildXlsx(sheets){ var enc=new TextEncoder(),files=[]; var ct=''+ ''+ ''+ ''+ ''+ ''+ sheets.map(function(s,i){return '';}).join('')+''; files.push({path:'[Content_Types].xml',data:enc.encode(ct)}); files.push({path:'_rels/.rels',data:enc.encode('')}); var wb=''+ ''+ ''+sheets.map(function(s,i){return '';}).join('')+''; files.push({path:'xl/workbook.xml',data:enc.encode(wb)}); files.push({path:'xl/_rels/workbook.xml.rels',data:enc.encode(''+ sheets.map(function(s,i){return '';}).join('')+ '')}); files.push({path:'xl/styles.xml',data:enc.encode(STYLE_XML)}); sheets.forEach(function(s,i){files.push({path:'xl/worksheets/sheet'+(i+1)+'.xml',data:enc.encode(buildSheetXml(s))});}); return MiniZip.build(files); } /* ---- 2.2 ZIP 读取(EOCD→中央目录→逐条解压) ---- */ function u16(u8,p){return u8[p]|(u8[p+1]<<8);} function u32(u8,p){return (u8[p]|(u8[p+1]<<8)|(u8[p+2]<<16)|(u8[p+3]<<24))>>>0;} function zipEntries(u8){ var i=u8.length-22,eocd=-1,lim=Math.max(0,u8.length-22-65535); for(;i>=lim;i--){if(u8[i]===0x50&&u8[i+1]===0x4b&&u8[i+2]===0x05&&u8[i+3]===0x06){eocd=i;break;}} if(eocd<0)throw new Error('不是有效的 .xlsx 文件(ZIP 结构缺失)'); var n=u16(u8,eocd+10),cdOff=u32(u8,eocd+16),p=cdOff,out={}; for(var k=0;ku8.length)throw new Error('Excel 文件数据不完整'); out[name]={method:method,csize:csize,usize:usize,comp:u8.subarray(start,start+csize)}; } return out; } /* 纯 JS 原生 DEFLATE 解压(DecompressionStream 不可用时兜底) */ function inflateRaw(src,usizeHint){ var bitpos=0,out=new Uint8Array(usizeHint>0?usizeHint:1<<16),olen=0; function ensure(n){if(olen+n>out.length){var cap=out.length*2;while(cap>3;if(byteI>=src.length)throw new Error('压缩数据不完整');var b=(src[byteI]>>(bitpos&7))&1;bitpos++;return b;} function bits(n){var v=0;for(var i=0;iolen||dist>32768)throw new Error('窗口距离非法'); ensure(len);var from=olen-dist; for(var i=0;i=29)throw new Error('长度码非法'); var len=LBASE[s]+bits(LEXT[s]); var d=decode(dh);if(d>=30)throw new Error('距离码非法'); copyWin(DBASE[d]+bits(DEXT[d]),len); } } } var fixedLit=null,fixedDist=null; function fixedTables(){ if(fixedLit)return; var L=new Array(288);for(var i=0;i<288;i++)L[i]=i<144?8:(i<256?9:(i<280?7:8)); fixedLit=construct(L,288); fixedDist=construct(new Array(30).fill(5),30); } var isFinal=0; do{ isFinal=bit(); var type=bits(2); if(type===0){ bitpos=(bitpos+7)&~7; var p0=bitpos>>3; if(p0+4>src.length)throw new Error('存储块头不完整'); var LEN=u16(src,p0),NLEN=u16(src,p0+2); if(((~LEN)&0xFFFF)!==NLEN)throw new Error('存储块校验失败'); p0+=4; if(p0+LEN>src.length)throw new Error('存储块数据不完整'); ensure(LEN);for(var i2=0;i20)lens.push(pv);} else if(cs===17){var z=3+bits(3);while(z-->0)lens.push(0);} else{var z2=11+bits(7);while(z2-->0)lens.push(0);} } if(lens.length>hlit+hdist)throw new Error('码长超长'); var lh=construct(lens.slice(0,hlit),hlit); var dh=construct(lens.slice(hlit),hdist); if(!lh)throw new Error('文字/长度表为空'); inflBlock(lh,dh); }else throw new Error('未知压缩块类型'); }while(!isFinal); return out.subarray(0,olen); } function unzipEntry(e){ if(e.method===0)return Promise.resolve(e.comp.slice(0,e.usize||e.comp.length)); if(e.method!==8)return Promise.reject(new Error('不支持的压缩方式('+e.method+'),请用 Excel/WPS 另存为标准 .xlsx')); if(typeof DecompressionStream!=='undefined'){ try{ var ds=new DecompressionStream('deflate-raw'); return new Response(new Blob([e.comp]).stream().pipeThrough(ds)).arrayBuffer() .then(function(ab){return new Uint8Array(ab);}) .catch(function(){return inflateRaw(e.comp,e.usize);}); }catch(err){return Promise.resolve(inflateRaw(e.comp,e.usize));} } return Promise.resolve(inflateRaw(e.comp,e.usize)); } /* ---- 2.3 工作表解析(sharedStrings / inlineStr / 日期序列号) ---- */ function xmlDecodeEnt(s){ return String(s).replace(/&(?:amp|lt|gt|quot|apos|#x?[0-9A-Fa-f]+);/g,function(m){ if(m==='&')return '&';if(m==='<')return '<';if(m==='>')return '>';if(m==='"')return '"';if(m===''')return "'"; var h=m.substring(2,m.length-1); try{return String.fromCodePoint(h.charAt(0)==='x'||h.charAt(0)==='X'?parseInt(h.substring(1),16):parseInt(h,10));}catch(e){return '';} }); } function colToIdx(ref){ var m=/^([A-Za-z]+)/.exec(ref);if(!m)return 0; var s=m[1].toUpperCase(),n=0; for(var i=0;i]*)?>([\s\S]*?)<\/si>/g,m; while((m=re.exec(xml))){ var s='',tre=/]*)?>([\s\S]*?)<\/t>/g,tm; while((tm=tre.exec(m[1])))s+=tm[1]; out.push(xmlDecodeEnt(s)); } return out; } function sheetToAOA(xml,shared){ var result=[],m; var reRow=/]*?)(\/>|>([\s\S]*?)<\/row>)/g; while((m=reRow.exec(xml))){ var attrs=m[1]||'',inner=m[3]||''; var rm2=/r="(\d+)"/.exec(attrs); var rnum=rm2?+rm2[1]:(result.length+1); var row=[]; var reCell=/]*?)(\/>|>([\s\S]*?)<\/c>)/g,cm; while((cm=reCell.exec(inner))){ var cAt=cm[1]||'',cIn=cm[3]||''; var rcm=/r="([A-Za-z]+\d+)"/.exec(cAt); var ci=rcm?colToIdx(rcm[1]):row.length; var tm2=/t="([^"]*)"/.exec(cAt); var ty=tm2?tm2[1]:''; var v=''; if(ty==='inlineStr'){ var parts=cIn.match(/]*)?>([\s\S]*?)<\/t>/g)||[]; v=parts.map(function(p){return p.replace(/^]*)?>/,'').replace(/<\/t>$/,'');}).join(''); }else{ var vm=/]*)?>([\s\S]*?)<\/v>/.exec(cIn); if(vm){ v=vm[1]; if(ty==='s')v=shared[+v]||''; else if(ty==='b')v=(v==='1'?'TRUE':'FALSE'); } } v=xmlDecodeEnt(v).replace(/_x000D_/g,''); if((ty===''||ty==='n'||ty==='str')&&typeof v==='string'&&/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(v))v=parseFloat(v); while(row.length]*>/g)||[]).forEach(function(tag){ var id=/Id="([^"]+)"/.exec(tag),tg=/Target="([^"]+)"/.exec(tag); if(id&&tg)rels[id[1]]=tg[1]; }); var out=[]; (wbXml.match(/]*>/g)||[]).forEach(function(tag){ var nm=/name="([^"]*)"/.exec(tag); var id=/r:id="([^"]+)"/.exec(tag)||/id="([^"]+)"/.exec(tag); if(nm&&id&&rels[id[1]]!=null){ var t=rels[id[1]].replace(/^\//,''); if(t.indexOf('xl/')!==0)t='xl/'+t; out.push({name:xmlDecodeEnt(nm[1]),path:t}); } }); return out; } async function readXlsx(buf){ var u8=new Uint8Array(buf); var entries=zipEntries(u8); async function part(path){ var e=entries[path]; if(!e)return null; var data=await unzipEntry(e); return new TextDecoder('utf-8').decode(data); } var wb=await part('xl/workbook.xml'); if(!wb)throw new Error('缺少 workbook.xml,请确认是标准 .xlsx 文件'); var sharedXml=await part('xl/sharedStrings.xml'); var shared=sharedXml?parseShared(sharedXml):[]; var sheets=parseSheets(wb,(await part('xl/_rels/workbook.xml.rels'))||''); var map={}; for(var i=0;i=0)return xl.sheet(n[j]);} } for(i=0;i1)return a;} return xl.first(); } function findHeader(aoa,aliases){ var maxScan=Math.min(aoa.length,8),best=-1,bestMap=null,bestScore=-1; for(var ri=0;ri=0;}); if(hit&&map[a[0]]==null){map[a[0]]=c;score++;break;} } }); if(score>bestScore){bestScore=score;best=ri;bestMap=map;} } return {row:best,map:bestMap,score:bestScore}; } function showImportDlg(o){ var errs=[],list=[]; for(var ri=(o.hd.row<0?0:o.hd.row+1);ri。请使用系统提供的导入模板(表头不能改动)。'; else html+='
📄
共解析到 '+list.length+' 条有效数据'+(errs.length?(','+errs.length+' 条有误将被跳过'):'')+'。请核对预览后点击「确认导入」。
'; if(list.length){ html+='
'+o.cols.map(function(cl){return '';}).join('')+''+ list.slice(0,50).map(function(it){return ''+o.cols.map(function(cl){ var v=it[cl.key]; if(cl.fmt)v=cl.fmt(v); return ''+esc(v==null?'':v)+'';}).join('')+'';}).join('')+ '
'+esc(cl.label)+'
'; if(list.length>50)html+='
仅预览前 50 条,实际导入共 '+list.length+' 条。
'; } if(errs.length)html+='
⚠️
'+errs.slice(0,30).map(esc).join('
')+(errs.length>30?'
… 等共 '+errs.length+' 条':'')+'
'; openDlg('📥',o.title||'导入预览',html,[ {t:'取消'}, {t:'确认导入',cls:'btn-p',fn:function(){ if(miss.length){toast('缺少必需列,无法导入','err');return false;} if(!list.length){toast('没有可导入的数据','err');return false;} o.apply(list); }} ],false,true); } function normDate(v){ var s=String(v==null?'':v).trim(); if(!s)return ''; var m=/^(\d{4})[-\/年.](\d{1,2})[-\/月.](\d{1,2})日?$/.exec(s); if(m)return m[1]+'-'+pad2(+m[2])+'-'+pad2(+m[3]); var n=pnum(s); if(n>20000&&n<80000){var d=new Date(Math.round((n-25569)*86400000));return d.getUTCFullYear()+'-'+pad2(d.getUTCMonth()+1)+'-'+pad2(d.getUTCDate());} return s; } function fmtMonthCn(m){var p=String(m).split('-');return p[0]+'年'+(+p[1])+'月';} function tplNoteSheet(notes){ return {name:'填写说明',header:['序号','填写说明'],types:['i','t'],widths:[6,110], rows:notes.map(function(t,i){return [i+1,t];})}; } /* ============ 3. 默认配置 ============ */ var STORE_KEY='HZ_PAYROLL_SYS_V1'; var DEF_TAX={basic:5000,brackets:[ {upto:36000,rate:3,quick:0},{upto:144000,rate:10,quick:2520},{upto:300000,rate:20,quick:16920}, {upto:420000,rate:25,quick:31920},{upto:660000,rate:30,quick:52920},{upto:960000,rate:35,quick:85920}, {upto:'',rate:45,quick:181920}]}; var DEF_INS=function(){return {socialMin:0,socialMax:0,fundMin:0,fundMax:0,roundTo:0.01,medicalExtra:0,items:[ {key:'yl',name:'养老保险',p:8,c:16,base:'social'}, {key:'yb',name:'医疗保险',p:2,c:8,base:'social'}, {key:'syb',name:'失业保险',p:0.5,c:0.5,base:'social'}, {key:'gsb',name:'工伤保险',p:0,c:0.5,base:'social'}, {key:'syf',name:'生育保险',p:0,c:0.8,base:'social'}, {key:'gjj',name:'住房公积金',p:12,c:12,base:'fund'}]};}; var DEF_MAP=function(){return { salaryPayable:'221101',socialPayable:'221103',fundPayable:'221104', taxPayable:'222112',personalSocial:'224108',personalFund:'224108',bank:'1002', cashFlow:'cf5',cashPrefixes:'1001,1002,1012',word:'记', auxEnabled:false, payMonth:'next',payDay:10,payMonth2:'next',socialDay:15, tpl1:'计提{Y}年{M}月工资',tpl2:'计提{Y}年{M}月社保及公积金(公司承担)', tpl3:'发放{Y}年{M}月工资并代扣五险一金及个税',tpl4:'缴纳{Y}年{M}月社保及公积金', deptSuffix:true};}; function demoData(){ var d1={id:'d1',name:'综合管理部',salaryAcc:'660209',socialAcc:'660210',fundAcc:'660210',memo:'管理费用'}; var d2={id:'d2',name:'财务部',salaryAcc:'660209',socialAcc:'660210',fundAcc:'660210',memo:'管理费用'}; var d3={id:'d3',name:'销售部',salaryAcc:'660110',socialAcc:'660112',fundAcc:'660112',memo:'销售费用'}; var d4={id:'d4',name:'生产部',salaryAcc:'50010102',socialAcc:'50010102',fundAcc:'50010102',memo:'生产成本-直接人工'}; var d5={id:'d5',name:'研发部',salaryAcc:'660219',socialAcc:'660219',fundAcc:'660219',memo:'管理费用-研发费用'}; function emp(code,name,dept,base,post,merit,allow,hire,ded){ return {id:uid(),code:code,name:name,deptId:dept,hireDate:hire,status:'在职',idcard:'', base:base,post:post,merit:merit,allowance:allow,socialBase:null,fundBase:null, ded:Object.assign({child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0},ded||{}), initCum:{months:0,income:0,insFund:0,special:0,tax:0}}; } var emps=[ emp('E001','张伟明',d1.id,12000,4000,3000,800,'2021-03-15',{child:2000,elder:3000,mortgage:1000}), emp('E002','刘慧',d2.id,9000,2500,1500,500,'2022-07-01',{child:2000,elder:1500,mortgage:1000}), emp('E003','陈国强',d4.id,6500,1200,800,300,'2020-05-20',{child:2000,elder:1500}), emp('E004','王小芳',d4.id,5800,1000,600,300,'2023-02-10',{elder:1500,rent:1500}), emp('E005','李建军',d4.id,6200,1100,700,300,'2019-09-01',{child:2000,child2:0,elder:3000}), emp('E006','赵雪梅',d3.id,7000,2000,3000,600,'2021-11-08',{child:2000,mortgage:1000}), emp('E007','孙浩',d3.id,6000,1500,2500,600,'2023-06-12',{}), emp('E008','周文娟',d5.id,11000,3500,4000,800,'2022-04-18',{child:2000,infant:2000,elder:1500,mortgage:1000}) ]; return {depts:[d1,d2,d3,d4,d5],emps:emps}; } /* ============ 4. 数据层(IndexedDB 主存储 · 容量不受限,localStorage 兜底) ============ */ var DB=null; /* 默认账套:空白账套(演示案例默认不载入,由「系统 · 数据管理」手动载入 / 移除) */ function defaultDB(){ return {meta:{version:1,company:'新账套',seeded:false,createdAt:new Date().toISOString()}, settings:{tax:JSON.parse(JSON.stringify(DEF_TAX)),ins:DEF_INS(),map:DEF_MAP()}, depts:[],emps:[],payroll:{},vchManual:{}}; } /* 演示案例账套(杭州星辰科技有限公司:5 部门 + 8 员工) */ function demoDB(){ var d=demoData(); return {meta:{version:1,company:'杭州星辰科技有限公司',seeded:true,createdAt:new Date().toISOString()}, settings:{tax:JSON.parse(JSON.stringify(DEF_TAX)),ins:DEF_INS(),map:DEF_MAP()}, depts:d.depts,emps:d.emps,payroll:{},vchManual:{}}; } /* ---- 演示数据:默认不载入;在「切换账套」面板或「系统 · 数据管理」点击载入,再次点击即移除 ---- */ function hasDemoCase(){return !!(DB&&DB.meta&&DB.meta.seeded);} function loadDemoCase(){ if(hasDemoCase())return toast('演示数据已载入,如需清空请点击「移除演示数据」','err'); var d=demoData(); function doLoad(){ DB.depts=d.depts.slice();DB.emps=d.emps.slice();DB.meta.seeded=true; if(!DB.meta.company||DB.meta.company==='新账套')DB.meta.company='杭州星辰科技有限公司'; if(!/^\d{4}-\d{2}$/.test(DB.meta.openingMonth||'')){DB.meta.openingMonth=curMonth();DB.meta.period=curMonth();} saveDB();syncAcctName();updatePeriodChip();updateAcctSel();nav(curPage);closeDlg(); toast('演示数据已载入:5 个部门 · 8 名员工','ok'); } if(DB.depts.length||DB.emps.length){ askConfirm('当前账套已有 '+DB.depts.length+' 个部门、'+DB.emps.length+' 名员工。载入演示数据将覆盖现有部门与员工档案(工资表、五险一金方案与设置保留),是否继续?',doLoad); }else doLoad(); } function removeDemoCase(){ if(!hasDemoCase())return toast('当前账套未载入演示数据,无需移除','err'); askConfirm('确认移除演示数据?将清空当前账套的部门、员工档案以及全部工资表与凭证,恢复为空白账套(五险一金方案、个税与科目对照设置保留)。',function(){ DB.depts=[];DB.emps=[];DB.payroll={};DB.vchManual={};DB.meta.seeded=false; if(typeof vchState!=='undefined'&&vchState)vchState.mode={}; saveDB();updatePeriodChip();nav(curPage);closeDlg(); toast('演示数据已移除,账套已恢复为空白账套','ok'); }); } function normalizeDB(){ if(!DB.settings)DB.settings={}; DB.settings.tax=Object.assign(JSON.parse(JSON.stringify(DEF_TAX)),DB.settings.tax||{}); DB.settings.ins=Object.assign(DEF_INS(),DB.settings.ins||{}); DB.settings.map=Object.assign(DEF_MAP(),DB.settings.map||{}); if(!DB.payroll)DB.payroll={};if(!DB.depts)DB.depts=[];if(!DB.emps)DB.emps=[];if(!DB.vchManual)DB.vchManual={}; DB.meta=DB.meta||{version:1,company:'企业工资社保管理系统'}; /* 建账所属期:已有核算数据的老账套取最早月份;新建空账套留空,由建账向导确定 */ if(!/^\d{4}-\d{2}$/.test(DB.meta.openingMonth||'')){ var ms0=Object.keys(DB.payroll).sort(); DB.meta.openingMonth=ms0.length?ms0[0]:''; } /* 当前账期(所属期):缺省取已核算的最晚月份,否则取建账月/系统当月 */ if(!/^\d{4}-\d{2}$/.test(DB.meta.period||'')){ var ms=Object.keys(DB.payroll).sort(); DB.meta.period=ms.length?ms[ms.length-1]:(DB.meta.openingMonth||curMonth()); } } function curPeriod(){ var p=(DB&&DB.meta)?DB.meta.period:''; return /^\d{4}-\d{2}$/.test(p)?p:curMonth(); } function setPeriod(m){ if(!/^\d{4}-\d{2}$/.test(m))return false; /* 所属期不能早于建账所属期(与进销存反接转守卫一致) */ if(DB.meta.openingMonth&&m当前账期(所属期)'+ '
账期决定全系统的默认月份:工作台、工资核算、凭证中心、月末结转均跟随当前账期。
· 月末结转会自动把账期转入下一个月;反结转会按顺序回退账期。
· 此处可手动调整(如补做历史月份、提前建立新年度账期),调整不影响任何历史数据。
'; openDlg('📅','调整所属期(账期)',html,[ {t:'取消'}, {t:'保存',cls:'btn-p',fn:function(){ var v=ge('apMonth').value; if(!/^\d{4}-\d{2}$/.test(v))return toast('请选择有效月份','err'); if(DB.meta.openingMonth&&v'+ '
'+(cur?'🏢':'🗃️')+'
'+ '
'+esc(a.name)+ (cur?'当前':'')+(a.auth?'🔒 已加密':'')+'
'+ '
创建于 '+String(a.createdAt||'').replace('T',' ').substring(0,10)+'
'+ ''+(cur?'使用中':'点击进入 →')+''+ ''+ (REG.accounts.length>1?'':'')+ ''; }).join(''); var demoOn=hasDemoCase(); var html=rows+ '
'+(demoOn?'✅':'🎓')+'
演示数据:'+(demoOn?'已载入':'未载入(当前账套为空)')+'
'+ (demoOn?'当前账套已含示例的 5 个部门与 8 名员工。点击下方「🗑️ 移除演示数据」可清空为空白账套(方案设置保留)。' :'演示数据默认不载入。点击「🎓 载入演示数据」可一键导入示例:5 个部门(综合管理部 / 财务部 / 销售部 / 生产部 / 研发部)+ 8 名员工,含完整工资结构与专项附加扣除,即可立即体验工资核算、五险一金计提、个税累计预扣与凭证生成。')+ '
'+ '
'+ ''+ ''+ ''+ '
'; openDlg('📚','切换账套',html,[{t:'关闭'}],false,true); } /* 面板内一键切换:密码门卫校验通过后关闭面板并进入目标账套 */ function doSwitchBook(id){ var a=getAcct(id); if(!a)return; ensureBookAccess(a).then(function(ok){ if(!ok)return; closeDlg(); if(id===REG.cur)return; continueSwitch(id); }); } /* 切换账套过场幕:全屏加载遮罩(与进销存 showBookLoading 一致,最低展示 520ms) */ function showAcctLoading(name){ var el=ge('acctLoad'); if(!el){ el=document.createElement('div'); el.id='acctLoad';el.className='acct-load'; el.innerHTML='
正在装载该账套全部数据 · 请稍候
'; document.body.appendChild(el); } var nm=el.querySelector('.acct-name'); if(nm)nm.textContent='正在进入「'+(name||'账套')+'」'; requestAnimationFrame(function(){el.classList.add('show');}); } function hideAcctLoading(){ var el=ge('acctLoad'); if(el){el.classList.remove('show');setTimeout(function(){try{el.remove();}catch(e){}},300);} } /* 切换账套(与进销存 switchBook 一致):过场幕 → 换库装载 → 数据隔离清场 → 进工作台 */ async function continueSwitch(id){ var a=REG.accounts.find(function(x){return x.id===id;}); if(!a)return; if(window.__SWITCHING_ACCT)return; window.__SWITCHING_ACCT=true; var same=(REG.cur===id); var t0=Date.now(); showAcctLoading(a.name); try{ if(!same){ closeDlg(); await loadAccount(id); /* 数据隔离清场:重置上一账套遗留的凭证模式/账期年/筛选,防止跨账套串扰 */ vchState.mode={};jzState.year='';empFilter.kw='';empFilter.dept=''; updateAcctSel();updatePeriodChip();nav('dash'); toast('已进入账套「'+a.name+'」','ok'); } }catch(e){ toast('切换账套失败:'+(e.message||e)+'。请按 Ctrl+F5 强制刷新页面后重试','err'); }finally{ var wait=Math.max(0,520-(Date.now()-t0)); setTimeout(function(){hideAcctLoading();window.__SWITCHING_ACCT=false;},wait); } } /* 新建账套(与进销存一致):创建空白账套并进入,随后自动弹出建账向导确定起始所属期 */ async function createAccount(name){ name=(name||'').trim()||('账套'+(REG.accounts.length+1)); var id=uid(); var data=blankAccount(name); data.meta.acctId=id; REG.accounts.push({id:id,name:name,createdAt:new Date().toISOString()}); if(IDB.ok()){try{await IDB.set('acct:'+id,data);}catch(e){}} saveReg(); await continueSwitch(id); /* 等切换过场幕淡出(约 520ms 起淡出)后再弹建账向导,避免向导被加载遮罩盖住 */ if(!DB.meta.openingMonth&&!DB.emps.length&&!Object.keys(DB.payroll).length)setTimeout(dlgBuildAccount,700); return id; } function createAccountDlg(){ openDlg('➕','新建账套', '
📐
创建一个空白账套(每个账套的部门、员工、五险一金方案、个税、科目对照、工资表、凭证、结转完全独立),创建后自动进入并弹出建账向导设置账套名称与建账年月(起始所属期)。
'+ fld('账套名称(可先用默认,建账向导中可再修改)','','naName','账套'+(REG.accounts.length+1),''), [{t:'取消'},{t:'➕ 创建并进入',cls:'btn-p',fn:function(){createAccount(ge('naName').value.trim());}}]); } /* 建账向导:账套名称 + 建账年份/月份 → 起始所属期(与进销存 dlgBuildAccount 一致) */ function dlgBaRefresh(){ var el=ge('baPreview'); if(el&&ge('baYear')&&ge('baMonth'))el.innerHTML='✅ 建账后,系统将以 '+esc(ge('baYear').value||'——')+'年'+(+ge('baMonth').value)+'月 作为起始所属期。'; } /* 建账年份:允许手工输入,强制纯数字且不得超过 4 位 */ function dlgBaYearInput(){ var el=ge('baYear');if(!el)return; var v=String(el.value||'').replace(/\D/g,'').slice(0,4); if(el.value!==v)el.value=v; dlgBaRefresh(); } function dlgBuildAccount(){ var now=new Date(); var pY=now.getFullYear(),pM=now.getMonth()+1; if(/^\d{4}-\d{2}$/.test(curPeriod())){var sp=curPeriod().split('-');pY=+sp[0];pM=+sp[1];} var mOpts='',m2; for(m2=1;m2<=12;m2++)mOpts+=''; var html='
📐
建账需确定 账套名称建账年份建账月份,以此判定系统启用所属期。建账后该月份即起始所属期,后续月份通过「月末结转」逐月推进;反结转与调整所属期都不会早于建账月份。
'+ fld('账套名称','req','baName',(DB.meta.company||''),'例如:XX有限公司')+ '
'+ ''+ '
'+ '
✅ 建账后,系统将以 '+pY+'年'+pM+'月 作为起始所属期。
'; openDlg('📐','建账 · 设置所属期',html,[ {t:'取消'}, {t:'✅ 完成建账',cls:'btn-p',fn:function(){ var nm=ge('baName').value.trim(); if(!nm)return toast('请填写账套名称','err'); var yv=String(ge('baYear').value||'').replace(/\D/g,'').slice(0,4); if(!/^\d{4}$/.test(yv))return toast('建账年份请输入 4 位数字(如 '+new Date().getFullYear()+')','err'); var mk=yv+'-'+pad2(+ge('baMonth').value); DB.meta.company=nm;DB.meta.openingMonth=mk;DB.meta.period=mk; saveDB();syncAcctName();updateAcctSel();updatePeriodChip(); closeDlg(); nav('pay'); toast('🎉 建账成功!起始所属期:'+mk,'ok'); }}]); } function renameAccountDlg(id){ var a=REG.accounts.find(function(x){return x.id===id;}); if(!a)return; var html='
'+fld('账套名称','req','raName',a.name,'')+'
'; openDlg('✏️','重命名账套',html,[ {t:'取消'}, {t:'保存',cls:'btn-p',fn:function(){ var n=ge('raName').value.trim(); if(!n)return toast('请填写账套名称','err'); a.name=n; if(id===REG.cur&&DB.meta){DB.meta.company=n;saveDB();} saveReg();updateAcctSel(); if(curPage==='data')nav('data'); toast('账套已重命名为「'+n+'」','ok'); }}]); } /* 删除账套(与进销存一致):有数据的账套先强制导出删除前备份,再勾选确认;加密账套需先验证密码 */ function deleteAccount(id){ if(REG.accounts.length<=1)return toast('至少保留一个账套,不能删除','err'); var a=getAcct(id); if(!a)return; var doIt=function(){ (async function(){ if(IDB.ok()){try{await IDB.del('acct:'+id);}catch(e){}} var wasCur=REG.cur===id; REG.accounts=REG.accounts.filter(function(x){return x.id!==id;}); if(wasCur)REG.cur=REG.accounts[0].id; saveReg();updateAcctSel(); if(wasCur)await loadAccount(REG.cur); if(curPage==='data')nav('data'); toast('账套「'+a.name+'」已删除','ok'); })(); }; var confirmDeleteDlg=function(){ var html='
⚠️
即将永久删除账套「'+esc(a.name)+'」的全部数据(部门/员工/工资表/凭证/结转),且无法恢复!
'+ ''; openDlg('🗑️','删除账套 — '+a.name,html,[ {t:'取消'}, {t:'确认删除',cls:'btn-d',fn:function(){ if(!ge('dlCk')||!ge('dlCk').checked)return toast('请先勾选确认','err'); doIt(); }} ]); }; var proceed=async function(){ var hasData=false; try{ var d=(a.id===REG.cur)?DB:(IDB.ok()?await IDB.get('acct:'+a.id):null); hasData=!!(d&&d.settings&&((d.emps&&d.emps.length)||Object.keys(d.payroll||{}).length)); }catch(e){} if(hasData){ /* 强制先导出该账套的删除前备份(与进销存删除保护一致) */ (async function(){ try{ var d2=(a.id===REG.cur)?DB:(IDB.ok()?await IDB.get('acct:'+a.id):null); var pack={format:'GZ_SINGLE_BOOK_AUTO',version:2,exportedAt:new Date().toISOString(),bookName:a.name,bookId:id,data:d2}; var blob=(await buildBackupBlob(pack,getBackupPassword())).blob; var buf=new Uint8Array(await blob.arrayBuffer()); downloadBlob(buf,sanitizeFileName(a.name)+'工资社保备份_删除前'+dateStamp()+'.fdb','application/octet-stream'); toast('已自动下载「'+a.name+'」的删除前备份','ok'); }catch(e){} confirmDeleteDlg(); })(); }else confirmDeleteDlg(); }; if(a.auth){ var html='
⚠️
账套「'+esc(a.name)+'」已开启密码保护。请输入该账套的密码以继续删除流程。
'+pwdIn('dlPwd','账套密码')+'
'; openDlg('🗑️','删除加密账套',html,[ {t:'取消'}, {t:'验证密码',cls:'btn-p',fn:function(){ (async function(){ if(!(await verifyAcctPwd(id,ge('dlPwd').value)))return toast('密码不正确,无法删除','err'); proceed(); })();return false; }} ]); return; } proceed(); } /* ---- 4.1c 账套密码与密保(默认不设置;密码用于进入账套验证,密保用于找回) ---- */ var AUTH_QUESTIONS=['您的出生城市是?','母亲的姓名是?','第一所学校的名称是?','您最熟悉的数字?(如工号/门牌号)']; function normAns(s){return String(s==null?'':s).trim().toLowerCase();} function getAcct(id){return REG.accounts.find(function(x){return x.id===id;});} async function hashPwd(pwd,salt){ var data=new TextEncoder().encode(String(salt)+'|'+String(pwd)); try{ if(typeof crypto!=='undefined'&&crypto.subtle){ var buf=await crypto.subtle.digest('SHA-256',data); var arr=new Uint8Array(buf),s=''; for(var i=0;i>>0; return 'f:'+h.toString(16); } async function verifyAcctPwd(id,pwd){ var a=getAcct(id); if(!a||!a.auth)return true; return (await hashPwd(String(pwd==null?'':pwd),a.auth.salt))===a.auth.hash; } async function verifyAcctAns(id,ans){ var a=getAcct(id); if(!a||!a.auth)return true; return (await hashPwd(normAns(ans),a.auth.salt))===a.auth.a; } async function setAccountAuth(id,opts){ var a=getAcct(id);if(!a)return false; var salt=uid(); a.auth={salt:salt,q:String(opts.q||''),hash:await hashPwd(opts.pwd,salt),a:await hashPwd(normAns(opts.ans),salt)}; saveReg();updateAcctSel(); return true; } async function clearAccountAuth(id,pwd){ var a=getAcct(id); if(!a||!a.auth)return false; if(!(await verifyAcctPwd(id,pwd)))return false; delete a.auth;saveReg(); return true; } function pwdIn(id,ph){return '';} function ansIn(id){return '';} function qSelHtml(idSel,idCust){ var opts=AUTH_QUESTIONS.map(function(q,i){return '';}).join(''); return ''+ ''; } function qTextOf(idSel,idCust){ var v=ge(idSel).value; return v==='c'?ge(idCust).value.trim():AUTH_QUESTIONS[+v]; } function authSetDlg(id){ var a=getAcct(id);if(!a)return; var isCh=!!a.auth; var html='
🔒
'+(isCh?('修改账套「'+esc(a.name)+'」的密码保护。'):'为账套「'+esc(a.name)+'」设置密码保护。不设置即为默认无密码。')+'设置后每次进入该账套都需输入密码;忘记密码时可用密保问题找回。
'+ (isCh?'
'+pwdIn('auOld','当前密码(验证后才能修改)')+'
':'')+ '
'+pwdIn('auP1',isCh?'新密码(至少 4 位)':'密码(至少 4 位)')+pwdIn('auP2','确认新密码')+'
'+ '
'+qSelHtml('auQ','auQc')+ansIn('auA')+'
'; openDlg('🔒',(isCh?'修改密码':'设置密码')+' — '+esc(a.name),html,[ {t:'取消'}, {t:'💾 保存',cls:'btn-p',fn:function(){ (async function(){ if(isCh&&!(await verifyAcctPwd(id,ge('auOld').value)))return toast('当前密码不正确','err'); var p1=ge('auP1').value,p2=ge('auP2').value; if(!p1||p1.length<4)return toast('密码至少 4 位(如需取消保护请用「清除密码」)','err'); if(p1!==p2)return toast('两次输入的密码不一致','err'); var q=qTextOf('auQ','auQc'),ans=ge('auA').value; if(!q||!ans.trim())return toast('请填写密保问题与答案(用于找回密码)','err'); await setAccountAuth(id,{pwd:p1,q:q,ans:ans}); closeDlg(); if(curPage==='data')nav('data'); toast('账套「'+a.name+'」密码保护已'+(isCh?'更新':'设置'),'ok'); })();return false; }} ]); } function authClearDlg(id){ var a=getAcct(id); if(!a||!a.auth)return; var html='
⚠️
清除账套「'+esc(a.name)+'」的密码保护后,进入该账套将不再需要密码。请输入当前密码确认。
'+pwdIn('clPwd','当前密码')+'
'; openDlg('🔓','清除密码保护 — '+esc(a.name),html,[ {t:'取消'}, {t:'确认清除',cls:'btn-d',fn:function(){ (async function(){ if(!(await verifyAcctPwd(id,ge('clPwd').value)))return toast('密码不正确','err'); await clearAccountAuth(id,ge('clPwd').value); closeDlg(); if(curPage==='data')nav('data'); toast('密码保护已清除,进入该账套不再需要密码','ok'); })();return false; }} ]); } /* 进入加密账套的锁屏:密码验证 → 忘记密码走密保 → 重设密码/清除 (与进销存 ensureBookAccess 一致:同一浏览器会话内已解锁的账套不再重复询问) */ var UNLOCKED_ACCTS={}; var lockCtx={id:'',mode:'pwd',then:null,noCancel:false,onCancel:null}; function lockEnterDlg(id,then,noCancel,onCancel){ var a=getAcct(id); if(!a||!a.auth){if(then)then();return;} lockCtx={id:id,mode:'pwd',then:then||null,noCancel:!!noCancel,onCancel:onCancel||null}; lockPaint(); } function ensureBookAccess(a){ if(!a)return Promise.resolve(true); if(UNLOCKED_ACCTS[a.id])return Promise.resolve(true); if(!a.auth)return Promise.resolve(true); return new Promise(function(resolve){ lockEnterDlg(a.id,function(){UNLOCKED_ACCTS[a.id]=1;resolve(true);},false,function(){resolve(false);}); }); } function lockPaint(){ var a=getAcct(lockCtx.id); if(!a||!a.auth){closeDlg();if(lockCtx.then)lockCtx.then();return;} var html='',btns=[]; if(lockCtx.mode==='pwd'){ html='
🔒
账套「'+esc(a.name)+'」已开启密码保护,请输入密码进入。
'+ '
'+pwdIn('lkPwd','账套密码')+'
'+ ''; btns=[{t:'🔓 解锁并进入',cls:'btn-p',fn:function(){lockSubmitPwd();return false;}}]; }else if(lockCtx.mode==='ans'){ html='
🔑
密保问题:'+esc(a.auth.q)+'
回答正确后可重新设置密码。
'+ '
'+ansIn('lkAns')+'
'+ ''; btns=[{t:'✅ 验证密保',cls:'btn-p',fn:function(){lockSubmitAns();return false;}}]; }else{ html='
密保验证通过!请设置新密码;两栏都留空并保存 = 清除密码保护。
'+ '
'+pwdIn('lkN1','新密码(至少 4 位,留空=清除)')+pwdIn('lkN2','确认新密码')+'
'+ '
'+qSelHtml('lkQ','lkQc')+ansIn('lkNA')+'
'+ '
设置新密码时需同时填写密保问题与答案(用于下次找回)。
'; btns=[{t:'💾 保存并进入',cls:'btn-p',fn:function(){lockResetSubmit();return false;}}]; } if(!lockCtx.noCancel)btns.push({t:'取消',fn:function(){closeDlg();if(lockCtx.onCancel)lockCtx.onCancel();}}); openDlg(lockCtx.mode==='ans'?'🔑':'🔒','进入账套 — '+esc(a.name),html,btns); } function lockToAnswer(){lockCtx.mode='ans';lockPaint();} async function lockSubmitPwd(){ if(await verifyAcctPwd(lockCtx.id,ge('lkPwd').value)){ UNLOCKED_ACCTS[lockCtx.id]=1; closeDlg();toast('密码验证通过,欢迎回来','ok'); if(lockCtx.then)lockCtx.then(); else if(lockCtx.id!==REG.cur)continueSwitch(lockCtx.id); }else toast('密码不正确,请重试','err'); } async function lockSubmitAns(){ if(await verifyAcctAns(lockCtx.id,ge('lkAns').value)){ lockCtx.mode='reset';lockPaint(); }else toast('密保答案不正确,请重试','err'); } async function lockResetSubmit(){ var p1=ge('lkN1').value,p2=ge('lkN2').value; if(p1!==p2)return toast('两次输入的新密码不一致','err'); if(p1){ if(p1.length<4)return toast('密码至少 4 位(或两栏留空以清除密码保护)','err'); var q=qTextOf('lkQ','lkQc'),ans=ge('lkNA').value; if(!q||!ans.trim())return toast('设置新密码时需同时填写密保问题与答案','err'); await setAccountAuth(lockCtx.id,{pwd:p1,q:q,ans:ans}); toast('新密码已设置','ok'); }else{ delete getAcct(lockCtx.id).auth;saveReg(); toast('密码保护已清除','ok'); } UNLOCKED_ACCTS[lockCtx.id]=1; closeDlg();updateAcctSel(); if(lockCtx.then)lockCtx.then(); else if(lockCtx.id!==REG.cur)continueSwitch(lockCtx.id); } async function loadDB(){ await loadReg(); if(REG.accounts.length){ if(!REG.cur||!REG.accounts.some(function(a){return a.id===REG.cur;}))REG.cur=REG.accounts[0].id; return loadAccount(REG.cur); } /* 旧版单账套数据迁移 */ var legacy=null; if(IDB.ok()){try{legacy=await IDB.get('main');}catch(e){}} if(!legacy){ try{ var raw=localStorage.getItem(STORE_KEY); if(raw){var o=JSON.parse(raw); if(o&&o.data&&o.acctId)legacy=o.data; else if(o&&o.settings)legacy=o; } }catch(e){} } if(legacy&&legacy.settings){DB=legacy;normalizeDB();} else DB=defaultDB(); var id=uid(); DB.meta.acctId=id; REG={accounts:[{id:id,name:DB.meta.company||'默认账套',createdAt:new Date().toISOString()}],cur:id}; saveReg();saveDB(); showBackupIndicator(); } /* ---- 4.2 备份中心(与进销存同款备份逻辑:单账套+全账套双文件 · 可选密码加密 · 五态指示器 · 1.5s 防抖自动触发) ---- */ var BK={dir:null,timer:null}; function dateStamp(){var d=new Date();return d.getFullYear()+String(d.getMonth()+1).padStart(2,'0')+String(d.getDate()).padStart(2,'0');} function sanitizeFileName(name){return String(name||'账套').replace(/[\\/:*?"<>|]/g,'').trim()||'账套';} function getBackupPassword(){return (DB&&DB.meta&&DB.meta.backupPassword)?String(DB.meta.backupPassword):'';} function saveBackupPwd(v){ if(DB&&DB.meta)DB.meta.backupPassword=String(v||''); saveDB(); toast('备份密码已保存'+(getBackupPassword()?',后续备份将自动加密':'(已清空,备份不加密)'),'ok'); } async function verifyPermission(fileHandle,readWrite){ var options={}; if(readWrite)options.mode='readwrite'; try{ if((await fileHandle.queryPermission(options))==='granted')return true; if((await fileHandle.requestPermission(options))==='granted')return true; }catch(e){} return false; } /* fdb 备份格式:未加密即标准 JSON;设置了备份密码则以魔数开头 + AES-GCM(降级 XOR) 密文,恢复时需输入密码 */ var FDB_MAGIC_AES='GZFDBA',FDB_MAGIC_XOR='GZFDBX'; function _fdbXor(data,password){ var pw=String(password||'fdb'),key=[],i; for(i=0;i为了防止数据丢失,建议您立即链接一个本地文件夹作为备份目录。
链接后,系统将在您日常录入操作时自动静默进行全库备份:
· 单账套:账套名称+工资社保备份+日期.fdb
· 全账套:全账套工资社保备份+日期.fdb
每天自动覆盖,保留每天的最后一个备份;可在【数据管理】设置备份密码加密。', [{t:'暂不链接'},{t:'🔗 立即选择备份文件夹',cls:'btn-p',fn:function(){setupBackupFolder();}}]); } /* 恢复:识别加密(询问密码)→ 按包类型恢复(单账套 / 全账套 / 旧版全量格式) */ async function restoreBackupBuffer(buffer,name){ var o; try{ var pwd=''; if(detectBackupEncrypted(buffer)){ pwd=await askBackupPassword(); if(pwd==null)return toast('已取消恢复','err'); } o=await parseBackupBuffer(buffer,pwd); }catch(e){return toast('备份文件无效或密码错误:'+(e.message||e),'err');} return applyRestorePack(o,name); } async function applyRestorePack(o,name){ if(o&&o.format==='GZ_SINGLE_BOOK_AUTO'&&o.data&&o.data.settings){ var nm=o.bookName||'导入账套'; var targetId=(o.bookId&&getAcct(o.bookId))?o.bookId:null; askConfirm('确认用备份'+(name?('('+name+')'):'')+'恢复账套「'+nm+'」?'+(targetId?'将覆盖该账套当前全部数据!':'将作为一个新账套导入。'),function(){ (async function(){ if(!targetId){targetId=uid();REG.accounts.push({id:targetId,name:nm,createdAt:new Date().toISOString()});} var data=o.data; if(!data.meta)data.meta={version:1}; if(!data.meta.company)data.meta.company=nm; data.meta.acctId=targetId; if(IDB.ok()){try{await IDB.set('acct:'+targetId,data);}catch(e){}} saveReg(); if(REG.cur===targetId){DB=data;normalizeDB();saveDB();updatePeriodChip();nav('dash');} else await continueSwitch(targetId); updateAcctSel(); toast('账套数据已从备份恢复','ok'); })(); }); return; } var accs=null,reg=null; if(o&&o.format==='GZ_MULTI_BOOK'&&Array.isArray(o.books)){ reg={accounts:[],cur:'',exportedAt:o.exportedAt}; accs={}; o.books.forEach(function(b,i){ if(!b.data||!b.data.settings)return; var id=(b.bookId&&getAcct(b.bookId))?b.bookId:('r'+i+uid()); accs[id]=b.data; reg.accounts.push({id:id,name:b.name||(b.data.meta&&b.data.meta.company)||'账套',createdAt:(b.data.meta&&b.data.meta.createdAt)||''}); }); if(reg.accounts.length)reg.cur=reg.accounts[0].id;else{accs=null;reg=null;} }else if(o&&o.accounts&&o.reg&&o.reg.accounts&&o.reg.accounts.length){ accs=o.accounts;reg=o.reg;reg.exportedAt=reg.exportedAt||o.exportedAt; } if(!accs)return toast('备份文件格式无法识别','err'); applyMultiRestore(accs,reg,name); } function applyMultiRestore(accs,reg,fromLabel){ var accList=reg.accounts.filter(function(a){return accs[a.id]&&accs[a.id].settings;}); if(!accList.length)return toast('备份中没有有效的账套数据','err'); var names=accList.map(function(a){return a.name;}).join('、'); askConfirm('确认从备份'+(fromLabel?('('+fromLabel+')'):'')+'恢复全部账套?将覆盖当前所有账套的数据!包含账套:'+esc(names)+';备份时间:'+String(reg.exportedAt||'').replace('T',' ').substring(0,16),function(){ (async function(){ REG={accounts:accList,cur:(reg.cur&&accList.some(function(a){return a.id===reg.cur;}))?reg.cur:accList[0].id}; if(IDB.ok()){ for(var i=0;i📁'+esc(BK.dir.name||'备份文件夹')+''+ '已连接 · 自动备份中'+ '每天覆盖保留当天的最后一个备份'+ '
正在读取文件夹…
'; openDlg('📂','备份文件夹',html,[ {t:'💾 立即备份一次',cls:'btn-p',fn:function(){doBackupNow().then(function(r){updateBackupIndicator(r?'done':'lost');setTimeout(renderBkList,50);});}}, {t:'📁 更换文件夹',fn:function(){setupBackupFolder();}}, {t:'⛔ 断开连接',cls:'btn-d',fn:function(){ askConfirm('断开后不再自动写入备份文件夹(已生成的备份文件保留在本文件夹中,本机数据不受影响)。',function(){ BK.dir=null; if(IDB.ok())IDB.set('bkDir',null).catch(function(){}); updateBackupIndicator('unlinked');toast('已断开自动备份连接','ok'); }); return false; }}, {t:'关闭'} ],false,true); renderBkList(); } async function renderBkList(){ var el=ge('bkList');if(!el)return; bkFiles=[]; var files=[]; try{ var it=BK.dir.entries(); for(;;){ var r=await it.next(); if(r.done)break; var name=r.value[0],h=r.value[1]; if(/\.fdb$/i.test(name)){ var f=await h.getFile(); files.push({name:name,size:f.size,at:f.lastModified,handle:h}); } } }catch(e){ el.innerHTML='
读取文件夹失败:'+esc(e.message||e)+'
'; return; } files.sort(function(a,b){return b.at-a.at;}); if(!files.length){ el.innerHTML='
🗂️此文件夹暂无备份文件
数据变动或点击「立即备份一次」后,会自动写入单账套与全账套两个 .fdb 备份
'; return; } bkFiles=files; var html='
'; files.slice(0,200).forEach(function(f,i){ html+=''+ ''; }); html+='
名称大小修改时间操作
💾 '+esc(f.name)+''+(f.size/1024).toFixed(1)+' KB'+new Date(f.at).toLocaleString('zh-CN')+'
共 '+files.length+' 个备份 · 「恢复」按该备份还原账套数据;「下载」另存一份 .fdb 副本。
'; el.innerHTML=html; } async function bkDownload(i){ try{ var f=await bkFiles[i].handle.getFile(); var buf=await f.arrayBuffer(); downloadBlob(new Uint8Array(buf),bkFiles[i].name,'application/octet-stream'); toast('备份文件已下载','ok'); }catch(e){toast('下载失败:'+(e.message||e),'err');} } function bkRestore(i){ bkFiles[i].handle.getFile().then(function(f){return f.arrayBuffer();}).then(function(buf){ restoreBackupBuffer(buf,bkFiles[i].name); }).catch(function(e){toast('读取失败:'+(e.message||e),'err');}); } function bkDelete(i){ askConfirm('确认删除备份文件 '+bkFiles[i].name+' ?',function(){ bkFiles[i].handle.remove().then(function(){toast('已删除','ok');renderBkList();}).catch(function(e){toast('删除失败:'+(e.message||e),'err');}); }); } function deptName(id){var d=DB.depts.find(function(x){return x.id===id;});return d?d.name:'未分配';} function deptOf(id){return DB.depts.find(function(x){return x.id===id;});} function isClosed(m){var r=DB.payroll[m];return !!(r&&r.closed);} /* ============ 5. 个税引擎(累计预扣法) ============ */ function taxFromBrackets(taxable,brackets){ brackets=brackets||DB.settings.tax.brackets; for(var i=0;i0&&sBase0&&sBase>cfg.socialMax)sBase=cfg.socialMax; if(cfg.fundMin>0&&fBase0&&fBase>cfg.fundMax)fBase=cfg.fundMax; var step=cfg.roundTo||0.01,out={sBase:r2(sBase),fBase:r2(fBase),detail:{},pSocial:0,cSocial:0,pFund:0,cFund:0}; items.forEach(function(it){ var base=it.base==='fund'?fBase:sBase; var pv=roundTo(base*(+it.p||0)/100,step),cv=roundTo(base*(+it.c||0)/100,step); out.detail[it.key]={name:it.name,p:pv,c:cv,base:base}; if(it.base==='fund'){out.pFund=r2(out.pFund+pv);out.cFund=r2(out.cFund+cv);} else{out.pSocial=r2(out.pSocial+pv);out.cSocial=r2(out.cSocial+cv);} }); out.pSocial=r2(out.pSocial+(+cfg.medicalExtra||0)); out.pTotal=r2(out.pSocial+out.pFund);out.cTotal=r2(out.cSocial+out.cFund); return out; } /* ============ 7. 工资核算引擎 ============ */ function buildRow(emp,month,input,prior){ input=input||{}; var fixed=r2(pnum(emp.base)+pnum(emp.post)+pnum(emp.merit)+pnum(emp.allowance)); var bonus=pnum(input.bonus),ot=pnum(input.ot),otherAdd=pnum(input.otherAdd); var absence=pnum(input.absence),otherDed=pnum(input.otherDeduct); var gross=r2(fixed+bonus+ot+otherAdd-absence-otherDed); var sBaseIn=(input.socialBase!=null&&input.socialBase!=='')?pnum(input.socialBase):emp.socialBase; var fBaseIn=(input.fundBase!=null&&input.fundBase!=='')?pnum(input.fundBase):emp.fundBase; var ins=calcIns(gross,sBaseIn,fBaseIn); var ded=emp.ded||{}; var special=(input.specialOver!=null&&input.specialOver!=='')?r2(pnum(input.specialOver)):r2(pnum(ded.child)+pnum(ded.infant)+pnum(ded.elder)+pnum(ded.mortgage)+pnum(ded.rent)+pnum(ded.edu)+pnum(ded.other)); var ic=emp.initCum||{months:0,income:0,insFund:0,special:0,tax:0}; var pc=prior[emp.id]||{gross:0,ins:0,special:0,tax:0}; /* 该员工本年更早月份在系统内的已计月份数(逐人判断,避免年中入职者多扣减除费用) */ var priorMonthsInSys=0; Object.keys(DB.payroll).forEach(function(m){ if(m0)v2rows.push({code:map.socialPayable,summary:fillTpl(map.tpl2,month),deb:0,cre:sums.cSocial}); if(sums.cFund>0)v2rows.push({code:map.fundPayable,summary:fillTpl(map.tpl2,month)+'-公积金',deb:0,cre:sums.cFund}); /* 凭证3:工资发放与代扣 */ var v3rows=[{code:map.salaryPayable,summary:fillTpl(map.tpl3,month),deb:sums.gross,cre:0}]; if(sums.pSocial>0)v3rows.push({code:map.personalSocial,summary:'代扣'+yr+'年'+mo+'月社保(个人部分)',deb:0,cre:sums.pSocial}); if(sums.pFund>0)v3rows.push({code:map.personalFund,summary:'代扣'+yr+'年'+mo+'月住房公积金(个人部分)',deb:0,cre:sums.pFund}); if(sums.tax>0)v3rows.push({code:map.taxPayable,summary:'代扣'+yr+'年'+mo+'月个人所得税',deb:0,cre:sums.tax}); v3rows.push({code:map.bank,summary:'实发'+yr+'年'+mo+'月工资',deb:0,cre:sums.net,cf:map.cashFlow}); /* 凭证4:五险一金缴纳 */ var v4rows=[]; if(sums.cSocial>0)v4rows.push({code:map.socialPayable,summary:'缴纳'+yr+'年'+mo+'月社保(公司部分)',deb:sums.cSocial,cre:0}); if(sums.cFund>0)v4rows.push({code:map.fundPayable,summary:'缴纳'+yr+'年'+mo+'月公积金(公司部分)',deb:sums.cFund,cre:0}); if(sums.pSocial>0)v4rows.push({code:map.personalSocial,summary:'缴纳'+yr+'年'+mo+'月社保(个人部分)',deb:sums.pSocial,cre:0}); if(sums.pFund>0)v4rows.push({code:map.personalFund,summary:'缴纳'+yr+'年'+mo+'月公积金(个人部分)',deb:sums.pFund,cre:0}); var payTotal=r2(sums.cSocial+sums.cFund+sums.pSocial+sums.pFund); if(payTotal>0)v4rows.push({code:map.bank,summary:'缴纳'+yr+'年'+mo+'月社保及公积金',deb:0,cre:payTotal,cf:map.cashFlow}); function mk(key,title,date,rows){ var rs=mergeRows(rows); var td=0,tc=0;rs.forEach(function(r){td=r2(td+r.deb);tc=r2(tc+r.cre);}); return {key:key,title:title,date:date,word:map.word,rows:rs,totalDeb:td,totalCre:tc,balanced:Math.abs(td-tc)<0.005}; } return [mk('v1','工资计提凭证',dAccrual,v1rows), mk('v2','五险一金计提凭证(公司部分)',dAccrual,v2rows), mk('v3','工资发放及代扣凭证',dPay,v3rows), mk('v4','社保公积金缴纳凭证',dSoc,v4rows)]; } /* 数据源一:按工资明细生成 */ function buildVouchers(month){ var rec=DB.payroll[month]; if(!rec||!rec.rows.length)return null; var sums=payrollTotals(rec.rows); return vouchersFromAgg(month,deptAggFromRows(rec.rows),sums); } /* 数据源二:按部门汇总生成(无需个人明细) */ function buildVouchersManual(month){ var m=DB.vchManual[month]; if(!m||!m.depts||!m.depts.length)return null; var g={gross:0,pSocial:0,pFund:0,tax:0,cSocial:0,cFund:0,net:0}; m.depts.forEach(function(d){ g.gross=r2(g.gross+pnum(d.gross));g.pSocial=r2(g.pSocial+pnum(d.pSocial));g.pFund=r2(g.pFund+pnum(d.pFund)); g.tax=r2(g.tax+pnum(d.tax));g.cSocial=r2(g.cSocial+pnum(d.cSocial));g.cFund=r2(g.cFund+pnum(d.cFund)); }); g.net=r2(Math.max(0,g.gross-g.pSocial-g.pFund-g.tax)); return vouchersFromAgg(month,m.depts,g); } var vchState={mode:{}}; function vchSourceOf(month){ if(vchState.mode[month])return vchState.mode[month]; if(DB.payroll[month])return 'detail'; if(DB.vchManual[month]&&DB.vchManual[month].depts&&DB.vchManual[month].depts.length)return 'manual'; return 'none'; } function getVouchersFor(month){ return vchSourceOf(month)==='manual'?buildVouchersManual(month):buildVouchers(month); } function setVchMode(month,md){ if(md==='detail'&&!DB.payroll[month])return toast('该月尚无工资表,无法按明细生成','err'); if(md==='manual'&&!(DB.vchManual[month]&&DB.vchManual[month].depts&&DB.vchManual[month].depts.length))return toast('该月尚未录入部门汇总数据,请先点击「部门汇总录入」','err'); vchState.mode[month]=md;pgVchRender(); } /* 凭证 → 可读预览表(导出文件第二个工作表) */ function vouchersToReadable(vs){ var rows=[],rs=[],marks=['①','②','③','④','⑤','⑥']; vs.forEach(function(v,i){ rows.push([(marks[i]||'')+' '+v.title+' · 凭证日期:'+v.date+' · 字号:'+v.word+'-XXXX']);rs.push('title'); rows.push(['摘要','科目编码','辅助项目','现金流','借方金额','贷方金额']);rs.push('h'); v.rows.forEach(function(r){rows.push([r.summary||'',r.code||'',r.aux||'',r.cf||'',r.deb||'',r.cre||'']);rs.push('d');}); rows.push(['合计','','','',v.totalDeb,v.totalCre]);rs.push('t'); }); return {name:'凭证预览',types:['t','t','t','t','m','m'],widths:[48,13,12,9,13,13],rows:rows,rowStyles:rs}; } function validateVouchers(vs){ var msgs=[],map=DB.settings.map; var prefixes=(map.cashPrefixes||'').split(',').map(function(s){return s.trim();}).filter(Boolean); vs.forEach(function(v){ if(!v.balanced)msgs.push('凭证【'+v.title+'】借贷不平:借 '+fmt(v.totalDeb)+' / 贷 '+fmt(v.totalCre)); v.rows.forEach(function(r,i){ if(!r.code||!/^\d+$/.test(r.code))msgs.push('【'+v.title+'】第'+(i+1)+'行科目编码“'+(r.code||'空')+'”无效'); if(prefixes.some(function(p){return r.code===p||r.code.indexOf(p)===0;})&&!r.cf) msgs.push('【'+v.title+'】第'+(i+1)+'行科目 '+r.code+' 为资金科目,缺少现金流编码'); }); }); return msgs; } /* 凭证 → 财务软件标准批量导入模板(与《凭证批量导入标准模板》逐列一致: 单表「批量导入模板」、13 列固定表头、借/贷方金额为数值单元格(显示两位小数)、其余列保持文本、固定列宽) */ var STD_VCH_HEADER=['日期(必须:YYYY-MM-DD)','凭证字号(如:记-0001)','摘要','科目编码','辅助项目','现金流项目(编码)','数量','单价','外币','原币金额','汇率','借方金额','贷方金额']; var STD_VCH_WIDTHS=[15.8,16.8,35.8,15.8,30.8,18.8,10.8,10.8,10.8,12.8,10.8,15.8,15.8]; function vouchersToStdRows(vs,seqStart){ var out=[],n=seqStart||1; vs.forEach(function(v){ var vno=(v.word||'记')+'-'+String(n).padStart(4,'0'); v.rows.forEach(function(r){ out.push([String(v.date||''),vno,String(r.summary==null?'':r.summary),String(r.code==null?'':r.code), String(r.aux||''),String(r.cf||''),'','','','','', r.deb?r2(r.deb):'',r.cre?r2(r.cre):'']); }); n++; }); return out; } function vouchersToAOA(vs,seqStart){ /* CSV 无单元格类型:金额列保留两位小数字符串,数值与 xlsx 数值版一致 */ var rows=vouchersToStdRows(vs,seqStart).map(function(r){ var o=r.slice(); if(o[11]!=='')o[11]=(+o[11]).toFixed(2); if(o[12]!=='')o[12]=(+o[12]).toFixed(2); return o; }); return [STD_VCH_HEADER].concat(rows); } /* ============ 9. 页面渲染 ============ */ var PAGES={dash:['📊','工作台'],dept:['🏢','部门管理'],emp:['👥','员工管理'],ins:['🛡️','五险一金方案'],tax:['🧾','个税税率设置'],pay:['💰','工资核算'],jz:['📅','月末结转'],vch:['📑','凭证中心'],map:['🔗','科目对照设置'],data:['💾','数据管理']}; var curPage='dash'; function nav(page){ curPage=page; document.querySelectorAll('.nav-item').forEach(function(el){el.classList.toggle('active',el.dataset.page===page);}); ge('tbIcon').textContent=PAGES[page][0];ge('tbTitle').textContent=PAGES[page][1]; updateAcctSel(); updatePeriodChip(); var c=ge('content');c.innerHTML=''; ({dash:pgDash,dept:pgDept,emp:pgEmp,ins:pgIns,tax:pgTax,pay:pgPay,jz:pgJz,vch:pgVch,map:pgMap,data:pgData})[page](c); } /* ---- 9.1 工作台 ---- */ function pgDash(c){ var months=Object.keys(DB.payroll).sort().reverse(); var last=curPeriod(); var rec=DB.payroll[last],t=null,depts=[]; if(rec){t=payrollTotals(rec.rows);depts=deptSummary(rec.rows);} var html='
'+ stat('📅','当前账期(所属期)',last,rec?'本月工资表已生成':'本月工资表尚未生成 · 点击顶部账期可调整','#3b82f6,#1d4ed8')+ stat('👥','核算人数',(t?t.count:DB.emps.filter(function(e){return e.status!=='离职';}).length)+' 人',t?'以工资表为准':'以在职档案为准','#0d9488,#0f766e')+ stat('💵','应发工资合计',t?fmt(t.gross):'0.00','元 · 工资总额','#4f46e5,#4338ca')+ stat('🛡️','五险一金(公司)',t?fmt(t.cTotal):'0.00','元 · 单位承担部分','#d97706,#b45309')+ stat('🧾','代扣个人所得税',t?fmt(t.tax):'0.00','元 · 累计预扣法','#dc2626,#b91c1c')+ stat('💳','实发工资合计',t?fmt(t.net):'0.00','元 · 银行代发金额','#059669,#047857')+ '
'; html+='
⚡ 快捷操作
'+ ''+ ''+ ''+ ''+ '
'; if(t){ html+='
🏢 部门归集汇总 '+esc(last)+' 应发 / 五险一金(个人 / 公司) / 个税 / 实发
'+ '
'; depts.forEach(function(d){ var dep=deptOf(d.deptId)||{}; html+=''+ ''; }); html+='
部门记账科目(工资)人数应发工资五险一金(个人)社保(公司)公积金(公司)代扣个税实发合计
'+esc(d.deptName)+''+esc(dep.salaryAcc||'未设置')+''+d.count+''+fmt(d.gross)+''+fmt(d.pTotal)+''+fmt(d.cSocial)+''+fmt(d.cFund)+''+fmt(d.tax)+''+fmt(d.net)+'
合计'+t.count+''+fmt(t.gross)+''+fmt(t.pTotal)+''+fmt(t.cSocial)+''+fmt(t.cFund)+''+fmt(t.tax)+''+fmt(t.net)+'
'; } html+='
🗂️ 历史工资表
'+(months.length?('
'+ months.map(function(m){var tt=payrollTotals(DB.payroll[m].rows); return '';}).join('')+ '
月份人数应发合计实发合计公司五险一金操作
'+esc(m)+''+tt.count+''+fmt(tt.gross)+''+fmt(tt.net)+''+fmt(tt.cTotal)+'
'):'
📭暂无工资表,请前往【工资核算】生成
')+'
'; c.innerHTML=html; } function stat(ico,label,val,sub,grad){ var g=(grad||'#1d4ed8,#1e40af').split(','); return '
'+ico+''+esc(label)+'
'+esc(val)+'
'+esc(sub)+'
'; } function jumpPay(m){nav('pay');setTimeout(function(){var el=ge('payMonth');if(el){el.value=m;pgPayRender();}},30);} /* ---- 9.2 部门管理 ---- */ function pgDept(c){ var html='
💡
部门即成本归集口径:每个部门配置「工资记账科目 / 公司社保科目 / 公司公积金科目」。生成凭证时,系统按部门将工资与五险一金(公司部分)自动归集到对应借方科目。科目编码必须与财务软件账套中的末级科目编码完全一致。
'+ '
🏢 部门档案 共 '+DB.depts.length+' 个部门
'+ '
'+ '
'; DB.depts.forEach(function(d){ var cnt=DB.emps.filter(function(e){return e.deptId===d.id&&e.status!=='离职';}).length; html+=''+ ''; }); if(!DB.depts.length)html+=''; html+='
部门名称工资记账科目公司社保科目公司公积金科目在职人数备注操作
'+esc(d.name)+''+esc(d.salaryAcc||'—')+''+esc(d.socialAcc||'—')+''+esc(d.fundAcc||'—')+''+cnt+''+esc(d.memo||'')+'
🏢暂无部门
'; c.innerHTML=html; } function editDept(id){ var d=id?deptOf(id):{name:'',salaryAcc:'660209',socialAcc:'660210',fundAcc:'660210',memo:''}; var html='
'+ fld('部门名称','req','dName',d.name,'例如:财务部')+ fld('工资记账科目编码','req','dSal',d.salaryAcc,'如 660209(管理费用-工资)')+ fld('公司社保科目编码','','dSoc',d.socialAcc,'如 660210(管理费用-保险金)')+ fld('公司公积金科目编码','','dFnd',d.fundAcc,'可与社保科目相同')+ fld('备注','','dMemo',d.memo,'费用口径说明')+'
'+ '
💡 常用参考(企业准则详细版):660209 管理费用-工资 · 660110 销售费用-工资 · 510101 制造费用-工资 · 50010102 生产成本-直接人工 · 660219 研发费用。请以贵司账套实际末级科目为准。
'; openDlg('🏢',id?'✏️ 编辑部门':'➕ 新增部门',html,[ {t:'取消'}, {t:'保存',cls:'btn-p',fn:function(){ var name=ge('dName').value.trim(),sal=ge('dSal').value.trim(); if(!name){toast('请填写部门名称','err');return false;} if(!/^\d+$/.test(sal)){toast('工资科目编码必须为纯数字','err');return false;} var obj=id?deptOf(id):{id:uid()}; obj.name=name;obj.salaryAcc=sal;obj.socialAcc=ge('dSoc').value.trim();obj.fundAcc=ge('dFnd').value.trim();obj.memo=ge('dMemo').value.trim(); if(!id)DB.depts.push(obj); saveDB();toast('部门已保存','ok');nav('dept'); }}]); } function delDept(id){ var used=DB.emps.filter(function(e){return e.deptId===id;}).length; if(used)return toast('该部门下仍有 '+used+' 名员工,请先调整员工部门','err'); askConfirm('确认删除该部门?',function(){DB.depts=DB.depts.filter(function(d){return d.id!==id;});saveDB();nav('dept');}); } function dlDeptTpl(){ var header=['部门名称*','工资记账科目*','公司社保科目','公司公积金科目','备注']; var note='导入规则:按「部门名称」匹配——已存在则更新,不存在则新增;带 * 为必填;科目编码必须为财务软件账套中的末级科目(纯数字)。'; downloadXlsx([ {name:'导入模板',title:'部门档案导入模板',note:note,header:header,types:['t','t','t','t','t'],widths:[18,16,16,16,20],rows:[ ['综合管理部','660209','660210','660210','管理费用'], ['销售部','660110','660112','660112','销售费用']]}, tplNoteSheet([ '第1列「部门名称」必填;与已有部门同名时更新其科目信息,否则新增部门。', '第2列「工资记账科目」必填、纯数字,必须是账套末级科目编码(如 660209 管理费用-工资、660110 销售费用-工资)。', '「公司社保科目」「公司公积金科目」建议填写(如 660210);留空时生成凭证会提示科目缺失。', '「备注」选填,用于费用口径说明。', '请勿修改模板表头,不要合并单元格;下载后可直接在下方空白行继续填写。'])] ,'部门导入模板'); toast('部门导入模板已下载','ok'); } /* ---- 部门/员工导入共享解析器(单表导入与「全部数据表」导入共用) ---- */ var DEPT_ALIASES=[['name',['部门名称','部门']],['salary',['工资记账科目','工资科目']],['social',['公司社保科目','社保科目']],['fund',['公司公积金科目','公积金科目']],['memo',['备注']]]; var EMP_ALIASES=[['code',['工号']],['name',['姓名']],['dept',['部门']],['hire',['入职日期','入职']],['status',['状态']],['idc',['身份证']], ['base',['基本工资']],['post',['岗位工资']],['merit',['绩效工资']],['allow',['补贴']], ['soc',['社保基数']],['fund',['公积金基数']], ['d1',['子女教育']],['d2',['婴幼儿','照护']],['d3',['赡养']],['d4',['住房贷款','房贷']],['d5',['住房租金','租金']],['d6',['继续教育']],['d7',['其他扣除']], ['m1',['已计月份']],['m2',['年初累计应发','累计应发']],['m3',['累计个人五险一金','累计五险一金']],['m4',['累计专项']],['m5',['已预扣税']]]; function parseDeptRow(row,map){ function g(k){return map[k]!=null?row[map[k]]:'';} var name=String(g('name')).trim(); if(!name)return {err:'部门名称为空'}; var sal=String(g('salary')).trim(); if(!/^\d+$/.test(sal))return {err:'工资科目编码「'+(sal||'空')+'」必须为纯数字'}; var soc=String(g('social')).trim(),fund=String(g('fund')).trim(); if(soc&&!/^\d+$/.test(soc))return {err:'社保科目编码「'+soc+'」必须为纯数字'}; if(fund&&!/^\d+$/.test(fund))return {err:'公积金科目编码「'+fund+'」必须为纯数字'}; return {ok:{name:name,salaryAcc:sal,socialAcc:soc,fundAcc:fund,memo:String(g('memo')).trim()}}; } function applyDeptRows(list){ var add=0,upd=0; list.forEach(function(r){ var ex=DB.depts.find(function(d){return d.name===r.name;}); if(ex){Object.assign(ex,r);upd++;} else{DB.depts.push(Object.assign({id:uid()},r));add++;} }); return {add:add,upd:upd}; } function parseEmpRow(row,map){ function g(k){return map[k]!=null?row[map[k]]:'';} function gn(k){var v=map[k]!=null?row[map[k]]:'';return (v===''||v==null)?null:pnum(v);} var code=String(g('code')).trim(),name=String(g('name')).trim(); if(!code&&!name)return {err:'工号与姓名均为空'}; if(!code)return {err:'工号为空(姓名 '+name+')'}; if(!name)return {err:'姓名为空(工号 '+code+')'}; var status=String(g('status')).trim(); return {ok:{code:code,name:name,dept:String(g('dept')).trim(),hire:normDate(g('hire')), status:status?(status.indexOf('离')>=0||status.indexOf('退')>=0?'离职':'在职'):'', idc:String(g('idc')).trim(), base:gn('base'),post:gn('post'),merit:gn('merit'),allow:gn('allow'), soc:gn('soc'),fund:gn('fund'), d1:gn('d1'),d2:gn('d2'),d3:gn('d3'),d4:gn('d4'),d5:gn('d5'),d6:gn('d6'),d7:gn('d7'), m1:gn('m1'),m2:gn('m2'),m3:gn('m3'),m4:gn('m4'),m5:gn('m5')}}; } function applyEmpRows(list){ var add=0,upd=0,newDept=0; list.forEach(function(it){ var dept=null; if(it.dept){ dept=DB.depts.find(function(d){return d.name===it.dept;}); if(!dept){dept={id:uid(),name:it.dept,salaryAcc:'660209',socialAcc:'660210',fundAcc:'660210',memo:'导入自动创建'};DB.depts.push(dept);newDept++;} } var e=DB.emps.find(function(x){return x.code===it.code;}); if(!e){ e={id:uid(),code:it.code,name:it.name,deptId:(dept||(DB.depts[0]||{})).id||'',hireDate:it.hire||'',status:it.status||'在职',idcard:it.idc||'', base:0,post:0,merit:0,allowance:0,socialBase:null,fundBase:null, ded:{child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0}, initCum:{months:0,income:0,insFund:0,special:0,tax:0}}; DB.emps.push(e);add++; }else{upd++;} e.name=it.name; if(dept)e.deptId=dept.id; if(it.hire)e.hireDate=it.hire; if(it.status)e.status=it.status; if(it.idc!=='')e.idcard=it.idc; if(it.base!=null)e.base=it.base; if(it.post!=null)e.post=it.post; if(it.merit!=null)e.merit=it.merit; if(it.allow!=null)e.allowance=it.allow; if(it.soc!=null)e.socialBase=it.soc>0?it.soc:null; if(it.fund!=null)e.fundBase=it.fund>0?it.fund:null; if(it.d1!=null)e.ded.child=it.d1; if(it.d2!=null)e.ded.infant=it.d2; if(it.d3!=null)e.ded.elder=it.d3; if(it.d4!=null)e.ded.mortgage=it.d4; if(it.d5!=null)e.ded.rent=it.d5; if(it.d6!=null)e.ded.edu=it.d6; if(it.d7!=null)e.ded.other=it.d7; if(it.m1!=null)e.initCum.months=Math.round(it.m1); if(it.m2!=null)e.initCum.income=it.m2; if(it.m3!=null)e.initCum.insFund=it.m3; if(it.m4!=null)e.initCum.special=it.m4; if(it.m5!=null)e.initCum.tax=it.m5; }); return {add:add,upd:upd,newDept:newDept}; } function importDepts(){ pickImport(function(f,xl){ var aoa=pickSheet(xl,['导入模板','部门','模板']); var hd=findHeader(aoa,DEPT_ALIASES); if(hd.row<0||hd.map.name==null)return toast('未找到「部门名称」列,请使用系统提供的导入模板','err'); showImportDlg({title:'部门导入预览',aoa:aoa,hd:hd,required:[['name','部门名称']], cols:[{label:'部门名称',key:'name'},{label:'工资科目',key:'salary'},{label:'社保科目',key:'social'},{label:'公积金科目',key:'fund'},{label:'备注',key:'memo'}], parseRow:function(row,map){return parseDeptRow(row,map);}, apply:function(list){ var r=applyDeptRows(list); saveDB();nav('dept'); toast('部门导入完成:新增 '+r.add+' 个,更新 '+r.upd+' 个','ok'); }}); }); } /* ---- 9.3 员工管理 ---- */ var empFilter={kw:'',dept:''}; function pgEmp(c){ var html='
👥 员工档案 在职 '+DB.emps.filter(function(e){return e.status!=='离职';}).length+' 人 / 全部 '+DB.emps.length+' 人'+ '
'+ ''+ '
'; c.innerHTML=html;pgEmpRender(); } function pgEmpRender(){ var list=DB.emps.filter(function(e){ if(empFilter.dept&&e.deptId!==empFilter.dept)return false; var kw=empFilter.kw.trim(); if(kw&&!(String(e.name||'').includes(kw)||String(e.code||'').includes(kw)))return false; return true; }); var html='
'; list.forEach(function(e){ var fixed=r2(pnum(e.base)+pnum(e.post)+pnum(e.merit)+pnum(e.allowance)); var ded=e.ded||{};var sp=r2(pnum(ded.child)+pnum(ded.infant)+pnum(ded.elder)+pnum(ded.mortgage)+pnum(ded.rent)+pnum(ded.edu)+pnum(ded.other)); html+=''+ ''+ ''+ ''+ ''; }); if(!list.length)html+=''; html+='
工号姓名部门入职日期基本工资岗位工资绩效工资补贴固定月薪合计月专项附加扣除状态操作
'+esc(e.code)+''+esc(e.name)+''+esc(deptName(e.deptId))+''+esc(e.hireDate||'')+''+fmt(e.base)+''+fmt(e.post)+''+fmt(e.merit)+''+fmt(e.allowance)+''+fmt(fixed)+''+fmt(sp)+''+(e.status==='离职'?'离职':'在职')+'
👥无匹配员工
'; ge('empTblWrap').innerHTML=html; } function fld(label,cls,id,val,ph,type){return '';} function editEmp(id){ var e=id?DB.emps.find(function(x){return x.id===id;}):{code:'E'+String(DB.emps.length+1).padStart(3,'0'),name:'',deptId:(DB.depts[0]||{}).id||'',hireDate:'',status:'在职',idcard:'',base:0,post:0,merit:0,allowance:0,socialBase:null,fundBase:null,ded:{child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0},initCum:{months:0,income:0,insFund:0,special:0,tax:0}}; if(!e.ded)e.ded={child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0}; if(!e.initCum)e.initCum={months:0,income:0,insFund:0,special:0,tax:0}; function moneyF(l,id,v,ph){return fld(l,'',id,v==null?0:v,ph,'num');} var html='
'+ fld('工号','req','eCode',e.code,'')+ fld('姓名','req','eName',e.name,'')+ ''+ fld('入职日期','','eHire',e.hireDate,'YYYY-MM-DD','')+ ''+ fld('身份证号','','eIdc',e.idcard,'选填')+'
'+ '
💵 固定月薪构成(元/月)
'+ moneyF('基本工资','eBase',e.base)+moneyF('岗位工资','ePost',e.post)+moneyF('绩效工资','eMerit',e.merit)+moneyF('补贴','eAllow',e.allowance)+'
'+ '
🛡️ 缴费基数(留空 / 0 = 按当月应发工资自动取基数,并执行上下限封顶保底)
'+ moneyF('社保缴费基数','eSoc',e.socialBase||'')+moneyF('公积金缴费基数','eFund',e.fundBase||'')+'
'+ '
🏠 专项附加扣除(月度金额,元/月)
'+ moneyF('子女教育','eD1',e.ded.child,'2000/孩')+moneyF('婴幼儿照护','eD2',e.ded.infant,'2000/孩')+moneyF('赡养老人','eD3',e.ded.elder,'3000 独生')+moneyF('住房贷款利息','eD4',e.ded.mortgage,'1000')+ moneyF('住房租金','eD5',e.ded.rent,'1500/1100/800')+moneyF('继续教育','eD6',e.ded.edu,'400')+moneyF('其他扣除','eD7',e.ded.other,'')+'
'+ '
📅 年初累计数据(本系统中途启用时填写:本年此前月份在原体系/其他渠道的累计值)
'+ moneyF('已计月份数','eM1',e.initCum.months)+moneyF('累计应发工资','eM2',e.initCum.income)+moneyF('累计个人五险一金','eM3',e.initCum.insFund)+moneyF('累计已预扣个税','eM5',e.initCum.tax)+'
'+ '
'+moneyF('累计专项附加扣除','eM4',e.initCum.special)+'
'+ '
累计预扣法说明:系统自动取「年初累计 + 本系统内同年更早月份」的累计应纳税所得额与已预缴税额计算当月应预扣税额。1 月起从零建账的用户全部留 0 即可。
'; openDlg('👤',id?'✏️ 编辑员工':'➕ 新增员工',html,[ {t:'取消'}, {t:'保存',cls:'btn-p',fn:function(){ var name=ge('eName').value.trim(),code=ge('eCode').value.trim(); if(!name||!code){toast('请填写工号与姓名','err');return false;} if(!DB.depts.length){toast('请先建立部门','err');return false;} e.code=code;e.name=name;e.deptId=ge('eDept').value;e.hireDate=ge('eHire').value;e.status=ge('eStatus').value;e.idcard=ge('eIdc').value.trim(); e.base=pnum(ge('eBase').value);e.post=pnum(ge('ePost').value);e.merit=pnum(ge('eMerit').value);e.allowance=pnum(ge('eAllow').value); var sb=pnum(ge('eSoc').value),fb2=pnum(ge('eFund').value); e.socialBase=sb>0?sb:null;e.fundBase=fb2>0?fb2:null; e.ded={child:pnum(ge('eD1').value),infant:pnum(ge('eD2').value),elder:pnum(ge('eD3').value),mortgage:pnum(ge('eD4').value),rent:pnum(ge('eD5').value),edu:pnum(ge('eD6').value),other:pnum(ge('eD7').value)}; e.initCum={months:Math.round(pnum(ge('eM1').value)),income:pnum(ge('eM2').value),insFund:pnum(ge('eM3').value),special:pnum(ge('eM4').value),tax:pnum(ge('eM5').value)}; if(!id)DB.emps.push(e); saveDB();toast('员工已保存','ok');pgEmpRender(); }}],true); } function delEmp(id){ askConfirm('确认删除该员工档案?历史工资表中的快照数据不受影响。',function(){ DB.emps=DB.emps.filter(function(e){return e.id!==id;});saveDB();pgEmpRender();toast('已删除','ok');}); } function empHeader(){return ['工号*','姓名*','部门*','入职日期','状态','身份证号','基本工资','岗位工资','绩效工资','补贴','社保基数','公积金基数','子女教育','婴幼儿照护','赡养老人','住房贷款利息','住房租金','继续教育','其他扣除','年初已计月份','年初累计应发','年初累计个人五险一金','年初累计专项附加','年初累计已预扣税'];} function empTypes(){return ['t','t','t','t','t','t','m','m','m','m','m','m','m','m','m','m','m','m','m','i','m','m','m','m'];} function empWidths(){return [9,9,13,12,7,19,10,10,10,9,11,11,10,11,10,11,10,10,10,11,12,15,13,13];} function empRowOf(e){ var d=e.ded||{},ic=e.initCum||{}; return [e.code,e.name,deptName(e.deptId),e.hireDate||'',e.status||'在职',e.idcard||'', pnum(e.base),pnum(e.post),pnum(e.merit),pnum(e.allowance), e.socialBase==null?'':+e.socialBase,e.fundBase==null?'':+e.fundBase, pnum(d.child),pnum(d.infant),pnum(d.elder),pnum(d.mortgage),pnum(d.rent),pnum(d.edu),pnum(d.other), Math.round(pnum(ic.months)),pnum(ic.income),pnum(ic.insFund),pnum(ic.special),pnum(ic.tax)]; } function exportEmps(){ if(!DB.emps.length)return toast('暂无员工可导出','err'); downloadXlsx([{name:'员工档案',title:(DB.meta.company||'')+' 员工档案', note:'导出时间:'+new Date().toLocaleString('zh-CN')+' · 共 '+DB.emps.length+' 人 · 本表可直接修改后通过「导入Excel」批量更新', header:empHeader(),types:empTypes(),widths:empWidths(),rows:DB.emps.map(empRowOf)}],'员工档案_'+curMonth()); toast('员工档案已导出(样式与界面一致)','ok'); } function dlEmpTpl(){ downloadXlsx([ {name:'导入模板',title:'员工档案导入模板',note:'按「工号」匹配:已存在则更新(留空项保持原值),不存在则新增;部门按名称匹配,不存在时自动创建(科目为默认值,请核对);日期格式 YYYY-MM-DD。', header:empHeader(),types:empTypes(),widths:empWidths(),rows:[ ['E001','张三','综合管理部','2024-03-01','在职','',8000,2000,1000,500,'','',2000,0,3000,1000,0,0,0,0,0,0,0,0], ['E002','李四','销售部','2025-06-15','在职','','',0,0,0,'','',0,0,0,0,1500,0,0,0,0,0,0,0]]}, tplNoteSheet([ '「工号」「姓名」必填;按工号匹配员工,工号不存在则新增,存在则更新(留空单元格保持原值不变)。', '「部门」按名称匹配;填了不存在的部门会自动创建该部门(费用科目为默认值,请到部门管理核对修改)。', '「入职日期」格式 YYYY-MM-DD(如 2024-03-01),Excel 日期单元格会自动识别。', '「状态」填 在职 或 离职;离职员工不参与工资核算但保留档案。', '「社保基数」「公积金基数」留空 = 按当月应发工资自动取基数并执行上下限;填写数字 = 固定基数。', '第13-19列为月度专项附加扣除(元/月);第20-24列为年初累计数据(年中启用系统时填写,1月起建账全部填0)。', '请勿修改模板表头,不要合并单元格。'])] ,'员工导入模板'); toast('员工导入模板已下载','ok'); } function importEmps(){ pickImport(function(f,xl){ var aoa=pickSheet(xl,['导入模板','员工','模板']); var hd=findHeader(aoa,EMP_ALIASES); if(hd.row<0||hd.map.code==null||hd.map.name==null)return toast('未找到「工号」「姓名」列,请使用系统提供的导入模板','err'); showImportDlg({title:'员工导入预览',aoa:aoa,hd:hd,required:[['code','工号'],['name','姓名']], cols:[{label:'工号',key:'code'},{label:'姓名',key:'name'},{label:'部门',key:'dept'},{label:'状态',key:'status'}, {label:'基本工资',key:'base',num:1},{label:'岗位工资',key:'post',num:1},{label:'绩效工资',key:'merit',num:1},{label:'补贴',key:'allow',num:1}, {label:'入职日期',key:'hire'}], parseRow:function(row,map){return parseEmpRow(row,map);}, apply:function(list){ var r=applyEmpRows(list); saveDB();pgEmpRender(); var msg='员工导入完成:新增 '+r.add+' 人,更新 '+r.upd+' 人'+(r.newDept?(',自动创建部门 '+r.newDept+' 个'):''); if(r.newDept)openDlg('📥','导入完成(请核对)','
自动创建的部门使用默认科目(660209/660210),请到【部门管理】核对修改:
'+esc(msg)+'
',[{t:'知道了'}]); else toast(msg,'ok'); }}); }); } /* ---- 9.4 五险一金方案 ---- */ function pgIns(c){ var cfg=DB.settings.ins; var html='
🛡️
比例单位为 %,基数上下限填 0 表示不启用。缴费金额 = 基数 × 比例,按舍入精度取整;「社保大病/补充医疗固定额」直接并入个人社保合计。
'+ '
🛡️ 险种比例方案(个人 / 公司)
'+ '
'; cfg.items.forEach(function(it,i){ var base=it.base==='fund'?'公积金基数':'社保基数'; html+=''+ ''+ ''+ ''+ ''+ ''; }); html+='
险种基数口径个人比例%公司比例%默认测算(基数10000)操作
个人 '+fmt(10000*(+it.p||0)/100)+' / 公司 '+fmt(10000*(+it.c||0)/100)+'
'+ '
'+ '
'+ numBox('社保基数下限(0=不启用)','iSocMin',cfg.socialMin)+numBox('社保基数上限(0=不封顶)','iSocMax',cfg.socialMax)+ numBox('公积金基数下限(0=不启用)','iFundMin',cfg.fundMin)+numBox('公积金基数上限(0=不封顶)','iFundMax',cfg.fundMax)+ numBox('个人大病医疗固定额(元/月)','iMed',cfg.medicalExtra)+ '
'+ '
'; c.innerHTML=html; } function numBox(label,id,val){return '';} function addInsItem(){DB.settings.ins.items.push({key:'x'+Date.now().toString(36),name:'新险种',p:0,c:0,base:'social'});saveDB();pgIns(ge('content'));toast('已增加险种,请设置比例','ok');} function delInsItem(i){askConfirm('确认删除该险种?',function(){DB.settings.ins.items.splice(i,1);saveDB();pgIns(ge('content'));});} function saveInsCfg(){ var cfg=DB.settings.ins; cfg.socialMin=pnum(ge('iSocMin').value);cfg.socialMax=pnum(ge('iSocMax').value); cfg.fundMin=pnum(ge('iFundMin').value);cfg.fundMax=pnum(ge('iFundMax').value); cfg.medicalExtra=pnum(ge('iMed').value);cfg.roundTo=parseFloat(ge('iRound').value)||0.01; saveDB();toast('五险一金方案已保存','ok'); } /* ---- 9.5 个税设置 ---- */ function pgTax(c){ var t=DB.settings.tax; var html='
🧾 综合所得累计预扣率表(工资薪金)
'+ '
'+ '
法定为 5000 元/月(60000 元/年)。累计应纳税所得额 = 累计应发 − 累计减除费用 − 累计个人五险一金 − 累计专项附加扣除。
'+ '
'; t.brackets.forEach(function(b,i){ html+=''+ ''+ ''+ ''; }); html+='
级次累计应纳税所得额区间上限(元)预扣率%速算扣除数
第'+(i+1)+'级
'+ '
📈 累计预扣法:本期应预扣税额 =(累计应纳税所得额 × 预扣率 − 速算扣除数)− 累计已预扣税额。系统自动逐月累计,年初数据可在员工档案中设置。
'; c.innerHTML=html; } function saveTaxCfg(){DB.settings.tax.basic=pnum(ge('tBasic').value)||5000;saveDB();toast('个税参数已保存','ok');} function resetTax(){DB.settings.tax=JSON.parse(JSON.stringify(DEF_TAX));saveDB();pgTax(ge('content'));toast('已恢复法定默认税率表','ok');} /* ---- 9.6 工资核算 ---- */ function pgPay(c){ var html='
💰 工资核算 五险一金与个税自动计算
'+ ''+ ''+ '
'; c.innerHTML=html;pgPayRender(); } function pgPayRender(){ var month=ge('payMonth').value,rec=DB.payroll[month],box=ge('payBody'); if(!rec){box.innerHTML='
🧮'+esc(month)+' 尚未生成工资表
生成逻辑:固定月薪 + 奖金/加班/其他增项 − 缺勤扣款/其他扣项 = 应发工资;
五险一金按方案比例与基数自动计算;个税按累计预扣法自动计算(自动衔接本系统同年更早月份)。
';return;} var rows=rec.rows,t=payrollTotals(rows),depts=deptSummary(rows); var html='
'+ stat('👥','核算人数',t.count+' 人',month,'#3b82f6,#1d4ed8')+ stat('💵','应发合计',fmt(t.gross),'元','#4f46e5,#4338ca')+ stat('🛡️','个人五险一金',fmt(t.pTotal),'元 · 代扣','#d97706,#b45309')+ stat('🏢','公司五险一金',fmt(t.cTotal),'元 · 计提费用','#0d9488,#0f766e')+ stat('🧾','代扣个税',fmt(t.tax),'元 · 累计预扣法','#dc2626,#b91c1c')+ stat('💳','实发合计',fmt(t.net),'元 · 银行代发','#059669,#047857')+'
'+ '
'+ ''+ ''+ ''+ ''+ ''+ ''+ ''+(rec.generatedAt?'已保存 · '+rec.generatedAt.replace('T',' ').substring(0,16):'未保存')+''+ (isClosed(month)?'🔒 已月末结转':'')+'
'+ (isClosed(month) ?'
🔒
本月已完成月末结转,金额已锁定不可修改;如需调整请先前往【月末结转】取消结转。
' :'
✏️
所属期:'+month+'-01 至 '+monthEnd(month)+'(税款所属期)。下表 奖金 / 加班费 / 其他增项 / 缺勤扣款 / 其他扣项 可直接编辑(输入后自动重算该行与合计);社保基数 / 公积金基数默认按应发工资自动取数,也可在表内直接手动修改(留空或填 0 即恢复按应发自动;带 * 列修改后自动重算五险一金与个税)。员工档案中设定的固定基数优先于应发,当月表内编辑优先于档案;批量修改可用「导入当月变动」。
')+ '
'+ ''+ ''+ ''; rows.forEach(function(r,i){ function ein(key,val){var dis=isClosed(month)?' disabled':'';return '';} function bin(key,val){var dis=isClosed(month)?' disabled':'';var v=(val==null||val==='')?'':val;return '';} html+=''+ ''+ ein('bonus',r.bonus)+ein('ot',r.ot)+ein('otherAdd',r.otherAdd)+ein('absence',r.absence)+ein('otherDeduct',r.otherDeduct)+ ''+ bin('socialBase',r.sBaseIn!=null?r.sBaseIn:r.sBase)+bin('fundBase',r.fBaseIn!=null?r.fBaseIn:r.fBase)+ ''+ ''+ ''; }); html+=''+ '
工号姓名部门基本岗位绩效补贴奖金*加班费*其他增*缺勤扣*其他扣*应发工资社保基数*公积金基数*五险一金(个人)五险一金(公司)代扣个税实发工资明细
'+esc(r.code)+''+esc(r.name)+''+esc(r.deptName)+''+fmt(r.base)+''+fmt(r.post)+''+fmt(r.merit)+''+fmt(r.allowance)+''+fmt(r.gross)+''+fmt(r.pTotal)+''+fmt(r.cTotal)+''+fmt(r.tax)+''+fmt(r.net)+'
合计('+t.count+'人)'+fmt(t.gross)+''+fmt(t.pTotal)+''+fmt(t.cTotal)+''+fmt(t.tax)+''+fmt(t.net)+'
'+ '
🏢 部门归集('+esc(month)+')
'+ depts.map(function(d){return '';}).join('')+ '
部门人数应发工资个人五险一金公司社保公司公积金代扣个税实发
'+esc(d.deptName)+''+d.count+''+fmt(d.gross)+''+fmt(d.pTotal)+''+fmt(d.cSocial)+''+fmt(d.cFund)+''+fmt(d.tax)+''+fmt(d.net)+'
'; box.innerHTML=html; } function payCellEdit(inp,month){ if(isClosed(month))return toast('本月已月末结转,金额已锁定','err'); inp.value=inp.value.replace(/[^0-9.\-]/g,''); var rec=DB.payroll[month];if(!rec)return; var i=+inp.dataset.row,k=inp.dataset.k; if(!rec.inputs)rec.inputs={}; var cur=rec.inputs[rec.rows[i].empId]||{bonus:0,ot:0,otherAdd:0,absence:0,otherDeduct:0}; var v=pnum(inp.value); if(k==='socialBase'||k==='fundBase'){ /* 缴费基数:留空或 0 = 恢复默认(按应发工资自动取数);填写数字 = 本月手动基数 */ if(v>0)cur[k]=v;else delete cur[k]; }else cur[k]=v; rec.inputs[rec.rows[i].empId]=cur; } function rebuildPayroll(month){ var rec=DB.payroll[month];if(!rec)return; var prior=priorCumFor(month); rec.rows=DB.emps.filter(function(e){return e.status!=='离职';}).map(function(e){return buildRow(e,month,(rec.inputs||{})[e.id],prior);}); saveDB(); if(curPage==='pay'&&ge('payMonth')&&ge('payMonth').value===month)pgPayRender(); } function pgPayRecalc(month){rebuildPayroll(month);pgPayRender();} function pgPayGen(){ var month=ge('payMonth').value; if(isClosed(month))return toast(month+' 已月末结转,请先在【月末结转】中取消结转','err'); if(!DB.depts.length)return toast('请先在【部门管理】建立部门','err'); if(!DB.emps.filter(function(e){return e.status!=='离职';}).length)return toast('没有在职员工','err'); var rows=buildPayroll(month); var inputs={}; rows.forEach(function(r){inputs[r.empId]={bonus:r.bonus,ot:r.ot,otherAdd:r.otherAdd,absence:r.absence,otherDeduct:r.otherDeduct};}); DB.payroll[month]={generatedAt:new Date().toISOString(),rows:rows,inputs:inputs}; saveDB();pgPayRender();toast(month+' 工资表已生成('+rows.length+'人)','ok'); } function pgPaySave(month){saveDB();toast('已保存','ok');} function delPayroll(m){ if(isClosed(m))return toast(m+' 已月末结转锁定,请先取消结转','err'); askConfirm('确认删除 '+m+' 的工资表?删除后该月之后的累计预扣税基将不再包含本月。',function(){delete DB.payroll[m];saveDB();pgPayRender();}); } function rowDetail(month,i){ var r=DB.payroll[month].rows[i]; var insHtml=Object.keys(r.ins).map(function(k){var d=r.ins[k]; return ''+esc(d.name)+''+fmt(d.base)+''+fmt(d.p)+''+fmt(d.c)+'';}).join(''); var html='
'+ ''+ ''+ ''+ ''+ ''+ ''+ '
项目金额
固定月薪合计'+fmt(r.fixed)+'
奖金 + 加班费 + 其他增项'+fmt(r2(r.bonus+r.ot+r.otherAdd))+'
缺勤扣款 + 其他扣项-'+fmt(r2(r.absence+r.otherDeduct))+'
应发工资'+fmt(r.gross)+'
个人五险一金合计'+(r.medicalExtra?'(含固定额'+fmt(r.medicalExtra)+')':'')+'-'+fmt(r.pTotal)+'
累计预扣法代扣个税-'+fmt(r.tax)+'
实发工资'+fmt(r.net)+'
'+ '
🛡️ 五险一金明细
'+ '
'+insHtml+ '
险种基数个人公司
合计'+fmt(r.pTotal)+''+fmt(r.cTotal)+'
'+ '
🧾 个税累计口径('+esc(r.name)+')
'+ '
'+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ '
项目累计值
累计已计月份数'+r.cum.months+'
累计应发工资'+fmt(r.cum.income)+'
累计减除费用('+fmt(DB.settings.tax.basic)+' × '+r.cum.months+')-'+fmt(r.cum.deduct)+'
累计个人五险一金-'+fmt(r.cum.ins)+'
累计专项附加扣除(本月 '+fmt(r.special)+')-'+fmt(r.cum.special)+'
累计应纳税所得额'+fmt(r.cum.taxable)+'
累计应预扣税额'+fmt(r.cum.cumTax)+'
减:此前已预扣税额-'+fmt(r2(r.cum.cumTax-r.tax))+'
本月应预扣税额'+fmt(r.tax)+'
'; openDlg('🔎',esc(r.name)+' 工资明细('+esc(r.deptName)+')',html,[{t:'关闭'}]); } function exportPayroll(month){ var rec=DB.payroll[month];if(!rec)return toast('请先生成工资表','err'); var rows=rec.rows,cfg=DB.settings.ins,t=payrollTotals(rows); var head1=['工号','姓名','部门','基本工资','岗位工资','绩效工资','补贴','奖金','加班费','其他增项','缺勤扣款','其他扣项','应发工资','社保基数','公积金基数']; cfg.items.forEach(function(it){head1.push(it.name+'(个人)');}); head1.push('大病固定额');cfg.items.forEach(function(it){head1.push(it.name+'(公司)');}); head1.push('个人五险一金合计','公司五险一金合计','月专项附加扣除','代扣个税','实发工资','累计应纳税所得额'); var body=rows.map(function(r){ var line=[r.code,r.name,r.deptName,r2(r.base),r2(r.post),r2(r.merit),r2(r.allowance),r2(r.bonus),r2(r.ot),r2(r.otherAdd),r2(r.absence),r2(r.otherDeduct),r2(r.gross),r2(r.sBase),r2(r.fBase)]; cfg.items.forEach(function(it){line.push(r2(r.ins[it.key]?r.ins[it.key].p:0));}); line.push(r2(r.medicalExtra)); cfg.items.forEach(function(it){line.push(r2(r.ins[it.key]?r.ins[it.key].c:0));}); line.push(r2(r.pTotal),r2(r.cTotal),r2(r.special),r2(r.tax),r2(r.net),r2(r.cum.taxable)); return line; }); var medTot=r2(rows.reduce(function(s,r){return s+(+r.medicalExtra||0);},0)); var fline=head1.map(function(){return '';}); fline[0]='合计';fline[2]=t.count+'人'; fline[12]=r2(t.gross); fline[head1.indexOf('大病固定额')]=medTot; fline[head1.indexOf('个人五险一金合计')]=r2(t.pTotal); fline[head1.indexOf('公司五险一金合计')]=r2(t.cTotal); fline[head1.indexOf('月专项附加扣除')]=r2(t.special); fline[head1.indexOf('代扣个税')]=r2(t.tax); fline[head1.indexOf('实发工资')]=r2(t.net); body.push(fline); var n=head1.length; var w1=[8,9,12,10,10,10,8,9,9,9,9,9,12,10,10].concat(cfg.items.map(function(){return 11;})).concat([9],cfg.items.map(function(){return 11;}),[14,14,12,10,12,14]); var ds=deptSummary(rows); var rows2=ds.map(function(d){return [d.deptName,d.count,r2(d.gross),r2(d.pTotal),r2(d.cSocial),r2(d.cFund),r2(d.cTotal),r2(d.tax),r2(d.net)];}); rows2.push(['合计',t.count,r2(t.gross),r2(t.pTotal),r2(t.cSocial),r2(t.cFund),r2(t.cTotal),r2(t.tax),r2(t.net)]); var w2=[14,7,12,13,11,12,14,10,12]; downloadXlsx([ {name:'工资明细',title:(DB.meta.company||'')+' '+fmtMonthCn(month)+' 工资表', note:'所属期:'+month+'-01 至 '+monthEnd(month)+' · 核算人数:'+t.count+' 人 · 金额单位:元'+(isClosed(month)?' · 🔒 已月末结转':''), header:head1,types:['t','t','t'].concat(head1.slice(3).map(function(){return 'm';})),widths:w1,rows:body,totalRow:true}, {name:'部门汇总',title:(DB.meta.company||'')+' '+fmtMonthCn(month)+' 部门归集汇总', note:'部门成本归集口径(生成凭证时借方按部门科目归集)', header:['部门','人数','应发工资','个人五险一金','公司社保','公司公积金','公司五险一金合计','代扣个税','实发工资'], types:['t','i','m','m','m','m','m','m','m'],widths:w2,rows:rows2,totalRow:true}], '工资表_'+fmtMonthCn(month)); toast('工资表已导出(样式与软件显示一致)','ok'); } function dlPayTpl(){ downloadXlsx([ {name:'导入模板',title:'当月变动导入模板',note:'按「工号」匹配员工(工号为空时按姓名唯一匹配);留空的单元格 = 不修改该项;填 0 = 清零;导入后自动重算五险一金、个税与合计。', header:['工号*','姓名','奖金','加班费','其他增项','缺勤扣款','其他扣项','社保基数(留空=档案/应发)','公积金基数(留空=档案/应发)','月专项附加扣除(留空=按档案)'], types:['t','t','m','m','m','m','m','m','m','m'],widths:[10,10,11,11,11,11,11,20,22,22],rows:[ ['E001','张伟明',1000,200,0,0,0,'','',''], ['E002','刘慧',0,0,300,150,0,'','','']]}, tplNoteSheet([ '「工号」必填(工号留空时按「姓名」匹配,且姓名必须唯一);请使用工资核算页导出的工号。', '奖金 / 加班费 / 其他增项 / 缺勤扣款 / 其他扣项:为增减项,影响应发工资。', '「社保基数」「公积金基数」留空 = 按员工档案或应发工资自动计算;填写数字 = 本月固定基数。', '「月专项附加扣除」留空 = 按员工档案各项合计;填写数字 = 本月使用该合计数。', '留空的单元格不修改原有数据;填 0 表示本月清零该项。', '导入前请先在【工资核算】选择对应月份并生成工资表;导入后系统自动重算并刷新。'])] ,'工资变动导入模板'); toast('变动导入模板已下载','ok'); } function importPayChanges(month){ if(isClosed(month))return toast(month+' 已月末结转锁定,请先取消结转','err'); pickImport(function(f,xl){ var aoa=pickSheet(xl,['导入模板','工资','变动','模板']); var hd=findHeader(aoa,[['code',['工号']],['name',['姓名']],['bonus',['奖金']],['ot',['加班']],['otherAdd',['其他增项','其他增']],['absence',['缺勤']],['otherDeduct',['其他扣项','其他扣']],['soc',['社保基数']],['fund',['公积金基数']],['special',['专项附加']]]); if(hd.row<0||(hd.map.code==null&&hd.map.name==null))return toast('未找到「工号」列,请使用系统提供的变动导入模板','err'); showImportDlg({title:'当月变动导入预览('+month+')',aoa:aoa,hd:hd,required:[['code','工号']], cols:[{label:'工号',key:'code'},{label:'姓名',key:'name'},{label:'奖金',key:'bonus',num:1},{label:'加班费',key:'ot',num:1},{label:'其他增项',key:'otherAdd',num:1},{label:'缺勤扣款',key:'absence',num:1},{label:'其他扣项',key:'otherDeduct',num:1}], parseRow:function(row,map){ function g(k){return map[k]!=null?row[map[k]]:'';} var code=String(g('code')).trim(),name=String(g('name')).trim(); if(!code&&!name)return {err:'工号与姓名均为空'}; var vals={}; [['bonus','bonus'],['ot','ot'],['otherAdd','otherAdd'],['absence','absence'],['otherDeduct','otherDeduct'],['soc','socialBase'],['fund','fundBase'],['special','specialOver']].forEach(function(p){ var v=g(p[0]); if(v!==''&&v!=null)vals[p[1]]=pnum(v); }); return {ok:{code:code,name:name,vals:vals}}; }, apply:function(list){ if(!DB.payroll[month])DB.payroll[month]={generatedAt:new Date().toISOString(),rows:[],inputs:{}}; var rec=DB.payroll[month]; if(!rec.inputs)rec.inputs={}; var okM=0,fails=[]; list.forEach(function(it){ var emp=DB.emps.find(function(e){return it.code&&e.code===it.code;}); if(!emp&&it.name){ var cand=DB.emps.filter(function(e){return e.name===it.name;}); if(cand.length===1)emp=cand[0]; } if(!emp){fails.push((it.code||it.name||'空行')+':未找到对应员工');return;} var cur=rec.inputs[emp.id]||{}; Object.keys(it.vals).forEach(function(k){cur[k]=it.vals[k];}); rec.inputs[emp.id]=cur;okM++; }); saveDB();rebuildPayroll(month); if(!okM)toast('没有匹配到任何员工,请检查工号/姓名','err'); else if(fails.length)toast('导入完成:更新 '+okM+' 人;'+fails.length+' 行未匹配(首个:'+fails[0]+')','err'); else toast('导入完成:已更新 '+okM+' 人的当月数据并重算','ok'); }}); }); } function printSlips(month){ var rec=DB.payroll[month];if(!rec)return toast('请先生成工资表','err'); var cfg=DB.settings.ins; var html='
'+esc(DB.meta.company)+' '+esc(month)+'月 工资条
'; rec.rows.forEach(function(r){ var items=cfg.items.map(function(it){return ''+esc(it.name)+'(个)'+fmt(r.ins[it.key]?r.ins[it.key].p:0)+'';}).join(''); var itemsC=cfg.items.map(function(it){return ''+esc(it.name)+'(司)'+fmt(r.ins[it.key]?r.ins[it.key].c:0)+'';}).join(''); html+='
部门:'+esc(r.deptName)+'姓名:'+esc(r.name)+'工号:'+esc(r.code)+'实发:'+fmt(r.net)+' 元
'+ ''+ ''+items+''+ ''+itemsC+'
基本'+fmt(r.base)+'岗位'+fmt(r.post)+'绩效'+fmt(r.merit)+'补贴'+fmt(r.allowance)+'奖金'+fmt(r.bonus)+'加班'+fmt(r.ot)+'应发'+fmt(r.gross)+'
个税'+fmt(r.tax)+'
公司五险一金合计'+fmt(r.cTotal)+'
'; }); ge('printArea').innerHTML=html;window.print(); } /* ---- 9.7 凭证中心 ---- */ var vchSel={v1:true,v2:true,v3:true,v4:true}; function vchDefaultMonth(){ return curPeriod(); } function pgVch(c){ var html='
📑 薪酬凭证自动生成 计提 → 发放 → 缴纳 全链路 · 支持明细/汇总双数据源
'+ ''+ '
'+ '
🔗
财务软件对接流程:① 在本页核对分录 → ② 点击「导出凭证 Excel(财务软件标准模板)」 → ③ 打开财务软件,将账期切换到凭证所属月份(发放/缴纳凭证在次月,需切换到次月账期) → ④ 凭证管理 → 📤 批量导入 → 选择导出的文件。导出格式已按《凭证批量导入标准模板》逐列适配;工资计提、五险一金计提按部门自动归集到各部门自定义的费用科目,发放/缴纳凭证使用全局自定义科目,资金科目自动携带现金流编码。
没有个人明细?切换到「按部门汇总生成」页签,点击「部门汇总录入」直接填写各部门汇总金额即可生成同样完整的会计分录(计提/发放/缴纳全套)。
'+ '
'; c.innerHTML=html;pgVchRender(); } function pgVchRender(){ var month=ge('vchMonth').value,box=ge('vchBody'); var hasDetail=!!(DB.payroll[month]&&DB.payroll[month].rows&&DB.payroll[month].rows.length); var hasManual=!!(DB.vchManual[month]&&DB.vchManual[month].depts&&DB.vchManual[month].depts.length); if(!hasDetail&&!hasManual){ box.innerHTML='
📄'+esc(month)+' 尚无可生成凭证的数据
'+ ''+ '
'; return; } var mode=vchSourceOf(month); var vs=mode==='manual'?buildVouchersManual(month):buildVouchers(month); if(!vs){box.innerHTML='
📄当前数据源为空,请切换页签或录入数据
';return;} var msgs=validateVouchers(vs); var html='
按工资明细生成'+(hasDetail?'':'(无数据)')+'
'+ '
按部门汇总生成'+(hasManual?'':'(未录入)')+'
'; if(mode==='manual')html+='
🧮
本账期按「部门汇总数据」生成凭证(无需个人明细)。如需按员工明细生成,请切换到「按工资明细生成」页签。
'; if(msgs.length)html+='
发现 '+msgs.length+' 个校验问题(不影响预览,但需修复后才能稳妥导入财务软件):
'+msgs.map(esc).join('
')+'
'; else html+='
全部凭证校验通过:借贷平衡、科目编码有效、资金科目已携带现金流编码 '+esc(DB.settings.map.cashFlow)+'(支付给职工以及为职工支付的现金)。可直接导出导入财务软件。
'; html+='
'+ ''+ ''+ ''+ ''+ ''+ ''+ '导出范围:'+ vs.map(function(v){return '';}).join('')+'
'; vs.forEach(function(v,idx){ html+='
'+['①','②','③','④'][idx]+' '+esc(v.title)+''+ ''+(v.balanced?'✓ 借贷平衡':'✗ 借贷不平')+''+ '
日期 '+esc(v.date)+'字号 '+esc(v.word)+'-XXXX(导入时自动编号)分录 '+v.rows.length+'金额 '+fmt(v.totalDeb)+''+(v.date.substring(0,7)!==month?'所属账期 '+v.date.substring(0,7)+':导入前请切换财务软件账期':'')+'
'+ '
'; v.rows.forEach(function(r){ html+=''+ ''; }); html+='
摘要科目编码辅助项目现金流借方金额贷方金额
'+esc(r.summary)+''+esc(r.code)+''+(r.aux?esc(r.aux):'')+''+(r.cf?''+esc(r.cf)+'':'')+''+(r.deb?fmt(r.deb):'')+''+(r.cre?fmt(r.cre):'')+'
合计:'+fmt(v.totalDeb)+''+fmt(v.totalCre)+'
'; }); html+='
📌
导入提示:导出文件与财务软件《凭证批量导入标准模板》逐列完全一致(单表「批量导入模板」、13 列、文本金额两位小数、固定列宽),可直接导入;财务软件按「日期 + 凭证字号」自动分组成凭证,导入时在当期最大凭证号之后重新顺序编号;每张凭证必须借贷平衡;科目编码为可自定义的全局科目(【科目对照设置】)与按部门配置的费用科目(【部门管理】),必须为账套内已存在的末级科目。如需留存可读版式,用「导出凭证预览表」。
'; box.innerHTML=html; } function exportVch(month){ var vs=getVouchersFor(month); if(!vs)return toast('当前账期无可导出的凭证','err'); var sel=vs.filter(function(v){return vchSel[v.key];}); if(!sel.length)return toast('请至少勾选一张凭证','err'); var msgs=validateVouchers(sel); if(msgs.length)return toast('存在校验问题,请先修复:'+msgs[0],'err'); /* 与财务软件《凭证批量导入标准模板》完全一致:单表、13 列、固定列宽;借/贷方金额导出为数值单元格 */ var stdTypes=STD_VCH_HEADER.map(function(){return 't';});stdTypes[11]='a';stdTypes[12]='a'; downloadXlsx([{name:'批量导入模板',header:STD_VCH_HEADER,types:stdTypes, widths:STD_VCH_WIDTHS,rows:vouchersToStdRows(sel,1),plain:true,freeze:false}], '凭证导入_工资社保_'+month+'_'+sel.length+'张'); toast('已按财务软件标准模板导出 '+sel.length+' 张凭证','ok'); } function exportVchReadable(month){ var vs=getVouchersFor(month); if(!vs)return toast('当前账期无可导出的凭证','err'); var sel=vs.filter(function(v){return vchSel[v.key];}); if(!sel.length)return toast('请至少勾选一张凭证','err'); downloadXlsx([vouchersToReadable(sel)],'凭证预览_'+month); toast('凭证预览表已导出(样式与界面一致)','ok'); } function exportVchCsv(month){ var vs=getVouchersFor(month); if(!vs)return toast('当前账期无可导出的凭证','err'); var sel=vs.filter(function(v){return vchSel[v.key];}); if(!sel.length)return toast('请至少勾选一张凭证','err'); downloadCsv(vouchersToAOA(sel,1),'凭证导入_工资社保_'+month); toast('CSV 已导出','ok'); } /* ---- 9.7.1 部门汇总录入(无个人明细也能生成凭证) ---- */ var vchManualTmp=[]; function mvBlank(){return {name:'',salaryAcc:'',socialAcc:'',fundAcc:'',gross:0,pSocial:0,pFund:0,tax:0,cSocial:0,cFund:0};} function vchManualDlg(month){ var m=DB.vchManual[month]; if(!m||!m.depts||!m.depts.length){ if(DB.payroll[month]&&DB.payroll[month].rows&&DB.payroll[month].rows.length){ vchManualTmp=deptAggFromRows(DB.payroll[month].rows); }else{ vchManualTmp=DB.depts.map(function(d){return {name:d.name,salaryAcc:d.salaryAcc||'',socialAcc:d.socialAcc||'',fundAcc:d.fundAcc||'',gross:0,pSocial:0,pFund:0,tax:0,cSocial:0,cFund:0};}); if(!vchManualTmp.length)vchManualTmp=[mvBlank()]; } }else{ vchManualTmp=JSON.parse(JSON.stringify(m.depts)); } var html='
🧮
无需个人明细:逐部门填写汇总金额即可自动生成四大凭证(计提工资 / 计提五险一金 / 发放代扣 / 缴纳)。实发 = 应发 − 个人社保 − 个人公积金 − 代扣个税(自动计算,只需保证各项 ≥ 0 且实发不为负)。科目默认取【部门管理】配置,可直接修改。
'+ '
'+ '
部门名称*工资科目社保科目公积金科目应发工资个人社保个人公积金代扣个税公司社保公司公积金操作
'+ '
'+ (DB.payroll[month]&&DB.payroll[month].rows&&DB.payroll[month].rows.length?'':'')+ ''+ '合计实发:0.00
'; openDlg('🧮','部门汇总数据('+esc(month)+')— 凭证生成',html,[ {t:'取消'}, {t:'💾 保存并生成凭证',cls:'btn-p',fn:function(){return mvSave(month);}} ],false,true); vchManualPaint(); } function vchManualPaint(){ var b=ge('mvBody');if(!b)return; b.innerHTML=vchManualTmp.map(function(r,i){ function tx(k,w){return '';} function nm(k){return '';} return ''+tx('name',110)+''+tx('salaryAcc',88)+''+tx('socialAcc',88)+''+tx('fundAcc',88)+''+ ''+nm('gross')+''+nm('pSocial')+''+nm('pFund')+''+nm('tax')+''+nm('cSocial')+''+nm('cFund')+''+ ''; }).join(''); mvSums(); } function mvSum(){var s={gross:0,pSocial:0,pFund:0,tax:0,cSocial:0,cFund:0};vchManualTmp.forEach(function(r){Object.keys(s).forEach(function(k){s[k]=r2(s[k]+pnum(r[k]));});});return s;} function mvFootHtml(){ var s=mvSum(); return '合计'+['gross','pSocial','pFund','tax','cSocial','cFund'].map(function(k){return ''+fmt(s[k])+'';}).join('')+''; } function mvSums(){ var s=mvSum(),el=ge('mvNet'); if(el)el.textContent=fmt(r2(Math.max(0,s.gross-s.pSocial-s.pFund-s.tax))); var f=ge('mvFoot');if(f)f.innerHTML=mvFootHtml(); } function mvAddRow(){vchManualTmp.push(mvBlank());vchManualPaint();} function mvFromPayroll(month){ var rec=DB.payroll[month];if(!rec)return; vchManualTmp=deptAggFromRows(rec.rows).map(function(a){ return {name:a.name,salaryAcc:a.salaryAcc,socialAcc:a.socialAcc,fundAcc:a.fundAcc,gross:a.gross,pSocial:a.pSocial,pFund:a.pFund,tax:a.tax,cSocial:a.cSocial,cFund:a.cFund}; }); vchManualPaint();toast('已带出 '+vchManualTmp.length+' 个部门的汇总','ok'); } function mvSave(month){ var depts=vchManualTmp.filter(function(r){return String(r.name||'').trim()||pnum(r.gross)||pnum(r.cSocial)||pnum(r.cFund);}); if(!depts.length)return toast('请至少填写一个部门','err'); for(var i=0;iitem.gross)return {err:'个人社保+个人公积金+代扣个税 超过应发工资(实发为负)'}; return {ok:item}; }, apply:function(list){ DB.vchManual[month]={depts:list,savedAt:new Date().toISOString()}; saveDB(); vchState.mode[month]='manual'; if(curPage==='vch')pgVchRender(); toast('已导入 '+list.length+' 个部门的汇总数据,凭证已生成','ok'); }}); }); } /* ---- 9.8 科目对照设置 ---- */ function pgMap(c){ var m=DB.settings.map; function accRow(id,label,val,ph,tip){return ''+label+''+(tip?'
'+tip+'
':'')+'';} var html='
🔗
全部科目编码均可自定义,必须与财务软件账套中的末级科目编码一致(导入按编码精确匹配)。默认值基于「一般企业会计准则(详细版)」科目表:221101 应付职工薪酬-工资 / 221103 社会保险费 / 221104 住房公积金 / 222112 应交个人所得税 / 224108 其他应付款-代收代付款。
'+ '
🔗 全局科目对照
'+ '
'+ accRow('mSalary','贷方 · 应付职工薪酬—工资(计提/发放)',m.salaryPayable,'221101')+ accRow('mSocial','贷方 · 应付职工薪酬—社会保险费(公司部分)',m.socialPayable,'221103')+ accRow('mFund','贷方 · 应付职工薪酬—住房公积金(公司部分)',m.fundPayable,'221104')+ accRow('mTax','贷方 · 应交税费—代扣个人所得税',m.taxPayable,'222112','发放代扣时挂贷方,缴纳时冲平')+ accRow('mPSocial','过渡 · 个人社保代扣(发放扣 / 缴纳冲)',m.personalSocial,'224108','先发放后缴纳→其他应付款;先缴纳后扣回→其他应收款 122104')+ accRow('mPFund','过渡 · 个人公积金代扣(发放扣 / 缴纳冲)',m.personalFund,'224108')+ accRow('mBank','贷方 · 银行存款(发放/缴纳付款账户)',m.bank,'1002','若账套中 1002 下设有明细户(如 100201),请填末级编码')+ accRow('mCf','资金行现金流编码',m.cashFlow,'cf5','cf5 = 支付给职工以及为职工支付的现金')+ accRow('mPre','资金科目前缀(逗号分隔,命中必须填现金流)',m.cashPrefixes,'1001,1002,1012')+ '
凭证角色科目编码
'+ '
🏢 部门借方科目(按部门归集费用)在【部门管理】中维护
'+ '
'+ DB.depts.map(function(d){return '';}).join('')+'
部门工资科目公司社保科目公司公积金科目
'+esc(d.name)+''+esc(d.salaryAcc||'—')+''+esc(d.socialAcc||'—')+''+esc(d.fundAcc||'—')+'
'+ '
📝 凭证文案与日期模板'+ '
'+ fld('凭证字号(记/收/付/转)','','mWord',m.word,'记')+ ''+ fld('摘要① 工资计提','','mT1',m.tpl1,'可用占位符 {Y}年 {M}月')+ fld('摘要② 五险一金计提','','mT2',m.tpl2,'')+ fld('摘要③ 工资发放','','mT3',m.tpl3,'')+ fld('摘要④ 社保缴纳','','mT4',m.tpl4,'')+ ''+ ''+ ''+ ''+ ''+ '
'+ '
计提凭证固定在归属月最后一天;发放 / 缴纳凭证默认次月 10 / 15 日(可改)。导出后按各凭证自身日期所属账期导入财务软件。
'; c.innerHTML=html; } function saveMap(){ var m=DB.settings.map; function g(id){return ge(id).value.trim();} function needDigit(id,name){var v=g(id);if(!/^\d+$/.test(v)){toast(name+' 必须为纯数字编码','err');throw 'stop';}return v;} try{ m.salaryPayable=needDigit('mSalary','应付职工薪酬-工资'); m.socialPayable=needDigit('mSocial','应付职工薪酬-社保'); m.fundPayable=needDigit('mFund','应付职工薪酬-公积金'); m.taxPayable=needDigit('mTax','应交个税科目'); m.personalSocial=g('mPSocial')||m.personalSocial;m.personalFund=g('mPFund')||m.personalFund; m.bank=needDigit('mBank','银行存款科目'); m.cashFlow=g('mCf')||'cf5';m.cashPrefixes=g('mPre')||'1001,1002,1012'; m.word=g('mWord')||'记';m.deptSuffix=ge('mSuf').value==='1'; m.tpl1=g('mT1');m.tpl2=g('mT2');m.tpl3=g('mT3');m.tpl4=g('mT4'); m.payMonth=ge('mPM1').value;m.payDay=Math.min(28,Math.max(1,parseInt(ge('mPD1').value)||10)); m.payMonth2=ge('mPM2').value;m.socialDay=Math.min(28,Math.max(1,parseInt(ge('mPD2').value)||15)); m.auxEnabled=ge('mAux').value==='1'; saveDB();toast('科目对照与凭证模板已保存','ok'); }catch(e){} } /* ---- 9.8 月末结转(按月结转 · 本年累计 · 所属期 · 反结转) ---- */ var jzState={year:''}; function pgJz(c){ var html='
📅
按月结转 · 当前账期(所属期):'+esc(curPeriod())+' 
每月工资核算并生成凭证后,执行「月末结转」锁定当月数据,系统自动衔接下月个税累计预扣,并把全系统账期(工作台/工资核算/凭证中心的默认月份)转入下一个月;「反结转」按顺序解锁最近结转的月份,账期同步回退。也可随时手动调整所属期。已结转月份如需修改,须先反结转。
'+ '
📅 月末结转 · 本年累计 结转后本月工资表锁定;反结转需按结转的逆序进行
'+ '
'+ '
'; c.innerHTML=html; var years=[]; Object.keys(DB.payroll).forEach(function(m){var y=m.substring(0,4);if(years.indexOf(y)<0)years.push(y);}); var py=curPeriod().substring(0,4); if(years.indexOf(py)<0)years.push(py); years.sort().reverse(); if(!jzState.year||years.indexOf(jzState.year)<0)jzState.year=py; ge('jzYear').innerHTML=years.map(function(y){return '';}).join(''); pgJzRender(); } function pgJzRender(){ var box=ge('jzBody'),cards=ge('jzCards'); if(!box)return; var months=Object.keys(DB.payroll).filter(function(m){return m.substring(0,4)===jzState.year;}).sort(); /* 本年逐月累计(含未结转月份) */ var acc={gross:0,tax:0,net:0,personal:0,company:0},cum={}; months.forEach(function(m){ var t=payrollTotals(DB.payroll[m].rows); acc.gross=r2(acc.gross+t.gross);acc.tax=r2(acc.tax+t.tax);acc.net=r2(acc.net+t.net); acc.personal=r2(acc.personal+t.pTotal);acc.company=r2(acc.company+t.cTotal); cum[m]={gross:acc.gross,tax:acc.tax,net:acc.net,personal:acc.personal,company:acc.company}; }); var closedN=months.filter(function(m){return isClosed(m);}).length; var lastCum=months.length?cum[months[months.length-1]]:{gross:0,tax:0,net:0,personal:0,company:0}; cards.innerHTML='
'+ stat('✅','已结转 / 有工资表月份',closedN+' / '+months.length+' 个月',jzState.year+' 年度','#059669,#047857')+ stat('💰',jzState.year+' 年累计应发',fmt(lastCum.gross),'元 · 含未结转月份','#4f46e5,#4338ca')+ stat('🛡️',jzState.year+' 年累计个人五险一金',fmt(lastCum.personal),'元 · 代扣合计','#d97706,#b45309')+ stat('🏢',jzState.year+' 年累计公司五险一金',fmt(lastCum.company),'元 · 计提合计','#0d9488,#0f766e')+ stat('🧾',jzState.year+' 年累计代扣个税',fmt(lastCum.tax),'元 · 累计预扣','#dc2626,#b91c1c')+ stat('💳',jzState.year+' 年累计实发',fmt(lastCum.net),'元 · 银行代发','#1d4ed8,#1e40af')+ '
'; if(!months.length){ box.innerHTML='
📭'+esc(jzState.year)+' 年暂无工资表,请先前往【工资核算】生成
'; return; } var html='
'+ ''+ ''+ ''; months.slice().reverse().forEach(function(m){ var rec=DB.payroll[m],t=payrollTotals(rec.rows),cl=cum[m]; var vs=buildVouchers(m),okV=vs?validateVouchers(vs).length===0:false; html+=''+ ''+ ''+ ''+ ''+ ''+ ''; }); html+='
月份所属期状态人数本月应发本月个税本月实发本年累计应发本年累计个税本年累计实发凭证检查操作
'+esc(m)+''+(m===curPeriod()?' 当前账期':'')+''+esc(m)+'-01 ~ '+monthEnd(m)+''+(rec.closed?'🔒 已结转':'未结转')+''+t.count+''+fmt(t.gross)+''+fmt(t.tax)+''+fmt(t.net)+''+fmt(cl.gross)+''+fmt(cl.tax)+''+fmt(cl.net)+''+(vs?(okV?'':'✗ 不平'):'')+''+ (rec.closed ?'' :'')+ '
💡 「本年累计」为该月及当年此前所有已生成工资表(含未结转月份)的合计;月末结转前请先完成凭证检查(✓)。对当前账期执行月末结转后,全系统时间(工作台/工资核算/凭证中心默认月份)自动转入下一个月;反结转最近结转的月份时账期自动回退。
'; box.innerHTML=html; } function closeMonth(m){ var rec=DB.payroll[m]; if(!rec||!rec.rows.length)return toast('该月尚无工资表,请先在【工资核算】生成','err'); if(m>curPeriod())return toast('不能结转晚于当前账期('+curPeriod()+')的月份','err'); var t=payrollTotals(rec.rows); var vs=buildVouchers(m),msgs=vs?validateVouchers(vs):['无法生成凭证']; if(msgs.length)return toast('存在校验问题,暂不能结转:'+msgs[0],'err'); var isCur=m===curPeriod(); askConfirm('确认对 '+m+' 执行月末结转?应发 '+fmt(t.gross)+' 元 / 个税 '+fmt(t.tax)+' 元 / 实发 '+fmt(t.net)+' 元;结转后本月工资表锁定。'+(isCur?('结转后全系统账期将转入 '+nextPeriod(m)+'(可反结转回退或手动调整所属期)。'):''),function(){ doCloseMonth(m); }); } function doCloseMonth(m){ var rec=DB.payroll[m]; if(!rec)return false; rec.closed={at:new Date().toISOString()}; var advanced=false; if(m===curPeriod()){DB.meta.period=nextPeriod(m);advanced=true;} saveDB();updatePeriodChip(); if(curPage==='jz')pgJzRender(); toast(m+' 已完成月末结转,本月数据已锁定'+(advanced?(',全系统账期已转入 '+DB.meta.period):''),'ok'); return advanced; } function uncloseMonth(m){ var laterClosed=Object.keys(DB.payroll).filter(function(x){return x>m&&DB.payroll[x].closed;}).sort(); if(laterClosed.length)return toast('存在晚于 '+m+' 的已结转月份('+laterClosed[laterClosed.length-1]+'),请按顺序先反结转 '+laterClosed[laterClosed.length-1],'err'); var rollBack=curPeriod()===nextPeriod(m); askConfirm('确认反结转 '+m+'?'+(rollBack?('全系统账期将回退至 '+m+'。'):'')+'解锁后该月工资数据与凭证可修改。',function(){ doUncloseMonth(m); }); } function doUncloseMonth(m){ if(!DB.payroll[m])return false; var laterClosed=Object.keys(DB.payroll).filter(function(x){return x>m&&DB.payroll[x].closed;}).sort(); if(laterClosed.length)return false; delete DB.payroll[m].closed; var rolled=false; if(curPeriod()===nextPeriod(m)){DB.meta.period=m;rolled=true;} saveDB();updatePeriodChip(); if(curPage==='jz')pgJzRender(); toast(m+' 已反结转(解锁)'+(rolled?(',账期已回退至 '+m):''),'ok'); return rolled; } /* ---- 9.9 数据管理 ---- */ function pgData(c){ var months=Object.keys(DB.payroll).sort(); var demoOn=hasDemoCase(); var html='
🎓 演示案例 默认不载入 · 载入后再次点击该按钮即可移除
'+ '
'+ '
'+(demoOn?'✅':'🎓')+'
当前状态:'+(demoOn?'已载入演示案例':'未载入演示案例(空白账套)')+'
'+ (demoOn?'本账套已写入演示用的 5 个部门8 名员工。点击右上角「🗑️ 移除演示案例」可一键清空部门、员工、工资表与凭证,账套恢复为空白状态(五险一金方案、个税与科目对照设置保留)。' :'系统默认不载入任何演示数据,账套为空白状态。点击右上角「📥 载入演示案例」可一键写入示例数据:5 个部门(综合管理部 / 财务部 / 销售部 / 生产部 / 研发部)与 8 名员工,含完整工资结构与专项附加扣除,便于快速体验工资核算、五险一金计提、个税累计预扣与凭证生成全流程。载入后再次点击该按钮即可移除。')+ '
'+ '
🏢 企业信息 账套名称将用于备份文件名(如:甲公司工资社保备份20260822.fdb)
'+ '
'+ '
'+ '
🗂️ 账套管理(多公司)
'+ '
'+ '
'+ REG.accounts.map(function(a){ return ''+ ''+ ''+ ''; }).join('')+ '
账套名称创建时间状态密码操作
'+esc(a.name)+''+String(a.createdAt||'').replace('T',' ').substring(0,16)+''+(a.id===REG.cur?'✓ 当前':'')+''+(a.auth?'🔒 已设置':'未设置')+''+ (a.auth?' ':' ')+ (a.id===REG.cur?'':' ')+ ''+ (REG.accounts.length>1?' ':'')+'
'+ '
每个账套相互独立(部门/员工/五险一金方案/个税/科目对照/工资表/凭证/结转),顶部下拉可随时无损切换。可为每个账套单独设置密码与密保问题(进入该账套需验证,忘记密码可用密保找回;默认不设置)。备份文件(.fdb)包含全部账套与其密码保护设置。
'+ '
🔗 自动备份 写入所选文件夹 · 单账套+全账套双文件 · 每天覆盖保留当天最后一份
'+ '
'+ '
'+(BK.dir?'✅':'🔗')+'
状态:'+(BK.dir?('已链接:'+esc(BK.dir.name||'备份文件夹')):'未链接备份文件夹')+'(连接状态见顶部指示器,点击可查看/下载备份文件)
链接后:录入数据、生成工资表、结转审核等任何变动都会在约 1.5 秒后自动静默备份到所选文件夹——
· 单账套:账套名称+工资社保备份+日期.fdb(如 甲公司工资社保备份20260822.fdb)
· 全账套:全账套工资社保备份+日期.fdb
每天自动覆盖,保留每天的最后一个备份;设置了下方备份密码时备份自动加密。
'+ '
💾 手工备份与恢复(.fdb · 可选加密)
'+ ''+ '
'+ '
'+ ''+ '
'+ '
.fdb 为本系统全量备份格式(含全部数据的部门/员工/方案/工资表/凭证/结转与账套密码设置)。恢复加密备份时需输入备份密码;恢复将覆盖对应账套的全部数据。
'+ '
📊 全部数据表(Excel · 与界面显示一致)
'+ ''+ '
'+ '
导出一个工作簿,包含:部门档案、员工档案、五险一金方案、个税税率、科目对照、工资总表(全部月份)、部门归集、凭证索引、结转状态——表样与各页面显示一致。导入时档案与配置类(部门/员工/五险一金/个税/科目)会更新数据,工资/凭证/结转类为核对存档不导入。
'+ '
🗂️ 数据概况
'+ '
'+ stat('🏢','部门数',DB.depts.length+' 个','','#1d4ed8,#1e40af')+ stat('👥','员工数',DB.emps.length+' 人','','#0d9488,#0f766e')+ stat('💰','工资表',months.length+' 个月','','#4f46e5,#4338ca')+ stat('📅','账期范围',(months[0]||'—')+' ~ '+(months[months.length-1]||'—'),'最早 ~ 最晚','#059669,#047857')+ '
'+ '
⚠️ 危险操作
'+ ''+ '
'; c.innerHTML=html; } /* ---- 9.9.1 全部数据表导出 / 导入 ---- */ function exportAllTables(){ var db=DB,sheets=[]; var company=db.meta.company||''; sheets.push({name:'部门档案',title:company+' 部门档案', header:['部门名称','工资记账科目','公司社保科目','公司公积金科目','在职人数','备注'], types:['t','t','t','t','i','t'],widths:[16,15,15,15,9,18], rows:db.depts.map(function(d){return [d.name,d.salaryAcc||'',d.socialAcc||'',d.fundAcc||'', db.emps.filter(function(e){return e.deptId===d.id&&e.status!=='离职';}).length,d.memo||''];})}); sheets.push({name:'员工档案',title:company+' 员工档案',note:'共 '+db.emps.length+' 人', header:empHeader(),types:empTypes(),widths:empWidths(),rows:db.emps.map(empRowOf)}); var cfg=db.settings.ins; sheets.push({name:'五险一金方案',title:company+' 五险一金方案', header:['险种','基数口径','个人比例%','公司比例%','社保基数下限','社保基数上限','公积金基数下限','公积金基数上限','大病固定额','舍入精度'], types:['t','t','m','m','m','m','m','m','m','m'],widths:[14,11,11,11,13,13,14,14,12,10], rows:cfg.items.map(function(it){return [it.name,it.base==='fund'?'公积金基数':'社保基数',+it.p||0,+it.c||0, +cfg.socialMin||0,+cfg.socialMax||0,+cfg.fundMin||0,+cfg.fundMax||0,+cfg.medicalExtra||0,+cfg.roundTo||0.01];})}); var t=db.settings.tax; sheets.push({name:'个税税率',title:company+' 个税累计预扣率表', header:['级次','减除费用标准(元/月)','累计应纳税所得额上限(空=不封顶)','预扣率%','速算扣除数'], types:['i','m','m','m','m'],widths:[7,18,28,10,12], rows:t.brackets.map(function(b,i){return [i+1,+t.basic,(b.upto===''||b.upto==null)?'':+b.upto,+b.rate,+b.quick];})}); var m=db.settings.map; sheets.push({name:'科目对照',title:company+' 科目对照与凭证设置', header:['项目','编码/内容'],types:['t','t'],widths:[26,34],rows:[ ['应付职工薪酬-工资',m.salaryPayable],['应付职工薪酬-社保(公司)',m.socialPayable], ['应付职工薪酬-公积金(公司)',m.fundPayable],['应交税费-代扣个税',m.taxPayable], ['个人社保过渡',m.personalSocial],['个人公积金过渡',m.personalFund], ['银行存款',m.bank],['现金流编码',m.cashFlow],['资金科目前缀',m.cashPrefixes], ['凭证字号',m.word],['借方行追加部门后缀(1/0)',m.deptSuffix?'1':'0'], ['摘要①工资计提',m.tpl1],['摘要②五险一金计提',m.tpl2],['摘要③工资发放',m.tpl3],['摘要④社保缴纳',m.tpl4], ['发放凭证月份(next/same)',m.payMonth],['发放日',m.payDay], ['缴纳凭证月份(next/same)',m.payMonth2],['缴纳日',m.socialDay], ['部门辅助核算(1/0)',m.auxEnabled?'1':'0']]}); var months=Object.keys(db.payroll).sort(); var pr=[],gt={gross:0,pSocial:0,pFund:0,pTotal:0,cSocial:0,cFund:0,cTotal:0,special:0,tax:0,net:0}; months.forEach(function(mo){ db.payroll[mo].rows.forEach(function(r){ pr.push([mo,r.code,r.name,r.deptName,r2(r.base),r2(r.post),r2(r.merit),r2(r.allowance),r2(r.bonus),r2(r.ot),r2(r.otherAdd),r2(r.absence),r2(r.otherDeduct),r2(r.gross), r2(r.pSocial),r2(r.pFund),r2(r.pTotal),r2(r.cSocial),r2(r.cFund),r2(r.cTotal),r2(r.special),r2(r.tax),r2(r.net)]); gt.gross=r2(gt.gross+r.gross);gt.pSocial=r2(gt.pSocial+r.pSocial);gt.pFund=r2(gt.pFund+r.pFund);gt.pTotal=r2(gt.pTotal+r.pTotal); gt.cSocial=r2(gt.cSocial+r.cSocial);gt.cFund=r2(gt.cFund+r.cFund);gt.cTotal=r2(gt.cTotal+r.cTotal); gt.special=r2(gt.special+r.special);gt.tax=r2(gt.tax+r.tax);gt.net=r2(gt.net+r.net); }); }); pr.push(['合计','','','',0,0,0,0,0,0,0,0,0,gt.gross,gt.pSocial,gt.pFund,gt.pTotal,gt.cSocial,gt.cFund,gt.cTotal,gt.special,gt.tax,gt.net]); sheets.push({name:'工资总表',title:company+' 工资总表(全部月份)',note:'所属月份逐人明细 · 金额单位:元', header:['月份','工号','姓名','部门','基本工资','岗位工资','绩效工资','补贴','奖金','加班费','其他增项','缺勤扣款','其他扣项','应发工资','个人社保','个人公积金','个人五险一金合计','公司社保','公司公积金','公司五险一金合计','月专项附加','代扣个税','实发工资'], types:['t','t','t','t','m','m','m','m','m','m','m','m','m','m','m','m','m','m','m','m','m','m','m'], widths:[10,8,9,12,10,10,10,9,9,9,9,9,9,12,10,11,13,10,11,13,11,10,12], rows:pr,totalRow:true}); var dgr=[],dcTot={gross:0,pTotal:0,cTotal:0,tax:0,net:0}; months.forEach(function(mo){ deptSummary(db.payroll[mo].rows).forEach(function(d){ dgr.push([mo,d.deptName,d.count,r2(d.gross),r2(d.pSocial),r2(d.pFund),r2(d.pTotal),r2(d.cSocial),r2(d.cFund),r2(d.cTotal),r2(d.tax),r2(d.net)]); dcTot.gross=r2(dcTot.gross+d.gross);dcTot.pTotal=r2(dcTot.pTotal+d.pTotal);dcTot.cTotal=r2(dcTot.cTotal+d.cTotal); dcTot.tax=r2(dcTot.tax+d.tax);dcTot.net=r2(dcTot.net+d.net); }); }); dgr.push(['合计','','',dcTot.gross,0,0,dcTot.pTotal,0,0,dcTot.cTotal,dcTot.tax,dcTot.net]); sheets.push({name:'部门归集',title:company+' 部门归集汇总(全部月份)', header:['月份','部门','人数','应发工资','个人社保','个人公积金','个人五险一金合计','公司社保','公司公积金','公司五险一金合计','代扣个税','实发工资'], types:['t','t','i','m','m','m','m','m','m','m','m','m'], widths:[10,14,7,12,10,11,13,10,11,13,10,12],rows:dgr,totalRow:true}); var vr=[]; months.forEach(function(mo){ var src=db.payroll[mo]&&db.payroll[mo].rows.length?'工资明细':''; var vs=buildVouchers(mo); (vs||[]).forEach(function(v,i){ vr.push([mo,src||'','①②③④'[i]+' '+v.title,v.date,v.word+'-XXXX',v.rows.length,r2(v.totalDeb),r2(v.totalCre),v.balanced?'平衡':'不平衡']); }); var vm=DB.vchManual[mo]; if(vm&&vm.depts.length){ var vs2=buildVouchersManual(mo); (vs2||[]).forEach(function(v,i){ vr.push([mo,'部门汇总','①②③④'[i]+' '+v.title,v.date,v.word+'-XXXX',v.rows.length,r2(v.totalDeb),r2(v.totalCre),v.balanced?'平衡':'不平衡']); }); } }); sheets.push({name:'凭证索引',title:company+' 凭证索引(计提/发放/缴纳全链路)', header:['账期','数据来源','凭证','凭证日期','字号','分录行数','借方合计','贷方合计','校验'], types:['t','t','t','t','t','i','m','m','c'], widths:[10,10,26,12,10,9,13,13,8],rows:vr}); var jzr=months.map(function(mo){ var rec=db.payroll[mo],tt=payrollTotals(rec.rows); return [mo,mo+'-01 ~ '+monthEnd(mo),rec.closed?'已结转':'未结转',rec.closed?String(rec.closed.at).replace('T',' ').substring(0,16):'',tt.count,r2(tt.gross),r2(tt.tax),r2(tt.net)]; }); sheets.push({name:'结转状态',title:company+' 月末结转状态', header:['月份','所属期','状态','结转时间','人数','应发工资','代扣个税','实发工资'], types:['t','t','c','t','i','m','m','m'],widths:[10,24,9,17,7,12,10,12],rows:jzr}); downloadXlsx(sheets,'全部数据表_'+curMonth()); toast('全部数据表已导出(9 个工作表,样式与界面一致)','ok'); } function importAllTables(){ pickImport(function(f,xl){ var plan=[],acts=[]; function findSheet(nm){for(var i=0;i=0)return xl.sheet(xl.names[i]);}return null;} /* 部门 */ var deptAoa=findSheet('部门档案'); var deptList=[]; if(deptAoa){ var dh=findHeader(deptAoa,DEPT_ALIASES); if(dh.row>=0&&dh.map.name!=null){ for(var i=dh.row+1;i=0&&eh.map.code!=null&&eh.map.name!=null){ for(var j=eh.row+1;j=0&&ih.map.name!=null&&ih.map.p!=null){ var items=[]; for(var k=ih.row+1;k=0?'fund':'social', p:pnum(g('p')),c:pnum(g('c'))}); if(!insData){ insData={socialMin:pnum(g('smin')),socialMax:pnum(g('smax')),fundMin:pnum(g('fmin')),fundMax:pnum(g('fmax')), medicalExtra:pnum(g('med')),roundTo:pnum(g('round'))||0.01}; } } if(items.length){insData.items=items;plan.push('五险一金方案:'+items.length+' 个险种(整体替换)');} } } /* 个税税率 */ var taxAoa=findSheet('个税税率'); var taxData=null; if(taxAoa){ var th=findHeader(taxAoa,[['lv',['级次']],['basic',['减除费用']],['upto',['上限']],['rate',['预扣率']],['quick',['速算扣除']]]); if(th.row>=0&&th.map.rate!=null){ var brackets=[],basic=null; for(var k2=th.row+1;k2=0&&mh.map.k!=null){ mapData={}; for(var k3=mh.row+1;k3')+ '

工资总表 / 部门归集 / 凭证索引 / 结转状态 为核对存档表,不参与导入。
'; openDlg('📤','导入数据表',html,[ {t:'取消'}, {t:'确认导入',cls:'btn-p',fn:function(){ var msg=[]; if(deptList.length){var dr=applyDeptRows(deptList);msg.push('部门 新增'+dr.add+'/更新'+dr.upd);} if(empList.length){var er2=applyEmpRows(empList);msg.push('员工 新增'+er2.add+'/更新'+er2.upd+(er2.newDept?(',自动建部门'+er2.newDept):''));} if(insData){DB.settings.ins.socialMin=insData.socialMin;DB.settings.ins.socialMax=insData.socialMax; DB.settings.ins.fundMin=insData.fundMin;DB.settings.ins.fundMax=insData.fundMax; DB.settings.ins.medicalExtra=insData.medicalExtra;DB.settings.ins.roundTo=insData.roundTo; DB.settings.ins.items=insData.items;msg.push('五险一金方案已替换');} if(taxData){DB.settings.tax.basic=taxData.basic;DB.settings.tax.brackets=taxData.brackets;msg.push('个税税率已替换');} if(mapData){ var km={'应付职工薪酬-工资':'salaryPayable','应付职工薪酬-社保(公司)':'socialPayable','应付职工薪酬-公积金(公司)':'fundPayable', '应交税费-代扣个税':'taxPayable','个人社保过渡':'personalSocial','个人公积金过渡':'personalFund','银行存款':'bank', '现金流编码':'cashFlow','资金科目前缀':'cashPrefixes','凭证字号':'word','借方行追加部门后缀(1/0)':'deptSuffix', '摘要①工资计提':'tpl1','摘要②五险一金计提':'tpl2','摘要③工资发放':'tpl3','摘要④社保缴纳':'tpl4', '发放凭证月份(next/same)':'payMonth','发放日':'payDay','缴纳凭证月份(next/same)':'payMonth2','缴纳日':'socialDay','部门辅助核算(1/0)':'auxEnabled'}; for(var mk2 in km){if(mapData[mk2]!=null&&mapData[mk2]!==''){ var v2=mapData[mk2]; if(km[mk2]==='deptSuffix'||km[mk2]==='auxEnabled')DB.settings.map[km[mk2]]=v2==='1'; else if(km[mk2]==='payDay'||km[mk2]==='socialDay')DB.settings.map[km[mk2]]=Math.min(28,Math.max(1,parseInt(v2)||10)); else DB.settings.map[km[mk2]]=v2; }} msg.push('科目对照已更新'); } saveDB();closeDlg();nav('dash'); toast('数据表导入完成:'+msg.join(';'),'ok'); }} ],false,true); }); } function clearDemo(){askConfirm('确认清空全部月份的工资表与结转记录?部门、员工与方案设置将保留。',function(){DB.payroll={};DB.vchManual={};vchState.mode={};saveDB();nav('data');toast('工资表已清空','ok');});} function resetAll(){askConfirm('确认恢复出厂?当前全部数据将被删除,账套恢复为空白状态(演示案例可在「系统 · 数据管理」中按需载入)。',function(){DB=defaultDB();saveDB();nav('dash');toast('已恢复出厂,账套已重置为空白账套','ok');});} /* ============ 9.9 侧边栏折叠(纯展示层,不影响任何业务数据) ============ */ function toggleSidebar(){ var b=document.body,mini=b.classList.toggle('sb-mini'); try{localStorage.setItem('sbMini',mini?'1':'0');}catch(e){} } function initSidebar(){ /* 折叠后仅显示图标,补上原生 tooltip 以便识别 */ document.querySelectorAll('.nav-item').forEach(function(el){ var tx=String(el.textContent||'').replace(/[^\u4e00-\u9fa5A-Za-z0-9()()·\-\s]/g,'').trim(); if(tx)el.setAttribute('title',tx); }); document.addEventListener('keydown',function(e){ if((e.ctrlKey||e.metaKey)&&(e.key==='b'||e.key==='B')){e.preventDefault();toggleSidebar();} }); } /* ============ 10. 启动 ============ */ async function boot(){ initSidebar(); await loadDB(); document.querySelectorAll('.nav-item').forEach(function(el){el.onclick=function(){nav(el.dataset.page);};}); ge('dlgMask').addEventListener('click',function(e){if(e.target===ge('dlgMask'))closeDlg();}); updateAcctSel(); showBackupIndicator(); nav('dash'); var curAcct=getAcct(REG.cur); if(curAcct&&curAcct.auth)lockEnterDlg(REG.cur,null,true); initAutoBackup(); /* 首次建账引导:新建的空账套自动弹出建账向导(与进销存一致) */ if(!DB.meta.openingMonth&&!DB.emps.length&&!Object.keys(DB.payroll).length)setTimeout(dlgBuildAccount,500); } if(typeof document!=='undefined'&&document.addEventListener){ if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot); else boot(); } /* 供自动化测试使用(浏览器中不会执行) */ if(typeof module!=='undefined'&&module.exports){ module.exports={buildRow:buildRow,buildPayroll:buildPayroll,buildVouchers:buildVouchers,buildVouchersManual:buildVouchersManual, vouchersFromAgg:vouchersFromAgg,deptAggFromRows:deptAggFromRows,validateVouchers:validateVouchers, vouchersToAOA:vouchersToAOA,vouchersToReadable:vouchersToReadable,calcIns:calcIns,taxFromBrackets:taxFromBrackets, buildXlsx:buildXlsx,buildSheetXml:buildSheetXml,readXlsx:readXlsx,inflateRaw:inflateRaw,zipEntries:zipEntries, sheetToAOA:sheetToAOA,parseShared:parseShared,findHeader:findHeader,normDate:normDate, loadDB:loadDB,saveDB:saveDB,setDB:function(d){DB=d;},getDB:function(){return DB;},defaultDB:defaultDB, rebuildPayroll:rebuildPayroll,deptAggFromRows:deptAggFromRows, exportAllTables:exportAllTables,vouchersToStdRows:vouchersToStdRows,STD_VCH_HEADER:STD_VCH_HEADER,STD_VCH_WIDTHS:STD_VCH_WIDTHS, dateStamp:dateStamp,sanitizeFileName:sanitizeFileName,getBackupPassword:getBackupPassword,saveBackupPwd:saveBackupPwd, buildBackupBlob:buildBackupBlob,parseBackupBuffer:parseBackupBuffer,detectBackupEncrypted:detectBackupEncrypted, doBackupNow:doBackupNow,singlePack:singlePack,allPack:allPack,normalizeDB:normalizeDB, getReg:function(){return REG;},setReg:function(r){REG=r;},blankAccount:blankAccount, getBK:function(){return BK;},setBK:function(b){Object.assign(BK,b);},renderBkList:renderBkList,doBackupNow:doBackupNow,setupBackupFolder:setupBackupFolder,triggerAutoBackup:triggerAutoBackup,updateBackupIndicator:updateBackupIndicator, openBookSwitcher:openBookSwitcher,doSwitchBook:doSwitchBook,ensureBookAccess:ensureBookAccess,dlgBuildAccount:dlgBuildAccount,cancelLock:function(){try{closeDlg();}catch(e){}if(lockCtx.onCancel)lockCtx.onCancel();}, loadAccount:loadAccount,switchAccount:switchAccount,createAccount:createAccount, renameAccountDlg:renameAccountDlg,deleteAccount:deleteAccount,applyRestorePack:applyRestorePack,restoreBackupBuffer:restoreBackupBuffer, curPeriod:curPeriod,setPeriod:setPeriod,doCloseMonth:doCloseMonth,doUncloseMonth:doUncloseMonth, hashPwd:hashPwd,verifyAcctPwd:verifyAcctPwd,verifyAcctAns:verifyAcctAns, setAccountAuth:setAccountAuth,clearAccountAuth:clearAccountAuth,getAcct:getAcct, parseDeptRow:parseDeptRow,parseEmpRow:parseEmpRow,applyDeptRows:applyDeptRows,applyEmpRows:applyEmpRows, payrollTotals:payrollTotals,deptSummary:deptSummary,r2:r2,pnum:pnum,isClosed:isClosed, __fns:{exportPayroll:exportPayroll,exportVch:exportVch,exportVchReadable:exportVchReadable,exportEmps:exportEmps,dlDeptTpl:dlDeptTpl,dlEmpTpl:dlEmpTpl,dlPayTpl:dlPayTpl,dlManualTpl:dlManualTpl,exportVchCsv:exportVchCsv,importPayChanges:importPayChanges,exportAllTables:exportAllTables,exportBackupFdb:exportBackupFdb,renderBkList:renderBkList}}; }