/* ================================================================
合并财务报表智能工作台 v3.0
引擎:权益法调整 → 长投与权益抵销 → 投资收益与利润分配抵销
→ 内部交易智能矩阵(含递延所得税/坏账/少数股东分摊)
支持报表数手工覆盖(override)并自动向上汇总传播
================================================================ */
/* ============ 报表项目定义(财会〔2019〕6号一般企业格式) ============ */
const FC=(...ks)=>ks.map(k=>[k,1]);
const BS_ITEMS=[
{k:'sec_a',sec:'流动资产:'},
{k:'monetary',s:'A',n:'货币资金'},{k:'tfa',s:'A',n:'交易性金融资产'},{k:'dfa',s:'A',n:'衍生金融资产'},
{k:'nr',s:'A',n:'应收票据'},{k:'ar',s:'A',n:'应收账款'},{k:'arf',s:'A',n:'应收款项融资'},
{k:'prepay',s:'A',n:'预付款项'},{k:'othRec',s:'A',n:'其他应收款'},{k:'inv',s:'A',n:'存货'},
{k:'contractA',s:'A',n:'合同资产'},{k:'hfs',s:'A',n:'持有待售资产'},{k:'nca1y',s:'A',n:'一年内到期的非流动资产'},{k:'othCA',s:'A',n:'其他流动资产'},
{k:'tca',s:'A',n:'流动资产合计',sub:1,f:FC('monetary','tfa','dfa','nr','ar','arf','prepay','othRec','inv','contractA','hfs','nca1y','othCA')},
{k:'sec_na',sec:'非流动资产:'},
{k:'debtInv',s:'A',n:'债权投资'},{k:'othDebtInv',s:'A',n:'其他债权投资'},{k:'ltRec',s:'A',n:'长期应收款'},
{k:'ltip',s:'A',n:'长期股权投资'},{k:'othEqInv',s:'A',n:'其他权益工具投资'},{k:'othNcfA',s:'A',n:'其他非流动金融资产'},
{k:'invProp',s:'A',n:'投资性房地产'},{k:'fa',s:'A',n:'固定资产'},{k:'cip',s:'A',n:'在建工程'},
{k:'ba',s:'A',n:'生产性生物资产'},{k:'oga',s:'A',n:'油气资产'},{k:'rou',s:'A',n:'使用权资产'},
{k:'ia',s:'A',n:'无形资产'},{k:'devExp',s:'A',n:'开发支出'},{k:'gw',s:'A',n:'商誉'},
{k:'ltDef',s:'A',n:'长期待摊费用'},{k:'dta',s:'A',n:'递延所得税资产'},{k:'othNCA',s:'A',n:'其他非流动资产'},
{k:'tnca',s:'A',n:'非流动资产合计',sub:1,f:FC('debtInv','othDebtInv','ltRec','ltip','othEqInv','othNcfA','invProp','fa','cip','ba','oga','rou','ia','devExp','gw','ltDef','dta','othNCA')},
{k:'ta',s:'A',n:'资产总计',grand:1,f:[['tca',1],['tnca',1]]},
{k:'sec_l',sec:'流动负债:'},
{k:'stb',s:'L',n:'短期借款'},{k:'tfl',s:'L',n:'交易性金融负债'},{k:'dfl',s:'L',n:'衍生金融负债'},
{k:'np',s:'L',n:'应付票据'},{k:'ap',s:'L',n:'应付账款'},{k:'adv',s:'L',n:'预收款项'},{k:'contractL',s:'L',n:'合同负债'},
{k:'payrol',s:'L',n:'应付职工薪酬'},{k:'taxP',s:'L',n:'应交税费'},{k:'othPay',s:'L',n:'其他应付款'},
{k:'hfsL',s:'L',n:'持有待售负债'},{k:'ncl1y',s:'L',n:'一年内到期的非流动负债'},{k:'othCL',s:'L',n:'其他流动负债'},
{k:'tcl',s:'L',n:'流动负债合计',sub:1,f:FC('stb','tfl','dfl','np','ap','adv','contractL','payrol','taxP','othPay','hfsL','ncl1y','othCL')},
{k:'sec_nl',sec:'非流动负债:'},
{k:'ltb',s:'L',n:'长期借款'},{k:'bond',s:'L',n:'应付债券'},{k:'leaseL',s:'L',n:'租赁负债'},{k:'ltPay',s:'L',n:'长期应付款'},
{k:'prov',s:'L',n:'预计负债'},{k:'defInc',s:'L',n:'递延收益'},{k:'dtl',s:'L',n:'递延所得税负债'},{k:'othNCL',s:'L',n:'其他非流动负债'},
{k:'tncl',s:'L',n:'非流动负债合计',sub:1,f:FC('ltb','bond','leaseL','ltPay','prov','defInc','dtl','othNCL')},
{k:'tl',s:'L',n:'负债合计',grand:1,f:[['tcl',1],['tncl',1]]},
{k:'sec_e',sec:'所有者权益(或股东权益):'},
{k:'paidIn',s:'E',n:'实收资本(或股本)'},{k:'othEq',s:'E',n:'其他权益工具'},
{k:'pref',s:'E',n:'其中:优先股',memo:1},{k:'perp',s:'E',n:' 永续债',memo:1},
{k:'capRes',s:'E',n:'资本公积'},{k:'treas',s:'E',n:'减:库存股',minus:1},{k:'oci',s:'E',n:'其他综合收益'},
{k:'spres',s:'E',n:'专项储备'},{k:'surRes',s:'E',n:'盈余公积'},{k:'retEarn',s:'E',n:'未分配利润',sp:1},
{k:'tparEq',s:'E',n:'归属于母公司所有者权益合计',sub:1,f:[['paidIn',1],['othEq',1],['capRes',1],['oci',1],['spres',1],['surRes',1],['retEarn',1],['treas',-1]]},
{k:'minEq',s:'E',n:'少数股东权益',sp:1},
{k:'teq',s:'E',n:'所有者权益合计',grand:1,f:[['tparEq',1],['minEq',1]]},
{k:'tle',s:'L',n:'负债和所有者权益总计',grand:1,f:[['tl',1],['teq',1]]}
];
const IS_ITEMS=[
{k:'rev',s:'I',n:'一、营业收入'},{k:'cost',s:'X',n:'减:营业成本'},{k:'tax',s:'X',n:' 税金及附加'},
{k:'sell',s:'X',n:' 销售费用'},{k:'adm',s:'X',n:' 管理费用'},{k:'rd',s:'X',n:' 研发费用'},{k:'fin',s:'X',n:' 财务费用'},
{k:'intexp',s:'X',n:' 其中:利息费用',memo:1},{k:'intinc',s:'I',n:' 利息收入',memo:1},
{k:'othInc',s:'I',n:'加:其他收益'},{k:'invInc',s:'I',n:' 投资收益(损失以“-”号填列)'},
{k:'assocInc',s:'I',n:' 其中:对联营企业和合营企业的投资收益',memo:1},
{k:'hedge',s:'I',n:' 净敞口套期收益(损失以“-”号填列)'},
{k:'fvInc',s:'I',n:' 公允价值变动收益(损失以“-”号填列)'},
{k:'credImp',s:'X',n:' 信用减值损失(损失金额以“+”填列)'},{k:'imp',s:'X',n:' 资产减值损失(损失金额以“+”填列)'},
{k:'dispInc',s:'I',n:' 资产处置收益(损失以“-”号填列)'},
{k:'opProfit',s:'I',n:'二、营业利润(亏损以“-”号填列)',sub:1,f:[['rev',1],['cost',-1],['tax',-1],['sell',-1],['adm',-1],['rd',-1],['fin',-1],['othInc',1],['invInc',1],['hedge',1],['fvInc',1],['credImp',-1],['imp',-1],['dispInc',1]]},
{k:'nonOpInc',s:'I',n:'加:营业外收入'},{k:'nonOpExp',s:'X',n:'减:营业外支出'},
{k:'totProfit',s:'I',n:'三、利润总额(亏损总额以“-”号填列)',sub:1,f:[['opProfit',1],['nonOpInc',1],['nonOpExp',-1]]},
{k:'incTax',s:'X',n:'减:所得税费用'},
{k:'netProfit',s:'I',n:'四、净利润(净亏损以“-”号填列)',sub:1,f:[['totProfit',1],['incTax',-1]]},
{k:'npCont',s:'I',n:'(一)持续经营净利润(净亏损以“-”号填列)',memo:1},{k:'npDisc',s:'I',n:'(二)终止经营净利润(净亏损以“-”号填列)',memo:1},
{k:'parInt',s:'I',n:'(一)归属于母公司所有者的净利润',sub:1,f:[['netProfit',1],['minInt',-1]]},
{k:'minInt',s:'I',n:'(二)少数股东损益',sp:1},
{k:'ociNet',s:'I',n:'五、其他综合收益的税后净额'},
{k:'tci',s:'I',n:'六、综合收益总额',grand:1,f:[['netProfit',1],['ociNet',1]]},
{k:'tciP',s:'I',n:' 归属于母公司所有者的综合收益总额',sub:1,f:[['tci',1],['tciM',-1]]},
{k:'tciM',s:'I',n:' 归属于少数股东的综合收益总额',sp:1},
{k:'epsB',s:'I',n:'七、基本每股收益(元/股)',memo:1},{k:'epsD',s:'I',n:' 稀释每股收益(元/股)',memo:1}
];
const CF_ITEMS=[
{k:'sec1',sec:'一、经营活动产生的现金流量:'},
{k:'cashSales',s:'I',n:'销售商品、提供劳务收到的现金'},{k:'taxRefund',s:'I',n:'收到的税费返还'},{k:'othOpIn',s:'I',n:'收到其他与经营活动有关的现金'},
{k:'opIn',s:'I',n:'经营活动现金流入小计',sub:1,f:FC('cashSales','taxRefund','othOpIn')},
{k:'cashPurch',s:'X',n:'购买商品、接受劳务支付的现金'},{k:'cashEmpl',s:'X',n:'支付给职工以及为职工支付的现金'},{k:'cashTax',s:'X',n:'支付的各项税费'},{k:'othOpOut',s:'X',n:'支付其他与经营活动有关的现金'},
{k:'opOut',s:'X',n:'经营活动现金流出小计',sub:1,f:FC('cashPurch','cashEmpl','cashTax','othOpOut')},
{k:'opNet',s:'I',n:'经营活动产生的现金流量净额',sub:1,f:[['opIn',1],['opOut',-1]]},
{k:'sec2',sec:'二、投资活动产生的现金流量:'},
{k:'invRecov',s:'I',n:'收回投资收到的现金'},{k:'invIncRecv',s:'I',n:'取得投资收益收到的现金'},{k:'faDisp',s:'I',n:'处置固定资产、无形资产和其他长期资产收回的现金净额'},{k:'subDisp',s:'I',n:'处置子公司及其他营业单位收到的现金净额'},{k:'othInvIn',s:'I',n:'收到其他与投资活动有关的现金'},
{k:'invIn',s:'I',n:'投资活动现金流入小计',sub:1,f:FC('invRecov','invIncRecv','faDisp','subDisp','othInvIn')},
{k:'faPurch',s:'X',n:'购建固定资产、无形资产和其他长期资产支付的现金'},{k:'invPaid',s:'X',n:'投资支付的现金'},{k:'subAcq',s:'X',n:'取得子公司及其他营业单位支付的现金净额'},{k:'othInvOut',s:'X',n:'支付其他与投资活动有关的现金'},
{k:'invOut',s:'X',n:'投资活动现金流出小计',sub:1,f:FC('faPurch','invPaid','subAcq','othInvOut')},
{k:'invNet',s:'I',n:'投资活动产生的现金流量净额',sub:1,f:[['invIn',1],['invOut',-1]]},
{k:'sec3',sec:'三、筹资活动产生的现金流量:'},
{k:'capRecv',s:'I',n:'吸收投资收到的现金'},{k:'loanRecv',s:'I',n:'取得借款收到的现金'},{k:'othFinIn',s:'I',n:'收到其他与筹资活动有关的现金'},
{k:'finIn',s:'I',n:'筹资活动现金流入小计',sub:1,f:FC('capRecv','loanRecv','othFinIn')},
{k:'minCapRecv',s:'I',n:' 其中:子公司吸收少数股东投资收到的现金',sp:1,memo:1},
{k:'debtRepay',s:'X',n:'偿还债务支付的现金'},{k:'divPaid',s:'X',n:'分配股利、利润或偿付利息支付的现金'},{k:'othFinOut',s:'X',n:'支付其他与筹资活动有关的现金'},
{k:'finOut',s:'X',n:'筹资活动现金流出小计',sub:1,f:FC('debtRepay','divPaid','othFinOut')},
{k:'minDivPaid',s:'X',n:' 其中:子公司支付给少数股东的股利、利润',sp:1,memo:1},
{k:'finNet',s:'I',n:'筹资活动产生的现金流量净额',sub:1,f:[['finIn',1],['finOut',-1]]},
{k:'fxEffect',s:'I',n:'四、汇率变动对现金及现金等价物的影响'},
{k:'netInc',s:'I',n:'五、现金及现金等价物净增加额',grand:1,f:[['opNet',1],['invNet',1],['finNet',1],['fxEffect',1]]},
{k:'begCash',s:'I',n:'加:期初现金及现金等价物余额'},
{k:'endCash',s:'I',n:'六、期末现金及现金等价物余额',grand:1,f:[['netInc',1],['begCash',1]]}
];
const LIST={bs:BS_ITEMS,is:IS_ITEMS,cf:CF_ITEMS};
const RPT_NAME={bs:'资产负债表',is:'利润表',cf:'现金流量表'};
const IM={bs:{},is:{},cf:{}};
Object.keys(LIST).forEach(r=>LIST[r].forEach(it=>{if(!it.sec)IM[r][it.k]=it;}));
/* 特殊合并行(由引擎计算,自动分录不可直接命中) */
const SPSET={retEarn:1,minEq:1,minInt:1,tciM:1,minCapRecv:1,minDivPaid:1};
/* 个别报表/底稿分公司列显示为“—”的行(retEarn 在个别报表中为正常录入项,不在此列) */
const SPFMT={minEq:1,parInt:1,minInt:1,tciP:1,tciM:1,minCapRecv:1,minDivPaid:1};
/* 工作底稿分公司列与合计列额外隐藏未分配利润(其合并数由引擎轧算) */
const WS_SP={retEarn:1,minEq:1,parInt:1,minInt:1,tciP:1,tciM:1,minCapRecv:1,minDivPaid:1};
function signOf(rpt,it){if(rpt==='cf')return it.s==='I'?1:-1;return (it.s==='A'||it.s==='X'||it.minus)?1:-1;}
/* ============ 工具 ============ */
function num(v){const n=parseFloat(v);return isNaN(n)?0:n;}
function r2(x){return Math.round((num(x)+Number.EPSILON)*100)/100;}
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');}
function fmtP(n){return num(n).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2});}
function uid(){return 'id'+(++state.seq)+'_'+Math.random().toString(36).slice(2,8);}
const UNIT_F={'元':1,'千元':1e3,'万元':1e4,'百万元':1e6};
function UF(){return UNIT_F[state.meta.unit]||1;}
function dnum(n){return num(n)/UF().toLocaleString?num(n)/UF():0;}
function dstr(n){const v=num(n)/UF();return v.toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2});}
function dcell(n,opt){opt=opt||{};if(Math.abs(num(n))<0.005)return opt.zero!=null?opt.zero:'0.00';const s=dstr(n);return num(n)<0?''+s+' ':s;}
function itemName(rpt,k){const it=IM[rpt]&&IM[rpt][k];return it?it.n.replace(/^[\s ]+/,''):k;}
function todayStr(){const t=new Date();return t.getFullYear()+'-'+String(t.getMonth()+1).padStart(2,'0')+'-'+String(t.getDate()).padStart(2,'0');}
/* ============ 状态与持久化 ============ */
const LS_DATA='consol_pro_data_v3',LS_UI='consol_pro_ui_v3';
let state=null,ui={theme:'light',accent:'indigo',collapsed:false};
function defState(){return{
meta:{co:'',period:'',date:todayStr(),unit:'元',taxRate:25,defTax:true,showMemo:true,showSign:true,showParent:true,showZero:true,editMode:true},
companies:[],data:{},tx:[],entries:[],ovr:{},stData:0,stGen:0,seq:0};}
let saveTimer=null;
function save(){try{localStorage.setItem(LS_DATA,JSON.stringify(state));const el=document.getElementById('saveSt');if(el)el.textContent='已自动保存 '+new Date().toLocaleTimeString('zh-CN',{hour12:false});}catch(e){}}
function saveSoon(){clearTimeout(saveTimer);saveTimer=setTimeout(save,400);}
function markData(){state.stData=Date.now();saveSoon();}
function loadData(){try{const s=localStorage.getItem(LS_DATA);if(s){const o=JSON.parse(s);if(o&&o.meta){state=Object.assign(defState(),o);state.meta=Object.assign(defState().meta,o.meta||{});return;}}}catch(e){}state=defState();}
function loadUi(){try{const s=localStorage.getItem(LS_UI);if(s){const o=JSON.parse(s);if(o)ui=Object.assign(ui,o);}}catch(e){}}
function saveUi(){try{localStorage.setItem(LS_UI,JSON.stringify(ui));}catch(e){}}
function applyUi(){document.body.classList.toggle('dark',ui.theme==='dark');document.documentElement.dataset.ac=ui.accent;document.body.classList.toggle('edit-on',!!state.meta.editMode);applySide();const b=document.getElementById('themeBtn');if(b)b.textContent=ui.theme==='dark'?'☀️':'🌙';renderAccents();}
/* ---- 侧栏折叠(状态随主题偏好一起持久化) ---- */
function applySide(){document.body.classList.toggle('side-min',!!ui.collapsed);
const t=document.getElementById('sideToggle');if(t)t.title=(ui.collapsed?'展开':'折叠')+'侧边栏(Ctrl+B)';}
function toggleSide(){ui.collapsed=!ui.collapsed;saveUi();applySide();}
function setTheme(t){ui.theme=t;saveUi();applyUi();}
function toggleTheme(){setTheme(ui.theme==='dark'?'light':'dark');}
function setAccent(a){ui.accent=a;saveUi();applyUi();}
function renderAccents(){const box=document.getElementById('accents');if(!box)return;
const arr=[['indigo','#6366f1'],['blue','#0ea5e9'],['teal','#14b8a6'],['rose','#f43f5e'],['amber','#f59e0b'],['graphite','#64748b']];
box.innerHTML=arr.map(a=>` `).join('');}
/* ============ toast ============ */
let toastTimer=null;
function toast(t,type){const el=document.getElementById('toast');if(!el)return;el.textContent=t;el.className='show '+(type||'');clearTimeout(toastTimer);toastTimer=setTimeout(()=>{el.className='';},3200);}
/* ============ 公司 ============ */
function parentCo(){return state.companies.find(c=>c.type==='parent')||null;}
function subCos(){return state.companies.filter(c=>c.type==='sub'&&c.inScope!==false);}
function allCos(){const p=parentCo();return p?[p].concat(subCos()):subCos();}
function coById(id){return state.companies.find(c=>c.id===id)||null;}
function r(c){return Math.min(1,Math.max(0,num(c.ratio))/100);}
function gv(cid,rpt,k){const d=state.data[cid];return d&&d[rpt]?num(d[rpt][k]):0;}
function setV(cid,rpt,k,v){if(!state.data[cid])state.data[cid]={bs:{},is:{},cf:{}};if(!state.data[cid][rpt])state.data[cid][rpt]={};state.data[cid][rpt][k]=num(v);}
function subEq(c){return ['paidIn','othEq','capRes','oci','spres','surRes','retEarn'].reduce((a,k)=>a+gv(c.id,'bs',k),0)-gv(c.id,'bs','treas');}
function npOf(c){return calcSheet(c.id,'is').netProfit;}
function gwOf(c){if(c.goodwill!=null&&c.goodwill!=='')return num(c.goodwill);return Math.max(0,num(c.cost)-num(c.acqEquity)*r(c));}
function adjAmt(c){return (npOf(c)-num(c.div))*r(c);}
/* ============ 个别报表计算 ============ */
function calcSheet(cid,rpt){
const out={};const items=LIST[rpt];
function val(k){if(out[k]!==undefined)return out[k];out[k]=0;
const it=IM[rpt][k];if(!it)return 0;
if(it.sp&&k!=='retEarn'){return 0;}
if(it.f){let s=0;it.f.forEach(t=>{s+=t[1]*val(t[0]);});out[k]=r2(s);return out[k];}
out[k]=gv(cid,rpt,k);return out[k];}
items.forEach(it=>{if(!it.sec)val(it.k);});
return out;}
/* ============ 内部交易矩阵 → 抵销分录 ============ */
function saleU(t){if(t.mode==='profit')return Math.max(0,num(t.v2));return Math.max(0,(num(t.v1)-num(t.v2))*num(t.v3||0)/100);}
function buildTxEntry(t){
const from=coById(t.from),to=coById(t.to);
if(!from||!to||from.id===to.id)return null;
const TR=state.meta.defTax?num(state.meta.taxRate)/100:0;
const lines=[];const tag=from.name+' → '+to.name;
const A=(rpt,k,side,amt,note)=>{if(Math.abs(num(amt))>0.005)lines.push({rpt,k,side,amt:r2(num(amt)),note:note||''});};
if(t.type==='debt'){
const bal=num(t.v1);if(bal<=0.005)return null;
const P={ar:['ap','ar'],othRec:['othPay','othRec'],nr:['np','nr'],prepay:['contractL','prepay']}[t.kind||'ar'];
A('bs',P[0],'dr',bal,'债务方账面');A('bs',P[1],'cr',bal,'债权方账面');
const bad=num(t.v2);
if(bad>0.005){A('bs',P[1],'dr',bad,'冲回内部坏账准备');A('is','credImp','cr',bad,'冲回信用减值损失');
if(TR>0){A('is','incTax','dr',bad*TR,'转回递延所得税');A('bs','dta','cr',bad*TR,'冲回递延所得税资产');}}
return {name:'内部债权债务抵销:'+tag,lines};
}
if(t.type==='sale'){
const rev=num(t.v1);if(rev<=0.005)return null;
const u=saleU(t);
A('is','rev','dr',rev,'内部收入');A('is','cost','cr',rev-u,'内部成本');
if(u>0.005){A('bs','inv','cr',u,'未实现内部利润');
const T=u*TR;if(T>0.005){A('bs','dta','dr',T,'未实现利润递延所得税');A('is','incTax','cr',T,'递延所得税');}}
const cf=num(t.v4);if(cf>0.005){A('cf','cashPurch','dr',cf,'购货方付现');A('cf','cashSales','cr',cf,'销货方收现');}
const dir=from.type==='sub'?'逆流':'顺流';
return {name:'内部商品购销抵销('+dir+'):'+tag+(u>0.005?',未实现利润 '+fmtP(u):''),lines};
}
if(t.type==='fa'){
const G=num(t.v1),D=num(t.v2);
if(Math.abs(G)<=0.005&&Math.abs(D)<=0.005)return null;
if(G>0.005){A('is','dispInc','dr',G,'未实现处置收益');A('bs','fa','cr',G,'调减固定资产');}
if(G<-0.005){A('is','dispInc','cr',-G,'未实现处置损失');A('bs','fa','dr',-G,'调增固定资产');}
if(D>0.005){A('bs','fa','dr',D,'冲回多提折旧');A('is','adm','cr',D,'冲减多提折旧费用');}
const net=G-D,T=Math.max(0,net)*TR;
if(T>0.005){A('bs','dta','dr',T,'未实现损益递延所得税');A('is','incTax','cr',T,'递延所得税');}
const cf=num(t.v3);if(cf>0.005){A('cf','faPurch','dr',cf,'购建方付现');A('cf','faDisp','cr',cf,'处置方收现');}
const dir=from.type==='sub'?'逆流':'顺流';
return {name:'内部固定资产交易抵销('+dir+'):'+tag,lines};
}
if(t.type==='loan'){
const it=num(t.v1),cf=num(t.v2);
if(it>0.005){A('is','invInc','dr',it,'内部利息收入');A('is','fin','cr',it,'内部利息支出');}
if(cf>0.005){A('cf','othInvOut','dr',cf,'借出方付现');A('cf','loanRecv','cr',cf,'借入方收现');}
if(!lines.length)return null;
return {name:'内部资金借贷及利息抵销:'+tag,lines};
}
return null;
}
/* 汇总全部交易的未实现损益信息(顺流/逆流/坏账) */
function txInfo(){
const TR=state.meta.defTax?num(state.meta.taxRate)/100:0;
const info={TR,downU:0,downT:0,up:[],badP:0,badUp:[]};
state.tx.forEach(t=>{
const from=coById(t.from);if(!from||!coById(t.to)||t.from===t.to)return;
if(t.type==='sale'){const u=saleU(t);if(u<=0.005)return;const T=u*TR;
if(from.type==='sub')info.up.push({r:r(from),u:u,T:T});else{info.downU+=u;info.downT+=T;}}
else if(t.type==='fa'){const net=num(t.v1)-num(t.v2);if(Math.abs(net)<=0.005)return;const T=Math.max(0,net)*TR;
if(from.type==='sub')info.up.push({r:r(from),u:net,T:T});else{info.downU+=net;info.downT+=T;}}
else if(t.type==='debt'){const bad=num(t.v2);if(bad<=0.005)return;const bn=bad*(1-TR);
if(from.type==='sub')info.badUp.push({r:r(from),bn:bn});else info.badP+=bn;}
});
return info;
}
/* ============ 一键生成全部分录 ============ */
function genAll(silent){
const P=parentCo(),subs=subCos();
if(!P){toast('请先在「① 集团与参数」中确认母公司','err');return false;}
const warns=[];
subs.forEach(c=>{
const has=state.data[c.id]&&Object.keys(state.data[c.id].bs||{}).length>0;
if(!has)warns.push(c.name+':尚未录入个别资产负债表');
else if(Math.abs(subEq(c))<0.005)warns.push(c.name+':所有者权益合计为0,请核对报表');
if(Math.abs(num(c.cost))<0.005)warns.push(c.name+':未填写母公司长期股权投资成本');
});
if(!silent&&warns.length&&!confirm('智能校验发现以下问题:\n\n· '+warns.join('\n· ')+'\n\n仍要继续生成分录吗?'))return false;
state.entries=state.entries.filter(e=>e.manual);
const push=(kind,name,lines)=>{const ls=lines.filter(l=>Math.abs(l.amt)>0.005);if(ls.length)state.entries.push({id:uid(),auto:1,kind,name,lines:ls});};
/* 1 权益法调整(成本法→权益法) */
subs.forEach(c=>{const a=adjAmt(c);if(Math.abs(a)<=0.005)return;
push('调整','权益法调整:确认对'+c.name+'投资的净损益份额(调增长投与投资收益)',[
{rpt:'bs',k:'ltip',side:'dr',amt:r2(a),note:'长投调增'},
{rpt:'is',k:'invInc',side:'cr',amt:r2(a),note:'按比例确认投资收益'}]);});
/* 2 长投与子公司所有者权益抵销 */
subs.forEach(c=>{
const lines=[];
['paidIn','othEq','capRes','oci','spres','surRes'].forEach(k=>{const v=gv(c.id,'bs',k);if(Math.abs(v)>0.005)lines.push({rpt:'bs',k,side:'dr',amt:r2(v),note:'子公司账面'});});
const re=gv(c.id,'bs','retEarn');if(Math.abs(re)>0.005)lines.push({rpt:'bs',k:'retEarn',side:'dr',amt:r2(re),note:'子公司未分配利润'});
const tr=gv(c.id,'bs','treas');if(Math.abs(tr)>0.005)lines.push({rpt:'bs',k:'treas',side:'cr',amt:r2(tr),note:'库存股'});
const g=gwOf(c);if(g>0.005)lines.push({rpt:'bs',k:'gw',side:'dr',amt:r2(g),note:'合并商誉'});
lines.push({rpt:'bs',k:'ltip',side:'cr',amt:r2(num(c.cost)+adjAmt(c)),note:'权益法调整后长投余额'});
const mi=r2(subEq(c)*(1-r(c)));if(Math.abs(mi)>0.005)lines.push({rpt:'bs',k:'minEq',side:'cr',amt:mi,note:'少数股东权益'});
let dr=0,cr=0;lines.forEach(l=>{if(l.side==='dr')dr+=l.amt;else cr+=l.amt;});
const d=r2(dr-cr);if(Math.abs(d)>0.005)lines.push({rpt:'bs',k:'capRes',side:d>0?'cr':'dr',amt:Math.abs(d),note:'合并差额(商誉为手工指定时产生)'});
push('抵销','母公司长期股权投资与'+c.name+'所有者权益抵销'+(g>0.005?'(含商誉 '+fmtP(g)+')':''),lines);});
/* 3 投资收益与子公司利润分配抵销 */
subs.forEach(c=>{
const p=npOf(c),rr=r(c),lines=[];
if(Math.abs(p*rr)>0.005)lines.push({rpt:'is',k:'invInc',side:'dr',amt:r2(p*rr),note:'母公司份额'});
if(Math.abs(p*(1-rr))>0.005)lines.push({rpt:'is',k:'minInt',side:'dr',amt:r2(p*(1-rr)),note:'少数股东损益'});
if(Math.abs(num(c.begRE))>0.005)lines.push({rpt:'bs',k:'retEarn',side:'dr',amt:r2(num(c.begRE)),note:'年初未分配利润'});
if(Math.abs(num(c.curSC))>0.005)lines.push({rpt:'bs',k:'retEarn',side:'cr',amt:r2(num(c.curSC)),note:'提取盈余公积'});
if(Math.abs(num(c.div))>0.005)lines.push({rpt:'bs',k:'retEarn',side:'cr',amt:r2(num(c.div)),note:'对所有者的分配'});
const end=gv(c.id,'bs','retEarn');if(Math.abs(end)>0.005)lines.push({rpt:'bs',k:'retEarn',side:'cr',amt:r2(end),note:'年末未分配利润'});
push('抵销','母公司对'+c.name+'投资收益与子公司利润分配抵销',lines);});
/* 4 内部交易智能矩阵 */
state.tx.forEach(t=>{const e=buildTxEntry(t);if(e)push('抵销',e.name,e.lines);});
/* 5 子公司向母公司分红收付现金抵销(少数股东部分保留) */
subs.forEach(c=>{const d=num(c.div)*r(c);if(d<=0.005)return;
push('抵销','内部股利收付现金抵销:'+c.name+' → 母公司',[
{rpt:'cf',k:'divPaid',side:'dr',amt:r2(d),note:'母公司应得份额'},
{rpt:'cf',k:'invIncRecv',side:'cr',amt:r2(d),note:'母公司投资收益收现'}]);});
state.stGen=Date.now();save();
toast('已生成 '+state.entries.filter(e=>e.auto).length+' 笔自动分录(手工分录已保留)','ok');
if(document.getElementById('entriesBox')!==null||document.querySelector('.page.on')&&document.querySelector('.page.on').id==='p3')renderP3();
return true;
}
function oneKey(){if(genAll(true))switchTab('p5');}
/* ============ 核心合并引擎 ============ */
function computeAll(){
const cos=allCos(),P=parentCo(),subs=subCos();
const per={};cos.forEach(c=>{per[c.id]={bs:calcSheet(c.id,'bs'),is:calcSheet(c.id,'is'),cf:calcSheet(c.id,'cf')};});
const sums={bs:{},is:{},cf:{}};
['bs','is','cf'].forEach(rpt=>{LIST[rpt].forEach(it=>{if(it.sec||it.f||it.sp)return;
let s=0;cos.forEach(c=>{s+=num(per[c.id][rpt][it.k]);});sums[rpt][it.k]=r2(s);});});
/* 分录聚合:调整(权益法)与抵销分开列示;自动分录不可命中公式行 */
const adjD={bs:{},is:{},cf:{}},adjC={bs:{},is:{},cf:{}},eliD={bs:{},is:{},cf:{}},eliC={bs:{},is:{},cf:{}};
state.entries.forEach(e=>{
const isAdj=e.kind==='调整';const DD=isAdj?adjD:eliD,CC=isAdj?adjC:eliC;
e.lines.forEach(l=>{
if(SPSET[l.k]&&e.auto)return;
if(!IM[l.rpt]||!IM[l.rpt][l.k])return;
if(l.side==='dr')DD[l.rpt][l.k]=(DD[l.rpt][l.k]||0)+num(l.amt);
else CC[l.rpt][l.k]=(CC[l.rpt][l.k]||0)+num(l.amt);});});
function manualNet(rpt,k){let n=0;state.entries.forEach(e=>{if(!e.manual)return;
e.lines.forEach(l=>{if(l.rpt===rpt&&l.k===k)n+=(l.side==='dr'?1:-1)*num(l.amt);});});
const it=IM[rpt][k];return it?signOf(rpt,it)*n:0;}
const info=txInfo();
/* 特殊行:未分配利润 / 少数股东权益 / 少数股东损益 / 少数股东综合收益 / 少数股东现金流 */
let retEarnC=P?gv(P.id,'bs','retEarn'):0;
subs.forEach(c=>{retEarnC+=(subEq(c)-num(c.acqEquity))*r(c);});
retEarnC-=(info.downU-info.downT);
info.up.forEach(x=>{retEarnC-=(x.u-x.T)*x.r;});
retEarnC+=info.badP;
info.badUp.forEach(x=>{retEarnC+=x.bn*x.r;});
retEarnC=r2(retEarnC+manualNet('bs','retEarn'));
let minEqC=0;subs.forEach(c=>{minEqC+=subEq(c)*(1-r(c));});
info.up.forEach(x=>{minEqC-=(x.u-x.T)*(1-x.r);});
info.badUp.forEach(x=>{minEqC-=x.bn*(1-x.r);});
minEqC=r2(minEqC+manualNet('bs','minEq'));
let minIntC=0;subs.forEach(c=>{minIntC+=npOf(c)*(1-r(c));});
info.up.forEach(x=>{minIntC-=(x.u-x.T)*(1-x.r);});
info.badUp.forEach(x=>{minIntC-=x.bn*(1-x.r);});
minIntC=r2(minIntC+manualNet('is','minInt'));
let tciMC=minIntC;subs.forEach(c=>{tciMC+=gv(c.id,'is','ociNet')*(1-r(c));});
tciMC=r2(tciMC+manualNet('is','tciM'));
const minCapRecvC=r2(subs.reduce((a,c)=>a+num(c.minCapCash),0)+manualNet('cf','minCapRecv'));
const minDivPaidC=r2(subs.reduce((a,c)=>a+num(c.div)*(1-r(c)),0)+manualNet('cf','minDivPaid'));
const spv={bs:{retEarn:retEarnC,minEq:minEqC},is:{minInt:minIntC,tciM:tciMC},cf:{minCapRecv:minCapRecvC,minDivPaid:minDivPaidC}};
const cons={bs:{},is:{},cf:{}},fin={bs:{},is:{},cf:{}};
['bs','is','cf'].forEach(rpt=>{
const c=cons[rpt];
const resolve=k=>{
if(c[k]!==undefined)return c[k];
c[k]=0;
const it=IM[rpt][k];if(!it)return 0;
if(it.sp){c[k]=spv[rpt][k]||0;return c[k];}
if(it.f){let s=0;it.f.forEach(t=>{s+=t[1]*resolve(t[0]);});c[k]=r2(s);return c[k];}
c[k]=r2(num(sums[rpt][k])+signOf(rpt,it)*((adjD[rpt][k]||0)-(adjC[rpt][k]||0)+(eliD[rpt][k]||0)-(eliC[rpt][k]||0)));
return c[k];};
LIST[rpt].forEach(it=>{if(!it.sec)resolve(it.k);});
/* 手工覆盖(override)沿公式链向上传播 */
const dm={};
const dl=k=>{if(dm[k]!==undefined)return dm[k];dm[k]=0;
const it=IM[rpt][k];let d=0;const ov=state.ovr[rpt+'.'+k];
if(ov!=null&&ov!==''&&isFinite(num(ov)))d+=num(ov)-num(c[k]);
if(it&&it.f)it.f.forEach(t=>{d+=t[1]*dl(t[0]);});
dm[k]=d;return d;};
const f=fin[rpt];
LIST[rpt].forEach(it=>{if(!it.sec)f[it.k]=r2(num(c[it.k])+dl(it.k));});
});
return {cos,per,sums,adjD,adjC,eliD,eliC,cons,fin,info};
}
/* ============ 智能校验 ============ */
function checksList(){
const C=computeAll();const out=[];
let bad=0;state.entries.forEach(e=>{let d=0,c=0;e.lines.forEach(l=>{if(l.side==='dr')d+=num(l.amt);else c+=num(l.amt);});if(Math.abs(d-c)>0.01)bad++;});
out.push({lvl:bad?'err':(state.entries.length?'ok':'warn'),t:bad?('有 '+bad+' 笔分录借贷不平衡,请修正'):(state.entries.length?('全部 '+state.entries.length+' 笔抵销与调整分录借贷平衡'):'尚未生成抵销分录,请一键生成'),p:'p3'});
const cos=allCos();
cos.forEach(c=>{
const v=C.per[c.id].bs,d=num(v.ta)-num(v.tle);
if(Math.abs(num(v.ta))>0.005&&Math.abs(d)>0.01)out.push({lvl:'err',t:c.name+' 个别资产负债表不平衡(差额 '+fmtP(d)+')',p:'p2'});
const cd=num(C.per[c.id].cf.endCash)-num(v.monetary);
if(Math.abs(num(v.ta))>0.005&&Math.abs(cd)>0.01)out.push({lvl:'warn',t:c.name+' 现金流量表期末现金与货币资金差异 '+fmtP(cd),p:'p2'});});
subCos().forEach(c=>{
if(Math.abs(subEq(c))<0.005)out.push({lvl:'warn',t:c.name+' 所有者权益为0(个别报表可能未录入)',p:'p2'});
if(Math.abs(num(c.cost))<0.005)out.push({lvl:'warn',t:c.name+' 未填写母公司长期股权投资成本',p:'p1'});});
const dta=num(C.fin.bs.ta)-num(C.fin.bs.tle);
out.push({lvl:Math.abs(dta)>0.01?'err':'ok',t:'合并资产负债表:资产总计 '+fmtP(C.fin.bs.ta)+' = 负债和所有者权益总计 '+fmtP(C.fin.bs.tle)+(Math.abs(dta)>0.01?('(差额 '+fmtP(dta)+',请检查是否有未平衡的手工调整)'):'(平衡)'),p:'p5'});
const dnp=num(C.fin.is.netProfit)-num(C.fin.is.parInt)-num(C.fin.is.minInt);
out.push({lvl:Math.abs(dnp)>0.01?'err':'ok',t:'合并净利润 '+fmtP(C.fin.is.netProfit)+' = 归母净利润 '+fmtP(C.fin.is.parInt)+' + 少数股东损益 '+fmtP(C.fin.is.minInt),p:'p5'});
const dcash=num(C.fin.cf.endCash)-num(C.fin.bs.monetary);
out.push({lvl:Math.abs(dcash)>0.01?(Math.abs(num(C.fin.bs.ta))>0.005?'warn':'ok'):'ok',t:'合并期末现金 '+fmtP(C.fin.cf.endCash)+' | 合并货币资金 '+fmtP(C.fin.bs.monetary)+(Math.abs(dcash)>0.01?('(差异 '+fmtP(dcash)+')'):'(一致)'),p:'p5'});
const chain=num(C.fin.cf.opNet)+num(C.fin.cf.invNet)+num(C.fin.cf.finNet)+num(C.fin.cf.fxEffect)-num(C.fin.cf.netInc);
out.push({lvl:Math.abs(chain)>0.01?'err':'ok',t:'现金流量内部勾稽:经营/投资/筹资净额及汇率影响合计 = 现金净增加额 '+(Math.abs(chain)>0.01?('(差额 '+fmtP(chain)+')'):'(通过)'),p:'p5'});
const ovc=Object.keys(state.ovr||{}).length;
if(ovc)out.push({lvl:'warn',t:'存在 '+ovc+' 处报表手工调整数,导出与报出前请复核',p:'p5'});
if(state.stGen'+(cs.length?cs.map(c=>`${esc(c.t)} 前往 ›
`).join(''):'暂无校验项
')+'';
}
/* ============ 页面框架 ============ */
const TITLES={p0:['工作台总览','集团合并财务报表一站式智能编制'],p1:['① 集团与参数','母子公司架构、持股比例与编制参数'],p2:['② 个别报表录入','母子公司三大报表 · 支持Excel模板批量导入'],p3:['③ 抵销与调整','智能交易矩阵 + 一键生成分录 + 手工调整'],p4:['④ 合并工作底稿','个别数 → 合计数 → 调整抵销 → 合并数 全过程透明'],p5:['⑤ 合并报表输出','三大合并报表 · 可手工修改 · 多格式导出']};
let curTab='p0';
function switchTab(p){curTab=p;
document.querySelectorAll('#nav a').forEach(a=>a.classList.toggle('on',a.dataset.p===p));
document.querySelectorAll('.page').forEach(pg=>pg.classList.toggle('on',pg.id===p));
const t=TITLES[p];document.getElementById('topTitle').textContent=t[0];document.getElementById('topSub').textContent=t[1];
renderCur();window.scrollTo(0,0);}
function renderCur(){const f={p0:renderP0,p1:renderP1,p2:renderP2,p3:renderP3,p4:renderP4,p5:renderP5};(f[curTab]||renderP0)();
const sel=document.getElementById('unitTop');if(sel){sel.innerHTML=['元','千元','万元','百万元'].map(u=>`${u} `).join('');}}
/* ============ ⓪ 工作台 ============ */
function renderP0(){
const C=computeAll();const P=parentCo();const subs=subCos();const cos=allCos();
const filled=cos.filter(c=>Math.abs(num(C.per[c.id].bs.ta))>0.005).length;
const balanced=Math.abs(num(C.fin.bs.ta)-num(C.fin.bs.tle))<0.01&&Math.abs(num(C.fin.bs.ta))>0.005;
const fresh=!state.stGen||state.stGen>=state.stData;
const st=(ok,okT,todoT)=>ok?`✔ ${okT} `:`${todoT} `;
const h=`
${esc(state.meta.co||'我的企业集团')}合并财务报表智能工作台
📅 ${esc(state.meta.period||'未设置报告期间')} 🗓 资产负债表日 ${esc(state.meta.date)} 💰 单位:${esc(state.meta.unit)} 🧾 法定税率 ${esc(state.meta.taxRate)}%${state.meta.defTax?'(含递延所得税智能计算)':'(未启用递延所得税)'}
⚡ 一键生成全套合并报表
⬇ 生成并导出Excel
🏢
${cos.length} 纳入合并主体(母公司1+子公司${subs.length})
🤝
${state.tx.length} 已声明内部交易事项
🧮
${state.entries.length} 抵销与调整分录
📈
${Math.abs(num(C.fin.bs.ta))>0.005?dstr(C.fin.bs.ta):'—'} 合并资产总计(${esc(state.meta.unit)})
${balanced?'✅':'⚠️'}
${Math.abs(num(C.fin.bs.ta))>0.005?(balanced?'平衡':'不平衡'):'待录入'} 报表勾稽状态
${fresh?'🔄':'📌'}
${fresh?'分录为最新':'数据已变更'} ${fresh?'抵销分录与基础数据同步':'建议重新一键生成分录'}
合并编制流程 · 点击直达
1 集团与参数 ${st(subs.length>0,'已纳入 '+subs.length+' 家子公司','待添加子公司')}
设置母子公司、持股比例、长投成本与商誉、宣告股利等合并基础数据。
2 个别报表录入 ${st(filled>=cos.length&&cos.length>0,'已录入 '+filled+'/'+cos.length+' 家','已录入 '+filled+'/'+cos.length+' 家')}
三大报表在线录入,或下载Excel模板分发给子公司填报后一键批量导入。
3 抵销与调整 ${st(state.entries.length>0&&fresh,'已生成 '+state.entries.length+' 笔','待一键生成')}
声明内部交易事实,系统按准则自动生成权益、利润、递延税与现金流抵销分录。
4 合并工作底稿 ${st(state.entries.length>0,'底稿就绪','待生成分录')}
个别数、合计数、调整与抵销、合并数全过程透明可溯源。
5 合并报表输出 ${st(balanced,'三大报表已配平','待完成编制')}
正式格式三大报表,支持数字手工修改、Excel/HTML导出与打印。
智能校验中心 自动检测数据完整性与勾稽关系
${checksBoxHTML(checksList())}
数据管理
📦 载入演示数据
⬇ 导出JSON备份
⬆ 恢复备份
🗑 清空全部数据
所有数据自动保存在本机浏览器,建议定期导出JSON备份。
`;
document.getElementById('p0').innerHTML=h;}
/* ============ ① 集团与参数 ============ */
function updMeta(k,v){if(k==='taxRate')state.meta.taxRate=num(v);else state.meta[k]=v;markData();}
function addSub(){state.companies.push({id:uid(),type:'sub',name:'子公司'+(state.companies.filter(c=>c.type==='sub').length+1),ratio:100,method:'nonsame',cost:0,goodwill:null,acqEquity:0,div:0,begRE:0,curSC:0,minCapCash:0,inScope:true});markData();renderP1();}
function updCo(id,f,v){const c=coById(id);if(!c)return;
if(f==='name'||f==='method')c[f]=v;
else if(f==='inScope')c[f]=!!v;
else c[f]=(v===''||v==null)?(f==='goodwill'?null:0):num(v);
markData();if(f==='ratio'||f==='inScope')renderP1();}
function delCo(id){const c=coById(id);if(!c)return;
if(!confirm('确定删除「'+c.name+'」?其个别报表数据将一并删除。'))return;
state.companies=state.companies.filter(x=>x.id!==id);delete state.data[id];
state.tx=state.tx.filter(t=>t.from!==id&&t.to!==id);
markData();renderP1();}
function renderP1(){
const P=parentCo();const subs=state.companies.filter(c=>c.type==='sub');
let h=`
集团基础信息与编制参数
💡 录入一律以元 为单位;上方“报表展示单位”仅影响报表展示与导出换算。启用递延所得税后,内部交易未实现损益将按法定税率自动生成“借:递延所得税资产 / 贷:所得税费用”抵销分录。
母公司(合并主体) 📝 录入母公司报表
📌 母公司个别报表中「长期股权投资」请按成本法 账面余额填列(系统将自动完成成本法→权益法调整)。
纳入合并范围的子公司 + 添加子公司
📌 权益抵销智能算法 :系统按“持股比例、长投成本、商誉”自动生成 ①成本法→权益法调整 ②长投与所有者权益抵销(含商誉与少数股东权益) ③投资收益与利润分配抵销 ④内部股利现金流抵销。 💡 取消勾选“纳入合并”可临时将子公司移出合并范围(数据保留)。
`;
document.getElementById('p1').innerHTML=h;}
function setParentName(v){let p=parentCo();
if(!p){p={id:uid(),type:'parent',name:v};state.companies.unshift(p);}
else p.name=v;markData();}
function gotoP2(cid){curCo2=cid;switchTab('p2');}
/* ============ ② 个别报表 ============ */
let curCo2=null,curRpt2='bs';
function selCo2(cid){curCo2=cid;renderP2();}
function selRpt2(r){curRpt2=r;renderP2();}
function renderP2(){
const el=document.getElementById('p2');
if(!allCos().length){el.innerHTML='
个别报表录入 请先在「① 集团与参数」中设置母公司或添加子公司。
';return;}
if(!coById(curCo2)||!allCos().some(c=>c.id===curCo2))curCo2=allCos()[0].id;
const cid=curCo2,rpt=curRpt2,c=coById(cid);
let h=`
🚀 推荐做法:下载Excel模板 → 发给各子公司填写 → 返回后批量选中多个文件导入 ,系统按公司名称智能匹配并自动映射三表数据。在线录入时,合计行实时自动计算。
${allCos().map(x=>`${x.type==='parent'?'🏢':'🏭'} ${esc(x.name)}${x.type==='sub'?' ('+(x.ratio==null?100:x.ratio)+'%)':''} `).join('')}
${['bs','is','cf'].map(r=>`${RPT_NAME[r]} `).join('')}
`;
el.innerHTML=h;refreshSheetTotals();updBar2();}
function refreshSheetTotals(){
const cid=curCo2,rpt=curRpt2;if(!coById(cid))return;
const v=calcSheet(cid,rpt);
LIST[rpt].forEach(it=>{if(it.f){const td=document.getElementById('tv_'+it.k);if(td)td.textContent=(Math.abs(num(v[it.k]))<0.005?'0.00':fmtP(v[it.k]));}});}
function updBar2(){
const bar=document.getElementById('chkBar');if(!bar)return;
const cid=curCo2,rpt=curRpt2;const v=calcSheet(cid,rpt);const c=coById(cid);
let h='';
if(rpt==='bs'){const d=num(v.ta)-num(v.tle);
h=`${Math.abs(num(v.ta))<0.005?'尚未录入':(Math.abs(d)<0.01?'✔ 资产 '+fmtP(v.ta)+' = 负债和权益 '+fmtP(v.tle)+'(平衡)':'✘ 不平衡:差额 '+fmtP(d))} `;}
else if(rpt==='is'){h=`净利润 ${fmtP(v.netProfit)} 归母/少数拆分在合并层面自动完成 `;}
else{const d=num(v.endCash)-gv(cid,'bs','monetary');
h=`期末现金 ${fmtP(v.endCash)} ${Math.abs(d)<0.01?'✔ 与资产负债表货币资金一致':'⚠ 与货币资金差异 '+fmtP(d)} `;}
bar.innerHTML=''+h+'
';}
function onCell(cid,rpt,k,v){setV(cid,rpt,k,v);state.stData=Date.now();saveSoon();refreshSheetTotals();updBar2();}
function clearSheet(){
const cid=curCo2,rpt=curRpt2,c=coById(cid);
if(!c||!confirm('确定清零「'+c.name+'」的'+RPT_NAME[rpt]+'全部数据?'))return;
if(!state.data[cid])state.data[cid]={bs:{},is:{},cf:{}};
state.data[cid][rpt]={};markData();renderP2();toast('已清零','ok');}
/* ============ ③ 抵销与调整 ============ */
const MTX_TYPE={debt:{n:'内部债权债务',ic:'🤝'},sale:{n:'内部商品购销',ic:'📦'},fa:{n:'内部固定资产交易',ic:'🏗'},loan:{n:'内部资金借贷',ic:'💰'}};
function addTx(type){const cos=allCos();if(cos.length<1){toast('请先设置公司','err');return;}
state.tx.push({id:uid(),type,from:cos[0].id,to:(cos[1]||cos[0]).id,kind:'ar',mode:'ratio',v1:0,v2:0,v3:0,v4:0});
markData();renderP3();}
function delTx(id){state.tx=state.tx.filter(t=>t.id!==id);markData();renderP3();}
function updTx(id,k,v){const t=state.tx.find(x=>x.id===id);if(!t)return;
t[k]=(k==='from'||k==='to'||k==='kind'||k==='mode')?v:num(v);
if(k==='mode'&&t.mode==='profit'&&t.type==='sale'){/* 切换口径时清空比例 */t.v3=0;}
markData();renderP3();}
function txPreviewHtml(t){
const e=buildTxEntry(t);
if(!e)return '填写金额后自动显示将生成的抵销分录
';
let dr=0,cr=0;e.lines.forEach(l=>{if(l.side==='dr')dr+=l.amt;else cr+=l.amt;});
let s='将自动生成: '+esc(e.name);
e.lines.forEach(l=>{s+='
'+(l.side==='dr'?'借':'贷')+':'+esc(itemName(l.rpt,l.k))+' '+fmtP(l.amt)+(l.note?'('+esc(l.note)+')':'')+'
';});
s+='
借方合计 '+fmtP(dr)+' / 贷方合计 '+fmtP(cr)+(Math.abs(dr-cr)<0.01?' ✓':' ✘不平衡')+'
';
return s;}
function renderP3(){
const el=document.getElementById('p3');const cos=allCos();
const fresh=!state.stGen||state.stGen>=state.stData;
let h=`
智能分录引擎
⚡ 一键生成全部抵销与调整分录
+ 新增手工分录
🧠 智能分录助手
系统将按准则自动生成:①成本法→权益法调整 ②长投与所有者权益抵销(商誉/少数股东权益) ③投资收益与利润分配抵销 ④下方内部交易矩阵全部分录 ⑤内部股利现金流抵销。手工分录始终保留,一键生成只刷新自动分录。 ${fresh?'':'(基础数据已变更,建议重新生成) '}
内部交易智能矩阵 · 像陈述业务一样申报
+ 债权债务
+ 商品购销
+ 固定资产交易
+ 资金借贷
`;
if(!state.tx.length)h+='
暂无内部交易事项。请按业务事实逐项添加,系统将自动判定顺流/逆流、计算未实现损益、计提递延所得税并分摊少数股东损益。
';
else h+='
'+state.tx.map((t,i)=>txCardHTML(t,i)).join('')+'
';
h+='
';
/* 智能分录助手 */
h+=`
智能分录助手 · 自定义借贷
抵销分录 调整分录
+ 添加一行
✓ 生成分录
收起
支持选择合并公式行(未分配利润、少数股东权益等)用于跨期调整;系统实时校验借贷平衡。
分录工作底稿(${state.entries.length} 笔)
自动分录修改任意一行后将自动转为手工分录,不会被一键生成覆盖
${entriesHTML()}
`;
el.innerHTML=h;
renderSmartLines();}
function txCardHTML(t,i){
const td=MTX_TYPE[t.type];
const coSel=(k,label)=>`${label} ${allCos().map(c=>`${esc(c.name)} `).join('')}
`;
let body='';
if(t.type==='debt'){
body=coSel('from','债权方(应收方)')+coSel('to','债务方(应付方)')+
`款项性质
应收/应付账款
其他应收/应付款
应收/应付票据
预付款项/合同负债
期末往来余额(元)
债权方已计提坏账准备
`;
}else if(t.type==='sale'){
body=coSel('from','销售方')+coSel('to','购货方')+
`未实现利润口径
按成本与未售比例计算
直接填未实现利润额
本期内部销售收入
`+
(t.mode==='profit'
?`期末未实现利润
`
:`销售方账面成本
购货方期末未售出比例%
`)+
`本期已现金结算金额
`;
}else if(t.type==='fa'){
body=coSel('from','资产出售方')+coSel('to','资产购入方')+
`未实现处置损益(损失填负数)
购入方本期多提折旧
本期交易收付现金
`;
}else if(t.type==='loan'){
body=coSel('from','资金借出方')+coSel('to','资金借入方')+
`本期内部利息
本期借贷收付现金
`;
}
const dir=t.type==='sale'||t.type==='fa'?((coById(t.from)||{}).type==='sub'?'逆流 ':'顺流 '):'';
return `
${td.ic} 事项 ${i+1} · ${td.n} ${dir} 移除
${body}
${txPreviewHtml(t)}
`;}
/* ---- 手工分录 ---- */
function addManual(){state.entries.push({id:uid(),manual:1,kind:'抵销',name:'手工分录 '+(state.entries.filter(e=>e.manual).length+1),lines:[
{rpt:'bs',k:'ap',side:'dr',amt:0,note:''},{rpt:'bs',k:'ar',side:'cr',amt:0,note:''}]});
saveSoon();renderP3();}
function convManual(id){const e=state.entries.find(x=>x.id===id);if(!e)return;e.auto=0;e.manual=1;saveSoon();renderP3();toast('已转为手工分录,一键生成不再覆盖它','ok');}
function delEntry(id){state.entries=state.entries.filter(x=>x.id!==id);saveSoon();renderP3();}
function editEntryName(id,v){const e=state.entries.find(x=>x.id===id);if(e){e.name=v;saveSoon();}}
function toManual(e){if(e.auto){e.auto=0;e.manual=1;}}
function addLineE(id,side){const e=state.entries.find(x=>x.id===id);if(!e)return;
toManual(e);e.lines.push({rpt:'bs',k:'monetary',side,amt:0,note:''});saveSoon();renderP3();}
function delLineE(id,idx){const e=state.entries.find(x=>x.id===id);if(!e)return;
toManual(e);e.lines.splice(idx,1);saveSoon();renderP3();}
function updLineE(id,idx,f,v){const e=state.entries.find(x=>x.id===id);if(!e)return;
toManual(e);const l=e.lines[idx];if(!l)return;
if(f==='rpt'){l.rpt=v;l.k=(v==='bs'?'monetary':v==='is'?'rev':'cashSales');}
else if(f==='k')l.k=v;
else if(f==='side')l.side=v;
else l.amt=num(v);
saveSoon();renderP3();}
function itemOptsFor(rpt,sel){
let h='';LIST[rpt].forEach(it=>{
if(it.sec||it.f||it.memo)return;
const lbl=(it.sp?'【公式行】':'')+it.n.replace(/^[\s ]+/,'');
h+=`${esc(lbl)} `;});
return h;}
function entriesHTML(){
if(!state.entries.length)return '尚无分录。点击上方「⚡ 一键生成」由系统智能编制,或「+ 新增手工分录」。
';
let h='';
state.entries.forEach((e,idx)=>{
let dr=0,cr=0;e.lines.forEach(l=>{if(l.side==='dr')dr+=num(l.amt);else cr+=num(l.amt);});
const ok=Math.abs(dr-cr)<0.01;
const tag=e.manual?`手工${e.kind==='调整'?'·调整':'·抵销'} `:(e.kind==='调整'?'自动·调整 ':'自动·抵销 ');
h+=`${tag}
借 ${fmtP(dr)} / 贷 ${fmtP(cr)}${ok?' ✓':' ✘'}
${e.auto?`✎ 转手工 `:''}
删除
';
if(e.manual)h+=`
+ 借方行 + 贷方行
`;
h+='
';
});
return h;}
/* ---- 智能分录助手 ---- */
let smartOpen=false,smartRows=[];
function toggleSmart(){smartOpen=!smartOpen;if(smartOpen&&smartRows.length<2)smartRows=[{rpt:'bs',k:'ap',side:'dr',amt:''},{rpt:'bs',k:'ar',side:'cr',amt:''}];renderP3();}
function smartAddLine(){smartRows.push({rpt:'bs',k:'monetary',side:'dr',amt:''});renderSmartLines();}
function smartUpd(i,f,v){smartRows[i][f]=f==='amt'?v:v;}
function smartDel(i){smartRows.splice(i,1);renderSmartLines();}
function renderSmartLines(){
const box=document.getElementById('smartLines');if(!box)return;
box.innerHTML=smartRows.map((r,i)=>`
${['bs','is','cf'].map(x=>`${RPT_NAME[x]} `).join('')}
${itemOptsFor(r.rpt,r.k)}
借方 贷方
×
`).join('')+
`${smartBalText()}
`;}
function smartBal(){let d=0,c=0;smartRows.forEach(r=>{const a=num(r.amt);if(!a)return;if(r.side==='dr')d+=a;else c+=a;});return{d:r2(d),c:r2(c)};}
function smartBalText(){const b=smartBal();if(!b.d&&!b.c)return '填写借贷金额,系统实时校验平衡';const ok=Math.abs(b.d-b.c)<0.01;return '借方合计 '+fmtP(b.d)+' / 贷方合计 '+fmtP(b.c)+(ok?' ✓ 平衡':' ✘ 差额 '+fmtP(b.d-b.c));}
function smartCommit(){
const kind=document.getElementById('smartKind')?document.getElementById('smartKind').value:'抵销';
const lines=smartRows.filter(r=>num(r.amt)>0.005).map(r=>({rpt:r.rpt,k:r.k,side:r.side,amt:r2(num(r.amt)),note:''}));
if(lines.length<2){toast('至少需要两行有效分录','err');return;}
const b=smartBal();
if(Math.abs(b.d-b.c)>0.01){toast('借贷不平衡:借 '+fmtP(b.d)+' / 贷 '+fmtP(b.c),'err');return;}
state.entries.push({id:uid(),manual:1,kind,name:'智能助手·'+kind+'分录 '+(state.entries.filter(e=>e.manual).length+1),lines});
saveSoon();renderP3();toast('分录已加入工作底稿','ok');}
/* ============ ④ 合并工作底稿 ============ */
let wsR='bs';
function wsSel(r){wsR=r;renderP4();}
function renderP4(){
const el=document.getElementById('p4');
if(!allCos().length){el.innerHTML='';return;}
const C=computeAll();const cos=C.cos;
const csum=(k)=>r2(cos.reduce((a,c)=>a+num(C.per[c.id][wsR][k]),0));
let h=`
合并工作底稿 · ${RPT_NAME[wsR]}
⬇ 导出Excel全套底稿
${['bs','is','cf'].map(r=>`${RPT_NAME[r]} `).join('')}
项目
${cos.map(c=>`${esc(c.name)}${c.type==='sub'?''+(c.ratio==null?100:c.ratio)+'% ':''} `).join('')}
合计数 调整借方 调整贷方 抵销借方 抵销贷方 合并数
`;
LIST[wsR].forEach(it=>{
if(it.sec){h+=`${esc(it.n)} `;return;}
const isSp=WS_SP[it.k];
const cls=it.grand?'grand':(it.sub?'sub':(it.memo?'memo':''));
const aD=C.adjD[wsR][it.k]||0,aC=C.adjC[wsR][it.k]||0,eD=C.eliD[wsR][it.k]||0,eC=C.eliC[wsR][it.k]||0;
const finV=C.fin[wsR][it.k];
h+=`${esc(it.n)}${it.sp?' [公式] ':''} `;
cos.forEach(c=>{h+=isSp?'— ':''+dcell(C.per[c.id][wsR][it.k],{zero:'—'})+' ';});
h+=(isSp?'— ':''+dcell(csum(it.k),{zero:'—'})+' ')
+''+dcell(aD,{zero:'—'})+' '+dcell(aC,{zero:'—'})+' '
+''+dcell(eD,{zero:'—'})+' '+dcell(eC,{zero:'—'})+' '
+''+dcell(finV,{zero:'0.00'})+' ';});
h+=`
💡 合并数 = 合计数 ± 调整与抵销;利润表分录对未分配利润的影响已由引擎在合并层面自动轧算,跨表勾稽无需手工结转。手动修改的报表数字(如有)已反映在“合并数”列。
底稿勾稽校验 ${checksBoxHTML(checksList())}
`;
el.innerHTML=h;}
/* ============ ⑤ 合并报表输出 ============ */
let outR='bs';
function outSel(r){outR=r;renderP5();}
function togOpt(k,el){state.meta[k]=!!el.checked;saveSoon();renderP5();}
function togEditMode(el){state.meta.editMode=!!el.checked;document.body.classList.toggle('edit-on',!!state.meta.editMode);saveSoon();}
function clearOvrs(){const n=Object.keys(state.ovr).length;if(!n){toast('当前没有手工调整数','ok');return;}
if(!confirm('确定清除全部 '+n+' 处报表手工调整数,恢复引擎计算结果?'))return;
state.ovr={};saveSoon();renderP5();toast('已恢复全部引擎计算数','ok');}
function rowNoMap(rpt){const m={};let i=0;LIST[rpt].forEach(it=>{if(!it.sec)m[it.k]=++i;});return m;}
function cellTd(rpt,k,fin,cons,edit,extraCls){
const ov=state.ovr[rpt+'.'+k]!=null;
const v=fin[k];
return ``;}
function reportSheetHTML(rpt,edit){
const C=computeAll();const fin=C.fin[rpt];const cons=C.cons[rpt];
const P=parentCo();const rn=rowNoMap(rpt);
const coLine='编制单位:'+esc(state.meta.co||'—')+'(合并)';
const unitLine='金额单位:'+esc(state.meta.unit);
let h='';
if(rpt==='bs'){
h+=`
合并资产负债表
编制单位:${esc(state.meta.co||'—')} (合并)
报告日期:${esc(state.meta.date||'—')}
金额单位:${esc(state.meta.unit)}
行次 资产 期末合并数 ${state.meta.showParent?`母公司数 `:''}行次 负债和所有者权益 期末合并数 ${state.meta.showParent?`母公司数 `:''}`;
const L=[],R=[];let side=0;
LIST.bs.forEach(it=>{if(it.k==='sec_l')side=1;(side?R:L).push(it);});
const n=Math.max(L.length,R.length);
/* 两侧总计数行底部对齐(账户式规范排法) */
const padBottom=arr=>{const d=n-arr.length;if(d<=0)return arr;const out=arr.slice(0,-1);for(let i=0;i{if(!it)return true;if(it.sec)return true;
if(it.memo&&!state.meta.showMemo)return false;
if(!state.meta.showZero&&(it.f||it.sp||it.memo===undefined)){}
if(!state.meta.showZero&&!it.f&&!it.sp&&!it.memo&&Math.abs(num(fin[it.k]))<0.005)return false;
return true;};
const cellHtml=(it,isA)=>{
if(!it)return ' '+(state.meta.showParent?' ':'');
if(it.sec)return ''+esc(it.n)+' ';
const cls=it.grand?'grand':(it.sub?'sub':(it.memo?'memo':''));
const no=isA?++lnA:++lnB;
const pv=state.meta.showParent?(P?C.per[P.id].bs[it.k]:0):0;
const pvCell=state.meta.showParent?`${isSpf('bs',it.k)?'— ':dcell(pv,{zero:''})} `:'';
return `${no} ${esc(it.n)} `+cellTd('bs',it.k,fin,cons,true,cls)+pvCell;};
for(let i=0;i'+cellHtml(A,true)+cellHtml(B,false)+'';}
h+='
';
}else{
const ttl=rpt==='is'?'合并利润表':'合并现金流量表';
const perLine=rpt==='is'?'报告期间:'+esc(state.meta.period||'—'):'报告期间:'+esc(state.meta.period||'—');
h+=`
${ttl}
编制单位:${esc(state.meta.co||'—')} (合并)
报告期间:${esc(state.meta.period||state.meta.date||'—')}
金额单位:${esc(state.meta.unit)}
行次 项目 本期合并金额 `;
LIST[rpt].forEach(it=>{
if(it.sec){h+=`${esc(it.n)} `;return;}
if(it.memo&&!state.meta.showMemo)return;
if(!state.meta.showZero&&!it.f&&!it.sp&&!it.memo&&Math.abs(num(fin[it.k]))<0.005)return;
const cls=it.grand?'grand':(it.sub?'sub':(it.memo?'memo':''));
h+=`${rn[it.k]} ${esc(it.n)} `+cellTd(rpt,it.k,fin,cons,true,cls)+' ';});
h+='
';}
if(state.meta.showSign)h+=`
法定代表人签字: 财务总监/主管会计签字: 制表人签字:
`;
h+='
';
return h;}
function isSpf(rpt,k){return !!SPFMT[k];}
function renderP5(){
const el=document.getElementById('p5');
if(!allCos().length){el.innerHTML='';return;}
const C=computeAll();window._OUT=C;
const ovc=Object.keys(state.ovr).length;
let h=`
${['bs','is','cf','all'].map(r=>`${r==='all'?'📑 全套三表':RPT_NAME[r]} `).join('')}
${outR==='bs'||outR==='all'?reportSheetHTML('bs',true):''}
${outR==='is'||outR==='all'?reportSheetHTML('is',true):''}
${outR==='cf'||outR==='all'?reportSheetHTML('cf',true):''}
报表勾稽校验 ${checksBoxHTML(checksList())}
${state.meta.editMode?'✏️ 编辑模式已开启:直接点击报表中的任意数字 即可手工修改(留空保存则恢复系统自动计算)。手工调整数将以 ✎ 标识并自动汇总至合计行,导出文件中以底色标出。
':''}`;
el.innerHTML=h;}
function cellEdit(td){
if(!state.meta.editMode)return;
if(td.querySelector('input'))return;
const rpt=td.dataset.rpt,k=td.dataset.k;
const cur=num(td.dataset.v);
const C=window._OUT;if(!C)return;
const f=UF();
const inp=document.createElement('input');inp.type='number';inp.step='any';inp.className='cellin';
inp.value=Math.abs(cur)<0.005?'':String(r2(cur/f));
td.textContent='';td.appendChild(inp);inp.focus();if(inp.value)inp.select();
let done=false;
const commit=ok=>{
if(done)return;done=true;
if(ok){
const raw=inp.value.trim();
if(raw===''){delete state.ovr[rpt+'.'+k];}
else{const v=num(raw)*f;
if(Math.abs(v-num(C.cons[rpt][k]))<0.005)delete state.ovr[rpt+'.'+k];
else state.ovr[rpt+'.'+k]=v;}
state.stData=Date.now();saveSoon();}
renderP5();};
inp.addEventListener('keydown',e=>{if(e.key==='Enter')commit(true);else if(e.key==='Escape')commit(false);});
inp.addEventListener('blur',()=>commit(true));}
/* ============ Excel 导出(原生 XLSX,打开无格式警告) ============ */
function xEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');}
function xCell(v,st,ma){return {v:v==null?'':v,s:st||'',ma:ma||0};}
/* --- ZIP(store)与 CRC32 --- */
const CRC_T=(function(){const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1);t[n]=c;}return t;})();
function crc32(u8){let c=0xFFFFFFFF;for(let i=0;i>>8);return (c^0xFFFFFFFF)>>>0;}
function zipStore(files){
const enc=new TextEncoder();const parts=[];const central=[];let offset=0;
const dosTime=12*2048,dosDate=((2026-1980)<<9)|(6<<5)|30;
files.forEach(f=>{
const nameB=enc.encode(f.name),crc=crc32(f.u8),sz=f.u8.length;
const lh=new Uint8Array(30+nameB.length);const lv=new DataView(lh.buffer);
lv.setUint32(0,0x04034b50,true);lv.setUint16(4,20,true);lv.setUint16(6,0x0800,true);lv.setUint16(8,0,true);
lv.setUint16(10,dosTime,true);lv.setUint16(12,dosDate,true);
lv.setUint32(14,crc,true);lv.setUint32(18,sz,true);lv.setUint32(22,sz,true);
lv.setUint16(26,nameB.length,true);
lh.set(nameB,30);
parts.push(lh,f.u8);
const ch=new Uint8Array(46+nameB.length);const cv=new DataView(ch.buffer);
cv.setUint32(0,0x02014b50,true);cv.setUint16(4,20,true);cv.setUint16(6,20,true);cv.setUint16(8,0x0800,true);cv.setUint16(10,0,true);
cv.setUint16(12,dosTime,true);cv.setUint16(14,dosDate,true);
cv.setUint32(16,crc,true);cv.setUint32(20,sz,true);cv.setUint32(24,sz,true);
cv.setUint16(28,nameB.length,true);
cv.setUint32(42,offset,true);
ch.set(nameB,46);
central.push(ch);
offset+=lh.length+sz;});
let cdSize=0;central.forEach(c=>cdSize+=c.length);
const out=new Uint8Array(offset+cdSize+22);let p=0;
parts.concat(central).forEach(u=>{out.set(u,p);p+=u.length;});
const ev=new DataView(out.buffer,p);
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,offset,true);
return out;}
/* --- XLSX 样式 --- */
const XLSX_STYLES=(function(){
const fonts=[{sz:10},{sz:10,b:1},{sz:14,b:1},{sz:9,c:'FF697386'},
{sz:10,b:1,c:'FF047857'},{sz:10,b:1,c:'FFB91C1C'},{sz:10,b:1,c:'FF1E40AF'}];
const fills=[{t:'none'},{t:'gray125'},
{t:'solid',bg:'FFEEF2FB'},{t:'solid',bg:'FFDBE7FF'},{t:'solid',bg:'FFF1F5FB'},{t:'solid',bg:'FFE0E7FF'},
{t:'solid',bg:'FFF8FAFC'},{t:'solid',bg:'FFDBEAFE'},{t:'solid',bg:'FFE8F6FE'},{t:'solid',bg:'FFFDF3D3'},
{t:'solid',bg:'FFD9F5E6'},{t:'solid',bg:'FFFDE3E3'}];
const defs=[
['xT',2,0,0,{h:'center'}],['xSub',3,0,0,{h:'center',wrap:1}],
['xH',1,2,1,{h:'center'}],['xH2',1,3,1,{h:'center'}],
['xSec',1,4,1,{h:'left'}],['xBan',1,5,1,{h:'left'}],
['xI',0,0,1,{h:'left',wrap:1}],['xC',0,0,1,{h:'center'}],['xE',0,0,0,{h:'left'}],
['xIB',1,6,1,{h:'left',wrap:1}],['xIG',1,7,1,{h:'left',wrap:1}],['xIM',3,0,1,{h:'left',wrap:1}],
['xN',0,0,1,{h:'right',num:1}],['xNB',1,6,1,{h:'right',num:1}],['xNG',1,7,1,{h:'right',num:1}],['xNM',3,0,1,{h:'right',num:1}],
['xNC',1,8,1,{h:'right',num:1}],['xOV',1,9,1,{h:'right',num:1}],
['xOk',4,10,1,{h:'left'}],['xBad',5,11,1,{h:'left'}],['xInfo',6,8,1,{h:'left'}]];
const ix={};defs.forEach((d,i)=>ix[d[0]]=i);
return {fonts:fonts,fills:fills,defs:defs,ix:ix};
})();
function xlsxStylesXml(){
const S=XLSX_STYLES;
const fonts=''+S.fonts.map(f=>''+(f.b?' ':'')+' '+(f.c?' ':'')+' ').join('')+' ';
const fills=''+S.fills.map(f=>''+(f.bg?' ':'')+' ').join('')+' ';
const borders=' '
+' ';
const xfs=S.defs.map(d=>{const o=d[4];
return ' ';}).join('');
return ''
+''
+' '
+fonts+fills+borders
+' '
+''+xfs+' '
+' '
+' ';}
/* --- 工作表组装 --- */
function xSheet(name,widths,rows){return {name:name,widths:widths,rows:rows};}
function colRef(i){let s='';i++;while(i>0){const m=(i-1)%26;s=String.fromCharCode(65+m)+s;i=(i-m-1)/26;}return s;}
function sheetXml(sh){
let body='',merges='',mc=0;
sh.rows.forEach((r,ri)=>{
let col=0,rowXml='';
(r||[]).forEach(c0=>{
const c=c0||{v:'',s:'',ma:0};
const span=1+(c.ma||0);
if(c.ma>0){merges+=' ';mc++;}
const hasS=c.s&&c.s!=='xE'&&XLSX_STYLES.ix[c.s]!=null;
const sId=hasS?XLSX_STYLES.ix[c.s]:null;
const v=c.v;
if((v===''||v==null)&&!hasS){col+=span;return;}
const ref=colRef(col)+(ri+1);
if(typeof v==='number'&&isFinite(v))rowXml+=''+v+' ';
else if(v==='')rowXml+=' ';
else rowXml+=''+xEsc(String(v))+' ';
col+=span;});
const isSub=r&&r[0]&&r[0].s==='xSub';
body+=''+rowXml+'
';});
const cols=sh.widths.map((w,i)=>' ').join('');
return ''
+''
+''+cols+' '+body+' '
+(mc?''+merges+' ':'')
+' ';
}
function xlsxFromSheets(sheets){
const enc=new TextEncoder();
const sheetsXml=sheets.map((sh,i)=>({name:'xl/worksheets/sheet'+(i+1)+'.xml',u8:enc.encode(sheetXml(sh))}));
const wb=''
+''
+sheets.map((sh,i)=>' ').join('')
+' ';
const wbRels=''
+''
+sheets.map((sh,i)=>' ').join('')
+' '
+' ';
const ct=''
+''
+' '
+' '
+' '
+sheets.map((sh,i)=>' ').join('')
+' '
+' ';
const rootRels=''
+''
+' '
+' ';
const files=[{name:'[Content_Types].xml',u8:enc.encode(ct)},{name:'_rels/.rels',u8:enc.encode(rootRels)},
{name:'xl/workbook.xml',u8:enc.encode(wb)},{name:'xl/_rels/workbook.xml.rels',u8:enc.encode(wbRels)},
{name:'xl/styles.xml',u8:enc.encode(xlsxStylesXml())}].concat(sheetsXml);
return zipStore(files);}
function sheetParamsX(){
const rows=[];
rows.push([xCell('集团架构与编制参数','xT',9)]);
rows.push([xCell('编制单位:'+(state.meta.co||'—')+' | 报告期间:'+(state.meta.period||'—')+' | 资产负债表日:'+state.meta.date+' | 导出时间:'+new Date().toLocaleString('zh-CN')+' | 金额单位:元','xSub',9)]);
rows.push([xCell('公司名称','xH'),xCell('类型','xH'),xCell('持股比例%','xH'),xCell('取得方式','xH'),xCell('母公司长投成本','xH'),xCell('合并商誉','xH'),xCell('取得日权益','xH'),xCell('本期宣告股利','xH'),xCell('年初未分配利润','xH'),xCell('提取盈余公积','xH'),xCell('少数股东增资现金','xH'),xCell('纳入合并','xH')]);
state.companies.forEach(c=>{
if(c.type==='parent')rows.push([xCell(c.name,'xI'),xCell('母公司','xC'),xCell('—','xC'),xCell('—','xC'),xCell('','xI'),xCell('','xI'),xCell('','xI'),xCell('','xI'),xCell('','xI'),xCell('','xI'),xCell('','xI'),xCell('—','xC')]);
else rows.push([xCell(c.name,'xI'),xCell(c.ratio>=100?'全资':'控股','xC'),xCell(num(c.ratio),'xN'),xCell(c.method==='same'?'同一控制':'非同一控制','xC'),xCell(num(c.cost),'xN'),xCell(gwOf(c),'xN'),xCell(num(c.acqEquity),'xN'),xCell(num(c.div),'xN'),xCell(num(c.begRE),'xN'),xCell(num(c.curSC),'xN'),xCell(num(c.minCapCash),'xN'),xCell(c.inScope!==false?'是':'否','xC')]);});
rows.push([xCell('','xE')]);
rows.push([xCell('内部交易智能矩阵','xBan',9)]);
state.tx.forEach((t,i)=>{
const from=coById(t.from),to=coById(t.to);
rows.push([xCell('事项'+(i+1)+':'+MTX_TYPE[t.type].n+'('+(from?from.name:'?')+' → '+(to?to.name:'?')+')','xI'),xCell('','xE')]);
const fields=t.type==='sale'?[['销售收入',t.v1],[t.mode==='profit'?'未实现利润':'销售成本/未售比例',t.mode==='profit'?t.v2:(t.v2+' / '+t.v3+'%')],['现金结算',t.v4]]
:t.type==='debt'?[['往来余额',t.v1],['坏账准备',t.v2]]
:t.type==='fa'?[['未实现损益',t.v1],['多提折旧',t.v2],['收付现金',t.v3]]
:[['内部利息',t.v1],['收付现金',t.v2]];
fields.forEach(f=>rows.push([xCell(' '+f[0],'xI'),xCell(typeof f[1]==='number'?num(f[1]):String(f[1]||0),typeof f[1]==='number'?'xN':'xC')]));});
if(!state.tx.length)rows.push([xCell('(无内部交易事项)','xIM')]);
return xSheet('集团参数',[26,9,11,12,15,15,15,15,15,15,16,10],rows);}
function sheetIndivX(){
const rows=[];
allCos().forEach((c,ci)=>{
if(ci)rows.push([xCell('','xE')]);
rows.push([xCell(c.name+'(个别报表)','xT',1)]);
['bs','is','cf'].forEach(rpt=>{
rows.push([xCell(RPT_NAME[rpt],'xBan',1)]);
rows.push([xCell('项目','xH'),xCell('金额(元)','xH')]);
const v=calcSheet(c.id,rpt);
LIST[rpt].forEach(it=>{
if(it.sec){rows.push([xCell(it.n,'xSec'),xCell('','xSec')]);return;}
if(it.sp&&it.k!=='retEarn'){rows.push([xCell(it.n,'xIM'),xCell('(合并层面计算)','xIM')]);return;}
if(it.k==='parInt'||it.k==='tciP'){rows.push([xCell(it.n,'xIM'),xCell('(合并层面计算)','xIM')]);return;}
if(it.f){rows.push([xCell(it.n,'xIB'),xCell(r2(num(v[it.k])),'xNB')]);return;}
rows.push([xCell(it.n,'xI'),xCell(gv(c.id,rpt,it.k),'xN')]);});
});});
return xSheet('个别报表',[50,16],rows);}
function sheetEntriesX(){
const rows=[];
rows.push([xCell('合并调整与抵销分录','xT',3)]);
rows.push([xCell('工作底稿分录,不过入个别账簿 | 编制单位:'+(state.meta.co||'—'),'xSub',3)]);
if(!state.entries.length)rows.push([xCell('(尚未生成分录)','xIM')]);
state.entries.forEach(e=>{
let dr=0,cr=0;e.lines.forEach(l=>{if(l.side==='dr')dr+=l.amt;else cr+=l.amt;});
rows.push([xCell('第'+(e.no||'')+'笔【'+(e.manual?'手工':'自动')+'·'+e.kind+'】'+e.name,'xBan',2),xCell(Math.abs(dr-cr)<0.01?'借 '+fmtP(dr)+' / 贷 '+fmtP(cr)+' ✓':'借 '+fmtP(dr)+' / 贷 '+fmtP(cr)+' ✘',Math.abs(dr-cr)<0.01?'xOk':'xBad')]);
rows.push([xCell('科目','xH'),xCell('报表','xH'),xCell('借方','xH'),xCell('贷方','xH')]);
e.lines.forEach(l=>{
const nm=(l.side==='cr'?' ':'')+(l.side==='cr'?'贷:':'借:')+itemName(l.rpt,l.k)+(l.note?'('+l.note+')':'');
rows.push([xCell(nm,'xI'),xCell(RPT_NAME[l.rpt],'xC'),l.side==='dr'?xCell(num(l.amt),'xN'):xCell('','xN'),l.side==='cr'?xCell(num(l.amt),'xN'):xCell('','xN')]);});
rows.push([xCell('','xE')]);});
return xSheet('抵销分录',[46,12,14,14],rows);}
function sheetWSX(rpt){
const C=computeAll();window._XC=C;const cos=C.cos;
const rows=[];
const span=cos.length+6;
rows.push([xCell('合并工作底稿 · '+RPT_NAME[rpt],'xT',span)]);
rows.push([xCell('编制单位:'+(state.meta.co||'—')+' | 报告期间:'+(state.meta.period||state.meta.date)+' | 金额单位:'+state.meta.unit,'xSub',span)]);
const hd=[xCell('项目','xH')];cos.forEach(c=>hd.push(xCell(c.name,'xH')));hd.push(xCell('合计数','xH2'),xCell('调整借','xH'),xCell('调整贷','xH'),xCell('抵销借','xH'),xCell('抵销贷','xH'),xCell('合并数','xH2'));
rows.push(hd);
const csum=k=>r2(cos.reduce((a,c)=>a+num(C.per[c.id][rpt][k]),0));
LIST[rpt].forEach(it=>{
if(it.sec){const r0=[xCell(it.n,'xSec')];for(let i=0;irow.push(isSp?xCell('—','xI'):xCell(r2(num(C.per[c.id][rpt][it.k])/UF()),ns)));
row.push(isSp?xCell('—','xI'):xCell(r2(csum(it.k)/UF()),ns));
row.push(xCell(r2(num(C.adjD[rpt][it.k]||0)/UF()),'xN'),xCell(r2(num(C.adjC[rpt][it.k]||0)/UF()),'xN'),xCell(r2(num(C.eliD[rpt][it.k]||0)/UF()),'xN'),xCell(r2(num(C.eliC[rpt][it.k]||0)/UF()),'xN'));
row.push(xCell(r2(num(C.fin[rpt][it.k])/UF()),state.ovr[rpt+'.'+it.k]!=null?'xOV':'xNC'));
rows.push(row);});
return xSheet('底稿-'+RPT_NAME[rpt],[50].concat(cos.map(()=>20)).concat([20,11,11,11,11,20]),rows);}
function sheetBSX(){
const C=computeAll();window._XC=C;const fin=C.fin.bs,P=parentCo();
const rows=[];
rows.push([xCell('合并资产负债表','xT',state.meta.showParent?7:3)]);
rows.push([xCell('编制单位:'+(state.meta.co||'—')+'(合并) | 报告日:'+state.meta.date+' | 金额单位:'+state.meta.unit,'xSub',state.meta.showParent?7:3)]);
const hd=[xCell('资产','xH'),xCell('期末余额','xH')];if(state.meta.showParent)hd.push(xCell('母公司数','xH'));hd.push(xCell('负债和所有者权益','xH'),xCell('期末余额','xH'));if(state.meta.showParent)hd.push(xCell('母公司数','xH'));
rows.push(hd);
const L=[],R=[];let side=0;LIST.bs.forEach(it=>{if(it.k==='sec_l')side=1;(side?R:L).push(it);});
const n=Math.max(L.length,R.length);
const padBottom=arr=>{const d=n-arr.length;if(d<=0)return arr;const out=arr.slice(0,-1);for(let i=0;i{
if(!it)return [xCell('','xI'),xCell('','xN')].concat(state.meta.showParent?[xCell('','xN')]:[]);
if(it.sec)return [xCell('','xSec'),xCell(it.n,'xSec')].concat(state.meta.showParent?[xCell('','xSec')]:[]);
const ls=it.grand?'xIG':(it.sub?'xIB':(it.memo?'xIM':'xI'));
const ns=it.grand?'xNG':(it.sub?'xNB':(it.memo?'xNM':'xN'));
const arr=[xCell(it.n,ls),xCell(r2(num(fin[it.k])/UF()),state.ovr['bs.'+it.k]!=null?'xOV':ns)];
if(state.meta.showParent)arr.push(xCell(P?r2(num(C.per[P.id].bs[it.k])/UF()):0,SPFMT[it.k]?'xIM':ns));
return arr;};
for(let i=0;i{
if(it.sec){rows.push([xCell(it.n,'xSec'),xCell('','xSec')]);return;}
if(it.memo&&!state.meta.showMemo)return;
const ls=it.grand?'xIG':(it.sub?'xIB':(it.memo?'xIM':'xI'));
const ns=it.grand?'xNG':(it.sub?'xNB':(it.memo?'xNM':'xN'));
rows.push([xCell(it.n,ls),xCell(r2(num(fin[it.k])/UF()),state.ovr[rpt+'.'+it.k]!=null?'xOV':ns)]);});
return xSheet(rpt==='is'?'合并利润表':'合并现金流量表',[50,15],rows);}
function sheetChecksX(){
const rows=[];
rows.push([xCell('合并勾稽关系校验表','xT',2)]);
rows.push([xCell('编制单位:'+(state.meta.co||'—')+' | 导出时间:'+new Date().toLocaleString('zh-CN'),'xSub',2)]);
rows.push([xCell('校验项目','xH'),xCell('结果','xH')]);
checksList().forEach(c=>rows.push([xCell(c.t,'xI'),xCell(c.lvl==='ok'?'✔ 通过':(c.lvl==='warn'?'⚠ 提醒':'✘ 异常'),c.lvl==='ok'?'xOk':(c.lvl==='warn'?'xInfo':'xBad'))]));
return xSheet('勾稽校验',[64,10],rows);}
function buildExcelXML(){
window._XC=computeAll();
return xlsxFromSheets([sheetParamsX(),sheetIndivX(),sheetEntriesX(),sheetWSX('bs'),sheetWSX('is'),sheetWSX('cf'),sheetBSX(),sheetSimpleX('is'),sheetSimpleX('cf'),sheetChecksX()]);}
function exportExcel(){
if(!allCos().length){toast('请先设置公司结构','err');return;}
try{
const bytes=buildExcelXML();
const blob=new Blob([bytes],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='合并财务报表全套_'+(state.meta.co||'集团')+'_'+state.meta.date+'.xlsx';a.click();
setTimeout(()=>URL.revokeObjectURL(a.href),4000);
toast('Excel全套工作簿已导出(原生xlsx,含参数/个别报表/分录/底稿/三表/校验)','ok');
}catch(e){toast('导出失败:'+e.message,'err');}}
/* ============ HTML 报表导出 ============ */
function exportHTML(){
if(!allCos().length){toast('请先设置公司结构','err');return;}
const oldMode=state.meta.editMode;state.meta.editMode=false;
const keep=window._OUT;
let body='';
['bs','is','cf'].forEach(r=>{body+=reportSheetHTML(r,false);});
state.meta.editMode=oldMode;window._OUT=keep;
const checks=checksList().map(c=>`${c.lvl==='ok'?'✔':(c.lvl==='warn'?'⚠':'✘')} ${esc(c.t)}
`).join('');
const html='合并财务报表 · '+(state.meta.co||'')+' '+body+'勾稽校验 '+checks+'
';
const blob=new Blob(['\ufeff'+html],{type:'text/html;charset=utf-8'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='合并财务报表_'+(state.meta.co||'集团')+'_'+state.meta.date+'.html';a.click();
setTimeout(()=>URL.revokeObjectURL(a.href),4000);
toast('HTML报表已导出,可直接浏览器打开或打印为PDF','ok');}
/* ============ Excel 模板下载与导入 ============ */
function buildTemplateXML(co){
const rows=[];
const sheets=['bs','is','cf'].map(rpt=>{
const rs=[];
rs.push([xCell('【合并报表工作台】个别报表填写模板 — 公司名称:'+co.name,'xT',1)]);
rs.push([xCell('公司名称:'+co.name,'xSub',1)]);
rs.push([xCell('报表项目','xH'),xCell('金额(元,合计行自动计算无需填写)','xH')]);
LIST[rpt].forEach(it=>{
if(it.sec){rs.push([xCell(it.n,'xSec'),xCell('','xSec')]);return;}
if(it.f||(it.sp&&it.k!=='retEarn')||it.k==='parInt'||it.k==='tciP'){rs.push([xCell(it.n,'xIB'),xCell('(不填)','xIB')]);return;}
rs.push([xCell(it.n,'xI'),xCell(gv(co.id,rpt,it.k)||'','xN')]);});
return xSheet(RPT_NAME[rpt]+'-'+co.name.slice(0,8),[50,36],rs);});
return xlsxFromSheets(sheets);
}
function downloadTpl(){
const c=coById(curCo2);if(!c){toast('请先选择公司','err');return;}
const bytes=buildTemplateXML(c);
const blob=new Blob([bytes],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='个别报表模板_'+c.name+'.xlsx';a.click();
setTimeout(()=>URL.revokeObjectURL(a.href),4000);
toast('xlsx模板已下载(含三大报表),填写后可在本页批量导入','ok');}
function normName(s){return String(s||'').replace(/[ \s]+/g,'').replace(/^[一二三四五六七八九十]+、/,'').replace(/^(减|加):/,'').replace(/^其中:/,'').replace(/([^)]*)$/,'');}
/* --- 轻量 XML 解析(用于 xlsx 部件与旧版模板) --- */
function xmlDec(s){return String(s).replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,"'").replace(/&/g,'&');}
function xmlParse(s){
s=String(s).replace(/<\?[\s\S]*?\?>/g,'').replace(//g,'');
const root={tag:'#root',attrs:{},children:[],text:''};
const stack=[root];
let i=0;
while(i',lt)+1;if(stack.length>1)stack.pop();continue;}
if(s[lt+1]==='!'){i=s.indexOf('>',lt)+1;continue;}
let j=lt+1;
while(j ]/.test(s[j]))j++;
const el={tag:s.slice(lt+1,j),attrs:{},children:[],text:''};
let k=j,selfClose=false;
while(k'){selfClose=true;k+=2;break;}
if(s[k]==='>'){k++;break;}
const a0=k;
while(k]/.test(s[k]))k++;av=s.slice(a1,k);}}
if(an)el.attrs[an]=xmlDec(av);}
i=k;
stack[stack.length-1].children.push(el);
if(!selfClose)stack.push(el);}
return root;}
function xEls(node,tag){const out=[];(function walk(n){(n.children||[]).forEach(c=>{if(c.tag===tag)out.push(c);walk(c);});})(node);return out;}
function xText(el){return el?(el.text||''):'';}
function xDeepText(el){let t=el.text||'';(el.children||[]).forEach(c=>{t+=xDeepText(c);});return t;}
function colIdxOf(ref){const m=/^([A-Za-z]+)/.exec(ref||'');if(!m)return 0;let s=0;for(const ch of m[1])s=s*26+(ch.toUpperCase().charCodeAt(0)-64);return s-1;}
/* --- ZIP 读取(支持 store 与 deflate) --- */
async function inflateRaw(u8){
if(typeof DecompressionStream==='undefined')throw new Error('当前浏览器不支持解压,请使用新版 Edge/Chrome');
const ds=new DecompressionStream('deflate-raw');
const ab=await new Response(new Blob([u8]).stream().pipeThrough(ds)).arrayBuffer();
return new Uint8Array(ab);}
async function readZipU8(buf){
const dv=new DataView(buf);const u8=new Uint8Array(buf);
let e=-1;
for(let i=buf.byteLength-22;i>=0&&i>=buf.byteLength-22-65535;i--){
if(dv.getUint32(i,true)===0x06054b50){e=i;break;}}
if(e<0)throw new Error('不是有效的xlsx(zip)文件');
const cnt=dv.getUint16(e+10,true);let off=dv.getUint32(e+16,true);
const dec=new TextDecoder();const pend={};
for(let i=0;ibuf.byteLength||dv.getUint32(off,true)!==0x02014b50)break;
const method=dv.getUint16(off+10,true),csize=dv.getUint32(off+20,true);
const nlen=dv.getUint16(off+28,true),elen=dv.getUint16(off+30,true),clen=dv.getUint16(off+32,true);
const lho=dv.getUint32(off+42,true);
const name=dec.decode(u8.subarray(off+46,off+46+nlen));
const lnlen=dv.getUint16(lho+26,true),lelen=dv.getUint16(lho+28,true);
const dstart=lho+30+lnlen+lelen;
const raw=u8.subarray(dstart,dstart+csize);
pend[name]=method===0?Promise.resolve(raw):inflateRaw(raw);
off+=46+nlen+elen+clen;}
const out={};
for(const name of Object.keys(pend))out[name]=await pend[name];
return out;}
/* --- 导入:xlsx(含真实Excel文件)与旧版SpreadsheetML --- */
function resolveImportCo(coName,fname,log){
let co=state.companies.find(c=>normName(c.name)===normName(coName))
||(coName&&state.companies.find(c=>c.name.indexOf(coName)>=0||coName.indexOf(c.name)>=0));
if(co)return co;
const base=normName(fname.replace(/\.[^.]+$/,''));
co=base&&state.companies.find(c=>base.indexOf(normName(c.name))>=0);
if(co)return co;
if(!coName){log.push('✘ '+fname+':未识别到公司名称');return null;}
co={id:uid(),type:'sub',name:coName,ratio:100,method:'nonsame',cost:0,goodwill:null,acqEquity:0,div:0,begRE:0,curSC:0,minCapCash:0,inScope:true};
state.companies.push(co);log.push('+ 自动新增子公司:'+coName);
return co;}
function cellTextOf(c,shared){
const t=c.attrs.t;
if(t==='inlineStr'){const is=xEls(c,'is')[0];return xDeepText(is||c).trim();}
const vEl=xEls(c,'v')[0];
let v=xText(vEl);
if(t==='s')v=shared[parseInt(v,10)]||'';
return String(v).trim();}
async function importXlsxBuffer(buf,fname,log){
const td=new TextDecoder();
let ab=buf;
if(ab&&ab.buffer&&ab.byteLength!==undefined)ab=ab.buffer.slice(ab.byteOffset||0,(ab.byteOffset||0)+ab.byteLength);
const files=await readZipU8(ab);
const shared=[];
if(files['xl/sharedStrings.xml']){
xEls(xmlParse(td.decode(files['xl/sharedStrings.xml'])),'si').forEach(si=>shared.push(xDeepText(si)));}
const wbRoot=xmlParse(td.decode(files['xl/workbook.xml']||''));
const rels={};
if(files['xl/_rels/workbook.xml.rels']){
xEls(xmlParse(td.decode(files['xl/_rels/workbook.xml.rels'])),'Relationship').forEach(r=>{rels[r.attrs.Id]=r.attrs.Target;});}
const sheetEls=xEls(wbRoot,'sheet');
const docs=sheetEls.map((sh,i)=>{
const rid=sh.attrs['r:id']||sh.attrs.id||'';
let t=rels[rid]||rels['rId'+(i+1)]||('worksheets/sheet'+(i+1)+'.xml');
t=String(t).replace(/^\//,'');
if(!/^xl\//.test(t))t='xl/'+t;
const raw=files[t]||files['xl/worksheets/sheet'+(i+1)+'.xml'];
return {name:sh.attrs.name||'',root:raw?xmlParse(td.decode(raw)):null};});
let coName='';
docs.forEach(d=>{
if(coName||!d.root)return;
const cs=xEls(d.root,'c').slice(0,60);
for(const c of cs){const m=cellTextOf(c,shared).match(/公司名称[::]\s*(.+)$/);if(m){coName=m[1].trim();break;}}});
const co=resolveImportCo(coName,fname,log);
if(!co)return;
let total=0;
docs.forEach(d=>{
if(!d.root)return;
const rpt=/资产负债/.test(d.name)?'bs':(/利润/.test(d.name)?'is':(/现金流/.test(d.name)?'cf':null));
if(!rpt)return;
const idx={};LIST[rpt].forEach(it=>{
if(it.sec||it.f||(it.sp&&it.k!=='retEarn')||it.k==='parInt'||it.k==='tciP')return;
idx[normName(it.n)]=it.k;});
xEls(d.root,'row').forEach(row=>{
const arr=[];
xEls(row,'c').forEach(c=>{arr[colIdxOf(c.attrs.r)]=cellTextOf(c,shared);});
let key='',keyCol=-1;
for(let i=0;i{if(!coName){const m=xDeepText(d).match(/公司名称[::]\s*(.+)$/);if(m)coName=m[1].trim();}});
const co=resolveImportCo(coName,fname,log);
if(!co)return;
let total=0;
xEls(root,'Worksheet').forEach(ws=>{
const nm=ws.attrs['ss:Name']||ws.attrs.name||'';
const rpt=/资产负债/.test(nm)?'bs':(/利润/.test(nm)?'is':(/现金流/.test(nm)?'cf':null));
if(!rpt)return;
const idx={};LIST[rpt].forEach(it=>{
if(it.sec||it.f||(it.sp&&it.k!=='retEarn')||it.k==='parInt'||it.k==='tciP')return;
idx[normName(it.n)]=it.k;});
let cnt=0;
xEls(ws,'Row').forEach(row=>{
const byCol={};let pos=0;
xEls(row,'Cell').forEach(c=>{
const ia=c.attrs['ss:Index'];if(ia)pos=parseInt(ia,10)-1;
const d=xEls(c,'Data')[0];byCol[pos]=d?xDeepText(d):'';pos++;});
const key=idx[normName(byCol[0])];
if(!key)return;
const v=parseFloat(String(byCol[1]).replace(/,/g,''));
if(!isNaN(v)){setV(co.id,rpt,key,v);cnt++;}});
total+=cnt;});
log.push('✔ '+fname+' → '+co.name+':共导入 '+total+' 项');}
function importStmts(inp){
const files=Array.from(inp.files||[]);if(!files.length)return;
const log=[];let done=0;
const finish=()=>{markData();save();renderCur();
alert('批量导入结果:\n\n'+log.join('\n')+'\n\n提示:可在“① 集团与参数”中核对新增公司的持股比例等信息。');
inp.value='';};
files.forEach(f=>{
(async()=>{
try{
const buf=await f.arrayBuffer();
const u8=new Uint8Array(buf);
if(u8[0]===0x50&&u8[1]===0x4b)await importXlsxBuffer(buf,f.name,log);
else importXmlText(new TextDecoder('utf-8').decode(buf),f.name,log);
}catch(err){log.push('✘ '+f.name+':解析失败('+err.message+')');}
if(++done===files.length)finish();
})();});
}
/* ============ 备份与恢复 ============ */
function backupJSON(){
const blob=new Blob([JSON.stringify(state,null,1)],{type:'application/json;charset=utf-8'});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='合并报表备份_'+(state.meta.co||'集团')+'_'+todayStr()+'.json';a.click();
setTimeout(()=>URL.revokeObjectURL(a.href),4000);
toast('JSON备份已导出','ok');}
function restoreJSON(inp){
const f=inp.files[0];if(!f)return;
const rd=new FileReader();
rd.onload=e=>{try{
const o=JSON.parse(e.target.result);
if(!o||!o.meta||!o.companies)throw new Error('格式不正确');
state=Object.assign(defState(),o);state.meta=Object.assign(defState().meta,o.meta||{});
save();renderCur();toast('备份已恢复:共 '+state.companies.length+' 家公司','ok');
}catch(err){toast('恢复失败:'+err.message,'err');}
inp.value='';};
rd.readAsText(f,'UTF-8');}
function clearAllData(){
if(!confirm('⚠ 将清空全部数据(集团、个别报表、内部交易、分录、手工调整)。\n建议先导出JSON备份。\n\n确定继续?'))return;
state=defState();
state.companies.push({id:uid(),type:'parent',name:'母公司'});
save();renderCur();toast('已重置','ok');}
/* ============ 演示数据 ============ */
function loadDemo(){
if(state.entries.length||state.tx.length||Object.keys(state.data).length){
if(!confirm('载入演示数据将覆盖当前全部数据(可先备份)。确定继续?'))return;}
state=defState();
state.meta.co='华信控股集团有限公司';state.meta.period='2026年1-6月';state.meta.date='2026-06-30';
state.seq=1;
const P={id:'cP',type:'parent',name:'华信控股(母公司)'};
const A={id:'cA',type:'sub',name:'华信智造(全资子公司)',ratio:100,method:'same',cost:3150000,goodwill:null,acqEquity:3150000,div:400000,begRE:0,curSC:150000,minCapCash:0,inScope:true};
const B={id:'cB',type:'sub',name:'华信新材(控股子公司)',ratio:80,method:'nonsame',cost:2300000,goodwill:null,acqEquity:2575000,div:500000,begRE:0,curSC:125000,minCapCash:0,inScope:true};
state.companies=[P,A,B];
state.data={
cP:{bs:{monetary:4700000,ar:3000000,inv:4000000,othCA:1000000,ltip:5450000,fa:6000000,ia:400000,stb:2000000,ap:2500000,payrol:500000,taxP:1000000,ltb:3000000,paidIn:10000000,capRes:2000000,surRes:1600000,retEarn:1950000},
is:{rev:20000000,cost:12000000,tax:200000,sell:1000000,adm:1800000,fin:300000,invInc:800000,incTax:1375000},
cf:{cashSales:22000000,cashPurch:13000000,cashEmpl:2000000,cashTax:1800000,othOpOut:200000,invIncRecv:800000,faPurch:1000000,loanRecv:1000000,debtRepay:1500000,divPaid:300000,begCash:700000}},
cA:{bs:{monetary:1750000,ar:1000000,inv:1500000,fa:2500000,ap:1200000,taxP:300000,ltb:1000000,paidIn:3000000,surRes:300000,retEarn:950000},
is:{rev:8000000,cost:5000000,tax:80000,adm:920000,incTax:500000},
cf:{cashSales:8500000,cashPurch:5000000,cashEmpl:800000,cashTax:500000,othOpOut:200000,faPurch:500000,debtRepay:500000,divPaid:400000,begCash:1150000}},
cB:{bs:{monetary:1000000,ar:600000,inv:900000,fa:1600000,ia:100000,ap:700000,taxP:300000,paidIn:2000000,capRes:500000,surRes:200000,retEarn:500000},
is:{rev:5000000,cost:3000000,tax:50000,adm:450000,incTax:375000},
cf:{cashSales:5200000,cashPurch:2800000,cashEmpl:600000,cashTax:400000,othOpOut:100000,faPurch:200000,divPaid:500000,begCash:400000}}};
state.tx=[
{id:uid(),type:'debt',from:'cA',to:'cP',kind:'ar',v1:500000,v2:0,v3:0,v4:0},
{id:uid(),type:'sale',from:'cP',to:'cB',mode:'ratio',v1:1000000,v2:700000,v3:30,v4:1000000}];
state.stData=Date.now();
genAll(true);
save();
renderCur();
alert('演示数据已载入并自动生成全部分录。\n\n集团架构:华信控股持有「华信智造」100%(同一控制)、「华信新材」80%(非同一控制,自动确认商誉 240,000)。\n内部交易:A对母公司应收账款 500,000;母公司向B销售商品 1,000,000(成本 700,000,未售出30%,未实现利润 90,000,含递延所得税自动计算);内部购销收付现金 1,000,000;子公司宣告股利自动抵销。\n\n预期结果(递延所得税开启、税率25%):\n· 合并资产总计 = 负债和所有者权益总计 = 29,722,500.00 元\n· 归母净利润 5,657,500.00 + 少数股东损益 225,000.00 = 合并净利润 5,882,500.00\n· 合并未分配利润 3,482,500.00;少数股东权益 640,000.00;递延所得税资产 22,500.00\n· 经营活动净额 8,300,000.00;期末现金 7,450,000.00 = 合并货币资金\n\n可在「⑤ 合并报表输出」中查看并点击任意数字手工调整体验。');}
/* ============ 启动 ============ */
function init(){
loadUi();loadData();
if(!state.companies.length)state.companies.push({id:uid(),type:'parent',name:'母公司'});
if(!parentCo()){const s=state.companies.find(c=>c.type==='sub');if(s)s.type='parent';}
if(!state.meta.date)state.meta.date=todayStr();
applyUi();renderCur();
document.addEventListener('keydown',e=>{
const k=e.key.toLowerCase();
if((e.ctrlKey||e.metaKey)&&k==='s'){e.preventDefault();save();toast('已保存','ok');return;}
if((e.ctrlKey||e.metaKey)&&k==='b'){e.preventDefault();toggleSide();}});
window.addEventListener('beforeunload',save);}
window.addEventListener('DOMContentLoaded',init);