buhanka-finance-app 1.74.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +72 -12
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -309,7 +309,7 @@ tr.subtotal>td:first-child{color:var(--accent);box-shadow:inset 3px 0 0 var(--ac
|
|
|
309
309
|
|
|
310
310
|
<script>
|
|
311
311
|
/* ============================================================ БУХАНКА · Финансы v2 ============================================================ */
|
|
312
|
-
const APP_VERSION='10.
|
|
312
|
+
const APP_VERSION='10.9.1';
|
|
313
313
|
const SCHEMA=9; /* версия структуры данных; растёт при доработках */
|
|
314
314
|
const STORE_KEY='buhanka_finance_v9';/* НЕ МЕНЯТЬ НИКОГДА — иначе данные «слетят» при обновлении файла */
|
|
315
315
|
const LEGACY_KEYS=['buhanka_finance_v8','buhanka_finance_v7','buhanka_finance_v6','buhanka_finance_v5'];
|
|
@@ -821,8 +821,9 @@ const isCashOp=o=>o.cash!==false;
|
|
|
821
821
|
/* ── каналы выручки: безнал / Яндекс Еда / наличные ── */
|
|
822
822
|
const CHANNELS=[['bank','Безналичные'],['yandex','Яндекс Еда'],['cash','Наличные']];
|
|
823
823
|
const CH_NAME=Object.fromEntries(CHANNELS);
|
|
824
|
+
const CH_ALIAS={acq:'bank',card:'bank',sbp:'bank',terminal:'bank'};
|
|
824
825
|
function opChannel(o){
|
|
825
|
-
if(o.ch) return o.ch;
|
|
826
|
+
if(o.ch) return CH_ALIAS[o.ch]||(CH_NAME[o.ch]?o.ch:'bank');
|
|
826
827
|
const a=(ST.accounts||[]).find(x=>x.name===o.account);
|
|
827
828
|
return (a&&accType(a)==='cash')?'cash':'bank';
|
|
828
829
|
}
|
|
@@ -948,6 +949,7 @@ function cfGroupRaw(cat){
|
|
|
948
949
|
if(/оборудован|ремонт|обеспечительн|капвлож/.test(c)) return 'inv';
|
|
949
950
|
return 'op';
|
|
950
951
|
}
|
|
952
|
+
function isIncomeCat(cat){ const c=(ST.opCats||[]).find(x=>x.name===cat); return c?c.grp==='in':(cat==='Выручка'); }
|
|
951
953
|
function cfGroup(cat){
|
|
952
954
|
const c=(ST.opCats||[]).find(x=>x.name===cat);
|
|
953
955
|
if(c) return c.grp==='in'?'op':c.grp;
|
|
@@ -968,8 +970,10 @@ function planSet(mo,d,key,v){ if(isLocked(mo)){toast('Месяц закрыт');
|
|
|
968
970
|
ST.plan=ST.plan||{}; ST.plan[mo]=ST.plan[mo]||{}; ST.plan[mo][d]=ST.plan[mo][d]||{};
|
|
969
971
|
const n=parseNum(v); if(n==null)delete ST.plan[mo][d][key]; else ST.plan[mo][d][key]=Math.abs(n);
|
|
970
972
|
save(); render(); }
|
|
971
|
-
function fixedPlanDay(cat,d){
|
|
972
|
-
|
|
973
|
+
function fixedPlanDay(cat,d,mo){
|
|
974
|
+
if(mo&&(ST.operations||[]).some(o=>o.month===mo&&o.category===cat&&o.type==='Расход'&&isCashOp(o))) return 0;
|
|
975
|
+
return (ST.fixed||[]).filter(f=>f.active&&f.category===cat&&parseInt(f.payDay)===d)
|
|
976
|
+
.reduce((s,f)=>s+Number(f.amount||0),0); }
|
|
973
977
|
/* последний день месяца, по который заведены операции */
|
|
974
978
|
function lastOpDayOf(mi){ const pre=`${YEAR}-${String(mi+1).padStart(2,'0')}-`; let d=0;
|
|
975
979
|
(ST.operations||[]).forEach(o=>{ if(o.date&&o.date.indexOf(pre)===0){ const n=Number(o.date.slice(8,10))||0; if(n>d)d=n; } });
|
|
@@ -1015,10 +1019,11 @@ function cfDayMatrix(mi){
|
|
|
1015
1019
|
if(g==='fin'){ fin[o.category]=(fin[o.category]||0)+(o.type==='Доход'?amt:-amt); return; }
|
|
1016
1020
|
if(g==='inv'){ inv[o.category]=(inv[o.category]||0)+(o.type==='Доход'?amt:-amt); return; }
|
|
1017
1021
|
if(o.type==='Доход'){ const ch=opChannel(o); inc[ch]=(inc[ch]||0)+amt; }
|
|
1022
|
+
else if(isIncomeCat(o.category)){ const ch=opChannel(o); inc[ch]=(inc[ch]||0)-amt; } /* возврат покупки — минус к выручке */
|
|
1018
1023
|
else exp[o.category]=(exp[o.category]||0)+amt; });
|
|
1019
1024
|
if(!actual){ /* прогноз: постоянные расходы + ручные правки */
|
|
1020
1025
|
CHANNELS.forEach(([k])=>{ const m=planVal(mo,d,'ch:'+k); if(m!=null) inc[k]=m; });
|
|
1021
|
-
cats.op.forEach(c=>{ const m=planVal(mo,d,'ex:'+c); if(m!=null) exp[c]=m; else { const f=fixedPlanDay(c,d); if(f) exp[c]=(exp[c]||0)+f; } });
|
|
1026
|
+
cats.op.forEach(c=>{ const m=planVal(mo,d,'ex:'+c); if(m!=null) exp[c]=m; else { const f=fixedPlanDay(c,d,mo); if(f) exp[c]=(exp[c]||0)+f; } });
|
|
1022
1027
|
cats.fin.forEach(c=>{ const m=planVal(mo,d,'fi:'+c); if(m!=null) fin[c]=-m; });
|
|
1023
1028
|
cats.inv.forEach(c=>{ const m=planVal(mo,d,'in:'+c); if(m!=null) inv[c]=-m; });
|
|
1024
1029
|
}
|
|
@@ -1970,15 +1975,70 @@ function renderCF(C){
|
|
|
1970
1975
|
}
|
|
1971
1976
|
|
|
1972
1977
|
/* ---------- CF обратный ---------- */
|
|
1978
|
+
/* сумма операций по статье со знаком: доход «+», расход «−» */
|
|
1979
|
+
function opsSigned(mm,cat){ return (ST.operations||[]).filter(o=>mm.includes(o.month)&&isCashOp(o)&&!isTransfer(o)&&(cat==null||o.category===cat))
|
|
1980
|
+
.reduce((s,o)=>s+(o.type==='Доход'?Math.abs(o.amount):-Math.abs(o.amount)),0); }
|
|
1981
|
+
/* мост: чистая прибыль по начислению → фактическое движение денег */
|
|
1982
|
+
function cfiBridge(period){
|
|
1983
|
+
const mm=periodMonths(period);
|
|
1984
|
+
const pnlByCat={}, order=[];
|
|
1985
|
+
structOf('Общий').forEach(r=>{ if(r.role!=='value')return;
|
|
1986
|
+
const cs=rowCats(r); if(!cs.length)return;
|
|
1987
|
+
cs.forEach(c=>{ if(pnlByCat[c]==null){ pnlByCat[c]=0; order.push(c); } });
|
|
1988
|
+
pnlByCat[cs[0]]+=rowFact('Общий',mm,r); });
|
|
1989
|
+
const extra=[];
|
|
1990
|
+
(ST.operations||[]).forEach(o=>{ if(isTransfer(o)||!mm.includes(o.month)||!isCashOp(o))return;
|
|
1991
|
+
const g=cfGroup(o.category); if(g!=='op'&&g!=='in')return;
|
|
1992
|
+
if(pnlByCat[o.category]==null&&extra.indexOf(o.category)<0) extra.push(o.category); });
|
|
1993
|
+
const R2=v=>Math.round(v*100)/100;
|
|
1994
|
+
const rows=order.concat(extra).map(c=>{ const pnl=R2(pnlByCat[c]||0), cash=R2(opsSigned(mm,c));
|
|
1995
|
+
return {cat:c,pnl,cash,delta:R2(cash-pnl),isExtra:pnlByCat[c]==null}; });
|
|
1996
|
+
const net=R2(netProfit('Общий',period));
|
|
1997
|
+
const adj=R2(rows.reduce((s,x)=>s+x.delta,0));
|
|
1998
|
+
const cats=cfCats(), fin=[], inv=[];
|
|
1999
|
+
cats.fin.forEach(c=>{ const v=R2(opsSigned(mm,c)); if(v) fin.push({cat:c,v}); });
|
|
2000
|
+
cats.inv.forEach(c=>{ const v=R2(opsSigned(mm,c)); if(v) inv.push({cat:c,v}); });
|
|
2001
|
+
const finSum=R2(fin.reduce((s,x)=>s+x.v,0)), invSum=R2(inv.reduce((s,x)=>s+x.v,0));
|
|
2002
|
+
const opFlow=R2(net+adj), total=R2(opFlow+finSum+invSum), factTotal=R2(opsSigned(mm,null));
|
|
2003
|
+
return {rows,net,adj,fin,inv,finSum,invSum,opFlow,total,factTotal,diff:R2(total-factTotal)};
|
|
2004
|
+
}
|
|
1973
2005
|
function renderCFI(C){
|
|
1974
|
-
const P=ST.ui.period
|
|
2006
|
+
const P=ST.ui.period;
|
|
1975
2007
|
document.getElementById('pageSub').textContent=`${P} · обратный (косвенный) метод`;
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
<
|
|
1981
|
-
<
|
|
2008
|
+
const B=cfiBridge(P);
|
|
2009
|
+
const money=v=>`<td class="num ${v<0?'neg':v>0?'pos':''}">${v?fmt(v):'—'}</td>`;
|
|
2010
|
+
let html=`<div class="cards">
|
|
2011
|
+
<div class="card"><div class="k">Чистая прибыль</div><div class="v ${B.net<0?'neg':'pos'}">${fmt(B.net)} ₽</div><div class="d muted">по начислению, PnL</div></div>
|
|
2012
|
+
<div class="card"><div class="k">Корректировки</div><div class="v ${B.adj<0?'neg':'pos'}">${fmt(B.adj)} ₽</div><div class="d muted">начисление → касса</div></div>
|
|
2013
|
+
<div class="card"><div class="k">Поток от операций</div><div class="v ${B.opFlow<0?'neg':'pos'}">${fmt(B.opFlow)} ₽</div></div>
|
|
2014
|
+
<div class="card"><div class="k">Денежный поток</div><div class="v ${B.total<0?'neg':'pos'}">${fmt(B.total)} ₽</div><div class="d muted">${B.diff?'⚠ расхождение '+fmt(B.diff):'сходится с операциями'}</div></div></div>`;
|
|
2015
|
+
html+=`<div class="panel"><div class="hd"><h3>От чистой прибыли к деньгам · ${P}</h3><div class="spacer"></div>
|
|
2016
|
+
<span class="muted small">разница = оплачено − начислено</span></div>
|
|
2017
|
+
<div class="bd" style="padding:0"><div class="tbl-wrap"><table class="pnl" style="min-width:760px">
|
|
2018
|
+
<colgroup><col><col class="colmoney"><col class="colmoney"><col class="colmoney"></colgroup>
|
|
2019
|
+
<thead><tr><th>Показатель</th><th class="num">Начислено (PnL)</th><th class="num">Оплачено (касса)</th><th class="num">Разница</th></tr></thead><tbody>`;
|
|
2020
|
+
html+=`<tr class="subtotal"><td class="nmcell">Чистая прибыль по PnL</td><td class="num"></td><td class="num"></td>${money(B.net)}</tr>`;
|
|
2021
|
+
html+=`<tr class="header"><td colspan="4" class="nmcell">Корректировки: начисление → деньги</td></tr>`;
|
|
2022
|
+
B.rows.filter(x=>x.delta).sort((a,b)=>Math.abs(b.delta)-Math.abs(a.delta)).forEach(x=>{
|
|
2023
|
+
html+=`<tr class="value"><td class="nmcell">${esc(cfLabel(x.cat))}${x.isExtra?' <span class="muted small">(нет строки в PnL)</span>':''}</td>
|
|
2024
|
+
<td class="num">${x.pnl?fmt(x.pnl):'—'}</td><td class="num">${x.cash?fmt(x.cash):'—'}</td>${money(x.delta)}</tr>`; });
|
|
2025
|
+
const zero=B.rows.filter(x=>!x.delta&&(x.pnl||x.cash));
|
|
2026
|
+
if(zero.length) html+=`<tr class="value"><td class="nmcell muted">Совпадают начисление и оплата: ${zero.map(x=>esc(cfLabel(x.cat))).join(', ')}</td><td class="num"></td><td class="num"></td><td class="num">—</td></tr>`;
|
|
2027
|
+
html+=`<tr class="subtotal"><td class="nmcell">Итого корректировок</td><td class="num"></td><td class="num"></td>${money(B.adj)}</tr>`;
|
|
2028
|
+
html+=`<tr class="subtotal"><td class="nmcell">Поток от операционной деятельности</td><td class="num"></td><td class="num"></td>${money(B.opFlow)}</tr>`;
|
|
2029
|
+
if(B.fin.length){ html+=`<tr class="header"><td colspan="4" class="nmcell">Финансовая деятельность</td></tr>`;
|
|
2030
|
+
B.fin.forEach(x=>{ html+=`<tr class="value"><td class="nmcell">${esc(cfLabel(x.cat))}</td><td class="num"></td><td class="num">${fmt(x.v)}</td>${money(x.v)}</tr>`; });
|
|
2031
|
+
html+=`<tr class="subtotal"><td class="nmcell">Поток от финансовой деятельности</td><td class="num"></td><td class="num"></td>${money(B.finSum)}</tr>`; }
|
|
2032
|
+
if(B.inv.length){ html+=`<tr class="header"><td colspan="4" class="nmcell">Инвестиционная деятельность</td></tr>`;
|
|
2033
|
+
B.inv.forEach(x=>{ html+=`<tr class="value"><td class="nmcell">${esc(cfLabel(x.cat))}</td><td class="num"></td><td class="num">${fmt(x.v)}</td>${money(x.v)}</tr>`; });
|
|
2034
|
+
html+=`<tr class="subtotal"><td class="nmcell">Поток от инвестиционной деятельности</td><td class="num"></td><td class="num"></td>${money(B.invSum)}</tr>`; }
|
|
2035
|
+
html+=`<tr class="grand"><td class="nmcell">ИТОГО денежный поток</td><td class="num"></td><td class="num"></td>${money(B.total)}</tr>`;
|
|
2036
|
+
html+=`<tr class="value"><td class="nmcell muted">Контроль: сальдо всех операций за период</td><td class="num"></td><td class="num">${fmt(B.factTotal)}</td>
|
|
2037
|
+
<td class="num ${B.diff?'neg':'pos'}">${B.diff?fmt(B.diff):'сходится'}</td></tr>`;
|
|
2038
|
+
html+=`</tbody></table></div>
|
|
2039
|
+
<p class="hint" style="padding:10px 16px 0">Разница «оплачено − начислено» положительная — деньги остались в кассе (начислили, но ещё не заплатили);
|
|
2040
|
+
отрицательная — заплатили больше, чем попало в расходы месяца. Переводы между своими счетами в расчёт не входят.</p>
|
|
2041
|
+
</div></div>`;
|
|
1982
2042
|
C.innerHTML=html;
|
|
1983
2043
|
}
|
|
1984
2044
|
function cfRows(cf,key,P,locked){
|