buhanka-finance-app 1.7.0 → 1.9.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 +84 -46
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -235,7 +235,7 @@ tr.subtotal>td:first-child{color:var(--accent);box-shadow:inset 3px 0 0 var(--ac
|
|
|
235
235
|
|
|
236
236
|
<script>
|
|
237
237
|
/* ============================================================ БУХАНКА · Финансы v2 ============================================================ */
|
|
238
|
-
const APP_VERSION='
|
|
238
|
+
const APP_VERSION='4.1';
|
|
239
239
|
const SCHEMA=3; /* версия структуры данных; растёт при доработках */
|
|
240
240
|
const STORE_KEY='buhanka_finance_v9';/* НЕ МЕНЯТЬ НИКОГДА — иначе данные «слетят» при обновлении файла */
|
|
241
241
|
const LEGACY_KEYS=['buhanka_finance_v8','buhanka_finance_v7','buhanka_finance_v6','buhanka_finance_v5'];
|
|
@@ -573,6 +573,9 @@ function opChannel(o){
|
|
|
573
573
|
}
|
|
574
574
|
const FIN_BASE=['Получение займа','Возврат займа','Дивиденды','Прочее (фин)'];
|
|
575
575
|
const INV_BASE=['Покупка оборудования','Ремонт','Обеспечительные платежи','Прочее (инвест)'];
|
|
576
|
+
/* в CF статья называется «Закупки», в PnL остаётся «Себестоимость» */
|
|
577
|
+
const CF_LABEL={'Себестоимость':'Закупки'};
|
|
578
|
+
const cfLabel=c=>CF_LABEL[c]||c;
|
|
576
579
|
/* ── группировка статей по разделам CF ── */
|
|
577
580
|
function cfGroup(cat){
|
|
578
581
|
if(FIN_BASE.includes(cat)) return 'fin';
|
|
@@ -1282,18 +1285,18 @@ function renderCF(C){
|
|
|
1282
1285
|
html+=row('sub','Поступления',M.days.map(d=>cell(num(d.inSum))).join(''),`<b>${num(sumOf(x=>x.inSum))}</b>`);
|
|
1283
1286
|
CHANNELS.forEach(([k,n])=>{ html+=row('val',n,M.days.map(d=>planCell(d,'ch:'+k,num(d.inc[k]||0))).join(''),num(sumOf(x=>x.inc[k]||0))); });
|
|
1284
1287
|
html+=row('sub','Расходы',M.days.map(d=>cell(neg(d.exSum),'neg')).join(''),`<b class="neg">${neg(sumOf(x=>x.exSum))}</b>`);
|
|
1285
|
-
M.cats.op.forEach(c=>{ html+=row('val',esc(c),M.days.map(d=>planCell(d,'ex:'+c,neg(d.exp[c]||0))).join(''),neg(sumOf(x=>x.exp[c]||0))); });
|
|
1288
|
+
M.cats.op.forEach(c=>{ html+=row('val',esc(cfLabel(c)),M.days.map(d=>planCell(d,'ex:'+c,neg(d.exp[c]||0))).join(''),neg(sumOf(x=>x.exp[c]||0))); });
|
|
1286
1289
|
|
|
1287
1290
|
if(M.cats.fin.length){
|
|
1288
1291
|
html+=row('sec','Финансовая деятельность',M.days.map(d=>cell(d.finSum>=0?num(d.finSum):neg(d.finSum),d.finSum>=0?'pos':'neg')).join(''),
|
|
1289
1292
|
(()=>{const t=sumOf(x=>x.finSum);return `<b class="${t>=0?'pos':'neg'}">${t>=0?num(t):neg(t)}</b>`;})());
|
|
1290
|
-
M.cats.fin.forEach(c=>{ html+=row('val',esc(c),M.days.map(d=>planCell(d,'fi:'+c,(d.fin[c]||0)>=0?num(d.fin[c]||0):neg(d.fin[c]||0))).join(''),
|
|
1293
|
+
M.cats.fin.forEach(c=>{ html+=row('val',esc(cfLabel(c)),M.days.map(d=>planCell(d,'fi:'+c,(d.fin[c]||0)>=0?num(d.fin[c]||0):neg(d.fin[c]||0))).join(''),
|
|
1291
1294
|
(()=>{const t=sumOf(x=>x.fin[c]||0);return t>=0?num(t):neg(t);})()); });
|
|
1292
1295
|
}
|
|
1293
1296
|
if(M.cats.inv.length){
|
|
1294
1297
|
html+=row('sec','Инвестиционная деятельность',M.days.map(d=>cell(d.invSum>=0?num(d.invSum):neg(d.invSum),d.invSum>=0?'pos':'neg')).join(''),
|
|
1295
1298
|
(()=>{const t=sumOf(x=>x.invSum);return `<b class="${t>=0?'pos':'neg'}">${t>=0?num(t):neg(t)}</b>`;})());
|
|
1296
|
-
M.cats.inv.forEach(c=>{ html+=row('val',esc(c),M.days.map(d=>planCell(d,'in:'+c,(d.inv[c]||0)>=0?num(d.inv[c]||0):neg(d.inv[c]||0))).join(''),
|
|
1299
|
+
M.cats.inv.forEach(c=>{ html+=row('val',esc(cfLabel(c)),M.days.map(d=>planCell(d,'in:'+c,(d.inv[c]||0)>=0?num(d.inv[c]||0):neg(d.inv[c]||0))).join(''),
|
|
1297
1300
|
(()=>{const t=sumOf(x=>x.inv[c]||0);return t>=0?num(t):neg(t);})()); });
|
|
1298
1301
|
}
|
|
1299
1302
|
|
|
@@ -1334,11 +1337,11 @@ function renderCF(C){
|
|
|
1334
1337
|
html+=rowM('sub','Поступления',x=>x.inSum);
|
|
1335
1338
|
CHANNELS.forEach(([k,n])=>{ html+=rowM('val',n,x=>x.inc[k]||0); });
|
|
1336
1339
|
html+=rowM('sub','Расходы',x=>x.exSum,'neg');
|
|
1337
|
-
catsY.op.forEach(c=>{ html+=rowM('val',esc(c),x=>x.exp[c]||0,'neg'); });
|
|
1340
|
+
catsY.op.forEach(c=>{ html+=rowM('val',esc(cfLabel(c)),x=>x.exp[c]||0,'neg'); });
|
|
1338
1341
|
html+=rowM('sec','Финансовая деятельность',x=>x.finSum,'sgn');
|
|
1339
|
-
catsY.fin.forEach(c=>{ html+=rowM('val',esc(c),x=>x.fin[c]||0,'sgn'); });
|
|
1342
|
+
catsY.fin.forEach(c=>{ html+=rowM('val',esc(cfLabel(c)),x=>x.fin[c]||0,'sgn'); });
|
|
1340
1343
|
html+=rowM('sec','Инвестиционная деятельность',x=>x.invSum,'sgn');
|
|
1341
|
-
catsY.inv.forEach(c=>{ html+=rowM('val',esc(c),x=>x.inv[c]||0,'sgn'); });
|
|
1344
|
+
catsY.inv.forEach(c=>{ html+=rowM('val',esc(cfLabel(c)),x=>x.inv[c]||0,'sgn'); });
|
|
1342
1345
|
{
|
|
1343
1346
|
let cells='',first=null,last=null;
|
|
1344
1347
|
MM.forEach(M=>{ if(!M){ cells+='<td></td>'; return; }
|
|
@@ -2299,7 +2302,7 @@ function defaultImpRules(){ const R=(m,type,cat,extra)=>Object.assign({id:uid(),
|
|
|
2299
2302
|
function applyRules(text,dir){
|
|
2300
2303
|
const t=String(text||'').toLowerCase();
|
|
2301
2304
|
for(const r of (ST.impRules||[])){ if(r.m && t.includes(String(r.m).toLowerCase())) return r; }
|
|
2302
|
-
return {type: dir==='in'?'Доход':'Расход', cat:
|
|
2305
|
+
return {type: dir==='in'?'Доход':'Расход', cat:'Прочее'};
|
|
2303
2306
|
}
|
|
2304
2307
|
|
|
2305
2308
|
/* --- разбор конкретных банков --- */
|
|
@@ -2400,6 +2403,7 @@ async function impFiles(files){
|
|
|
2400
2403
|
function impBuild(){
|
|
2401
2404
|
if(!IMP) return;
|
|
2402
2405
|
const acqRe=/эквайринг/i, out=[], byDay={};
|
|
2406
|
+
const R2=v=>Math.round(v*100)/100;
|
|
2403
2407
|
IMP.merchUnknown=new Set();
|
|
2404
2408
|
IMP.rows.forEach(r=>{
|
|
2405
2409
|
const dup=!!ST.impSeen[impKey(r)];
|
|
@@ -2409,59 +2413,80 @@ function impBuild(){
|
|
|
2409
2413
|
const cm=r.purpose.match(/комисси[яи]\s*[:\-]?\s*([\d]+[.,]?[\d]*)/i);
|
|
2410
2414
|
const merch=mm?mm[1]:'';
|
|
2411
2415
|
const fee=cm?Number(String(cm[1]).replace(/\.$/,'').replace(',','.')):0;
|
|
2412
|
-
|
|
2413
|
-
if(merch&&!point) IMP.merchUnknown.add(merch);
|
|
2416
|
+
if(merch&&!ST.merchants[merch]) IMP.merchUnknown.add(merch);
|
|
2414
2417
|
if(IMP.fold){
|
|
2415
2418
|
const k=r.date+'|'+merch;
|
|
2416
2419
|
if(!byDay[k]) byDay[k]={date:r.date,merch,net:0,fee:0,n:0,dup:true,keys:[]};
|
|
2417
2420
|
byDay[k].net+=r.in; byDay[k].fee+=fee; byDay[k].n++;
|
|
2418
2421
|
byDay[k].keys.push(impKey(r)); if(!dup) byDay[k].dup=false;
|
|
2419
2422
|
} else {
|
|
2420
|
-
out.push({date:r.date,
|
|
2421
|
-
amount:Math.round((r.in+fee)*100)/100,purpose:r.purpose,dup,keys:[impKey(r)],acq:true,merch,fee});
|
|
2423
|
+
out.push(...acqPair({date:r.date,merch,net:r.in,fee,n:1,dup,keys:[impKey(r)]},R2));
|
|
2422
2424
|
}
|
|
2423
2425
|
return;
|
|
2424
2426
|
}
|
|
2425
2427
|
const dir=r.in>0?'in':'out';
|
|
2426
2428
|
const rule=applyRules((r.purpose||'')+' '+(r.cp||''),dir);
|
|
2427
2429
|
out.push({date:r.date,type:rule.type||(dir==='in'?'Доход':'Расход'),cat:rule.cat||'Прочее',
|
|
2428
|
-
ch:rule.ch||null,point:'Общий',amount:r.in>0?r.in:r.out,
|
|
2429
|
-
purpose:(r.cp?r.cp+' · ':'')+r.purpose,
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
const point=ST.merchants[g.merch]||'';
|
|
2433
|
-
out.push({date:g.date,type:'Доход',cat:'Выручка',ch:'bank',point:point||'Общий',
|
|
2434
|
-
amount:Math.round((g.net+g.fee)*100)/100,
|
|
2435
|
-
purpose:`Эквайринг, мерчант ${g.merch}, операций: ${g.n}`,
|
|
2436
|
-
dup:g.dup,keys:g.keys,acq:true,merch:g.merch,fee:Math.round(g.fee*100)/100});
|
|
2430
|
+
ch:rule.ch||null,point:rule.point||'Общий',acc2:null,amount:r.in>0?r.in:r.out,
|
|
2431
|
+
purpose:(r.cp?r.cp+' · ':'')+r.purpose,
|
|
2432
|
+
sig:(r.cp||r.purpose||'').slice(0,32).toLowerCase(),
|
|
2433
|
+
dup,keys:[impKey(r)]});
|
|
2437
2434
|
});
|
|
2435
|
+
Object.values(byDay).forEach(g=>{ out.push(...acqPair(g,R2)); });
|
|
2438
2436
|
out.sort((a,b)=>a.date<b.date?-1:a.date>b.date?1:(a.type<b.type?1:-1));
|
|
2439
2437
|
out.forEach(d=>{ if(d.dup) d.off=true; });
|
|
2440
2438
|
IMP.drafts=out;
|
|
2441
2439
|
}
|
|
2440
|
+
/* эквайринг = валовая выручка + отдельная строка комиссии */
|
|
2441
|
+
function acqPair(g,R2){
|
|
2442
|
+
const point=ST.merchants[g.merch]||'Общий';
|
|
2443
|
+
const base={date:g.date,dup:g.dup,keys:g.keys,merch:g.merch,acc2:null,sig:'эквайринг:'+g.merch};
|
|
2444
|
+
const rows=[Object.assign({},base,{type:'Доход',cat:'Выручка',ch:'bank',point,amount:R2(g.net+g.fee),
|
|
2445
|
+
purpose:`Эквайринг${g.merch?', мерчант '+g.merch:''} · операций ${g.n} · зачислено ${fmt(g.net)}, комиссия ${fmt(g.fee)}`})];
|
|
2446
|
+
if(g.fee>0) rows.push(Object.assign({},base,{type:'Расход',cat:'Эквайринг',ch:null,point,amount:R2(g.fee),
|
|
2447
|
+
purpose:`Комиссия эквайринга${g.merch?', мерчант '+g.merch:''}`}));
|
|
2448
|
+
return rows;
|
|
2449
|
+
}
|
|
2450
|
+
/* применить классификацию строки ко всем похожим */
|
|
2451
|
+
function impApplySimilar(i){
|
|
2452
|
+
const d=IMP&&IMP.drafts[i]; if(!d) return;
|
|
2453
|
+
let n=0;
|
|
2454
|
+
IMP.drafts.forEach((x,j)=>{ if(j===i||x.sig!==d.sig) return;
|
|
2455
|
+
x.type=d.type; x.cat=d.cat; x.ch=d.ch; x.point=d.point; x.acc2=d.acc2; x.off=d.off; n++; });
|
|
2456
|
+
render(); toast(n?('Применено к '+n+' строкам'):'Похожих строк нет');
|
|
2457
|
+
}
|
|
2458
|
+
/* сохранить классификацию строки как правило */
|
|
2459
|
+
function impSaveRule(i){
|
|
2460
|
+
const d=IMP&&IMP.drafts[i]; if(!d) return;
|
|
2461
|
+
openModal('Новое правило из строки',
|
|
2462
|
+
`<label class="fld">Если встречается<input id="ir2-m" value="${esc((d.sig||'').trim())}"></label>
|
|
2463
|
+
<p class="hint">Строка: ${esc(d.purpose.slice(0,120))}<br>Будет проставляться: <b>${d.type} · ${esc(d.cat)}</b>${d.ch?' · '+CH_NAME[d.ch]:''}${d.point&&d.point!=='Общий'?' · '+esc(d.point):''}</p>`,
|
|
2464
|
+
()=>{ const m=val('ir2-m').trim(); if(!m)return;
|
|
2465
|
+
ST.impRules.unshift({id:uid(),m,type:d.type,cat:d.cat,ch:d.ch||null,point:d.point!=='Общий'?d.point:null});
|
|
2466
|
+
save(); impBuild(); render(); toast('Правило сохранено'); });
|
|
2467
|
+
}
|
|
2442
2468
|
function impCommit(){
|
|
2443
2469
|
if(!IMP) return;
|
|
2444
2470
|
const list=IMP.drafts.filter(d=>!d.off);
|
|
2445
2471
|
if(!list.length){ toast('Нечего добавлять'); return; }
|
|
2446
2472
|
const acc=IMP.account||impAccName(IMP.acc);
|
|
2473
|
+
const bad=list.filter(d=>d.type==='Перевод'&&(!d.acc2||d.acc2===acc));
|
|
2474
|
+
if(bad.length){ alert('У '+bad.length+' переводов не выбран счёт-получатель (или он совпадает со счётом выписки).'); return; }
|
|
2447
2475
|
const locked=[...new Set(list.map(d=>MONTHS[new Date(d.date).getMonth()]))].filter(isLocked);
|
|
2448
2476
|
if(locked.length){ alert('Месяцы закрыты: '+locked.join(', ')+'. Откройте их в Конструкторе.'); return; }
|
|
2449
2477
|
bkCreate('Перед импортом выписки '+IMP.bank,true,true);
|
|
2450
|
-
let n=0
|
|
2478
|
+
let n=0;
|
|
2451
2479
|
list.forEach(d=>{
|
|
2452
2480
|
const month=MONTHS[new Date(d.date).getMonth()];
|
|
2453
|
-
ST.operations.push({id:uid(),date:d.date,month,type:d.type,
|
|
2454
|
-
|
|
2481
|
+
ST.operations.push({id:uid(),date:d.date,month,type:d.type,
|
|
2482
|
+
category:d.type==='Перевод'?'Перевод':d.cat,point:d.point||'Общий',
|
|
2483
|
+
account:acc,account2:d.type==='Перевод'?d.acc2:null,
|
|
2484
|
+
amount:Math.abs(d.amount),cash:true,ch:d.type==='Доход'?(d.ch||null):null,employeeId:null,
|
|
2455
2485
|
note:d.purpose.slice(0,180),comment:d.purpose.slice(0,180)}); n++;
|
|
2456
|
-
if(d.acq&&d.fee>0){
|
|
2457
|
-
ST.operations.push({id:uid(),date:d.date,month,type:'Расход',category:'Эквайринг',point:d.point||'Общий',
|
|
2458
|
-
account:acc,amount:Math.abs(d.fee),cash:true,ch:null,employeeId:null,
|
|
2459
|
-
note:'Комиссия эквайринга'+(d.merch?', мерчант '+d.merch:''),comment:'Комиссия эквайринга'}); n++; fees+=d.fee;
|
|
2460
|
-
}
|
|
2461
2486
|
(d.keys||[]).forEach(k=>ST.impSeen[k]=1);
|
|
2462
2487
|
});
|
|
2463
2488
|
save(); IMP=null; render();
|
|
2464
|
-
toast(
|
|
2489
|
+
toast('Добавлено операций: '+n);
|
|
2465
2490
|
}
|
|
2466
2491
|
function impCancel(){ IMP=null; render(); }
|
|
2467
2492
|
function impRuleAdd(){ openModal('Правило импорта',
|
|
@@ -2495,15 +2520,16 @@ function renderImport(C){
|
|
|
2495
2520
|
if(IMP){
|
|
2496
2521
|
const total=IMP.drafts.length, dups=IMP.drafts.filter(d=>d.dup).length, on=IMP.drafts.filter(d=>!d.off).length;
|
|
2497
2522
|
const dates=IMP.drafts.map(d=>d.date).sort();
|
|
2498
|
-
const acqFee=IMP.drafts.filter(d
|
|
2523
|
+
const acqFee=IMP.drafts.filter(d=>!d.off&&d.cat==='Эквайринг').reduce((s,d)=>s+d.amount,0);
|
|
2499
2524
|
const inSum=IMP.drafts.filter(d=>!d.off&&d.type==='Доход').reduce((s,d)=>s+d.amount,0);
|
|
2500
|
-
const outSum=IMP.drafts.filter(d=>!d.off&&d.type==='Расход').reduce((s,d)=>s+d.amount,0)
|
|
2525
|
+
const outSum=IMP.drafts.filter(d=>!d.off&&d.type==='Расход').reduce((s,d)=>s+d.amount,0);
|
|
2526
|
+
const trSum=IMP.drafts.filter(d=>!d.off&&d.type==='Перевод').reduce((s,d)=>s+d.amount,0);
|
|
2501
2527
|
html+=`<div class="cards">
|
|
2502
2528
|
<div class="card"><div class="k">Выписка</div><div class="v" style="font-size:19px">${IMP.bank}</div><div class="d muted">счёт ${esc(IMP.acc||'—')}</div></div>
|
|
2503
2529
|
<div class="card"><div class="k">Период</div><div class="v" style="font-size:19px">${dates[0]||'—'} → ${dates[dates.length-1]||'—'}</div><div class="d muted">${IMP.rows.length} строк выписки</div></div>
|
|
2504
2530
|
<div class="card"><div class="k">К добавлению</div><div class="v">${on}</div><div class="d muted">${dups?dups+' уже импортировано ранее':'дублей нет'}</div></div>
|
|
2505
2531
|
<div class="card"><div class="k">Приход / расход</div><div class="v" style="font-size:19px"><span class="pos">${fmt(inSum)}</span> / <span class="neg">${fmt(outSum)}</span></div>
|
|
2506
|
-
<div class="d muted">${acqFee?'в т.ч. эквайринг '+fmt(acqFee):'без эквайринга'}</div></div></div>`;
|
|
2532
|
+
<div class="d muted">${acqFee?'в т.ч. эквайринг '+fmt(acqFee):'без эквайринга'}${trSum?' · переводы '+fmt(trSum):''}</div></div></div>`;
|
|
2507
2533
|
|
|
2508
2534
|
html+=`<div class="panel"><div class="bd"><div class="filters">
|
|
2509
2535
|
<label class="f"><span>Зачислять на счёт</span><select onchange="impSetAcc(this.value)">${
|
|
@@ -2524,8 +2550,8 @@ function renderImport(C){
|
|
|
2524
2550
|
<span class="tag lock">${unknown.length} без привязки</span></div>
|
|
2525
2551
|
<div class="bd" style="padding:0"><div class="tbl-wrap"><table>
|
|
2526
2552
|
<thead><tr><th>Мерчант</th><th class="num">Операций</th><th class="num">Сумма</th><th style="width:280px">Точка</th></tr></thead><tbody>`;
|
|
2527
|
-
unknown.forEach(m2=>{ const ds=IMP.drafts.filter(d=>d.merch===m2);
|
|
2528
|
-
html+=`<tr><td class="small">№${m2}</td><td class="num">${ds.
|
|
2553
|
+
unknown.forEach(m2=>{ const ds=IMP.drafts.filter(d=>d.merch===m2&&d.type==='Доход');
|
|
2554
|
+
html+=`<tr><td class="small">№${m2}</td><td class="num">${ds.length}</td>
|
|
2529
2555
|
<td class="num">${fmt(ds.reduce((s,d)=>s+d.amount,0))}</td>
|
|
2530
2556
|
<td><select onchange="impSetMerch('${m2}',this.value)"><option value="">— выберите точку —</option>${
|
|
2531
2557
|
ST.points.map(p=>`<option>${esc(p)}</option>`).join('')}</select></td></tr>`; });
|
|
@@ -2536,20 +2562,32 @@ function renderImport(C){
|
|
|
2536
2562
|
html+=`<div class="panel"><div class="hd"><h3>Черновик операций</h3><div class="spacer"></div>
|
|
2537
2563
|
<span class="muted small">снимите галочку, чтобы не импортировать строку</span></div>
|
|
2538
2564
|
<div class="bd" style="padding:0"><div class="tbl-wrap" style="max-height:60vh"><table>
|
|
2539
|
-
<thead><tr><th style="width:34px"></th><th>Дата</th><th style="width:
|
|
2540
|
-
<th style="width:
|
|
2565
|
+
<thead><tr><th style="width:34px"></th><th style="width:96px">Дата</th><th style="width:118px">Тип</th><th style="width:180px">Статья</th>
|
|
2566
|
+
<th style="width:140px">Канал / на счёт</th><th style="width:190px">Точка</th><th class="num" style="width:120px">Сумма</th>
|
|
2567
|
+
<th style="width:78px"></th><th>Назначение</th></tr></thead><tbody>`;
|
|
2541
2568
|
IMP.drafts.forEach((d,i)=>{
|
|
2569
|
+
const isTr=d.type==='Перевод';
|
|
2542
2570
|
html+=`<tr style="${d.off?'opacity:.42':''}">
|
|
2543
2571
|
<td><input type="checkbox" ${d.off?'':'checked'} onchange="impToggle(${i})"></td>
|
|
2544
2572
|
<td class="small">${d.date}</td>
|
|
2545
|
-
<td><select onchange="impRowSet(${i},'type',this.value)"
|
|
2546
|
-
|
|
2547
|
-
<td>${
|
|
2548
|
-
|
|
2573
|
+
<td><select onchange="impRowSet(${i},'type',this.value)">${
|
|
2574
|
+
['Доход','Расход','Перевод'].map(t=>`<option ${d.type===t?'selected':''}>${t}</option>`).join('')}</select></td>
|
|
2575
|
+
<td>${isTr?'<span class="muted small">перевод между счетами</span>':
|
|
2576
|
+
`<input list="impcats" value="${esc(d.cat)}" onchange="impRowSet(${i},'cat',this.value)" style="width:100%">`}</td>
|
|
2577
|
+
<td>${isTr
|
|
2578
|
+
? `<select onchange="impRowSet(${i},'acc2',this.value)"><option value="">— на счёт —</option>${
|
|
2579
|
+
ST.accounts.filter(a=>a.name!==IMP.account).map(a=>`<option ${d.acc2===a.name?'selected':''}>${esc(a.name)}</option>`).join('')}</select>`
|
|
2580
|
+
: d.type==='Доход'
|
|
2581
|
+
? `<select onchange="impRowSet(${i},'ch',this.value)"><option value="">авто</option>${
|
|
2582
|
+
CHANNELS.map(([k,n])=>`<option value="${k}" ${d.ch===k?'selected':''}>${n}</option>`).join('')}</select>`
|
|
2583
|
+
: '<span class="muted small">—</span>'}</td>
|
|
2549
2584
|
<td><select onchange="impRowSet(${i},'point',this.value)"><option>Общий</option>${
|
|
2550
2585
|
ST.points.map(p=>`<option ${d.point===p?'selected':''}>${esc(p)}</option>`).join('')}</select></td>
|
|
2551
|
-
<td class="num ${d.type==='Доход'?'pos':'neg'}">${
|
|
2552
|
-
<td class="
|
|
2586
|
+
<td class="num ${isTr?'muted':d.type==='Доход'?'pos':'neg'}">${isTr?'⇄ ':''}${fmt(d.amount)}</td>
|
|
2587
|
+
<td><div class="ctrls" style="opacity:1;gap:3px;flex-wrap:nowrap;white-space:nowrap">
|
|
2588
|
+
<button class="icon" title="Применить эту классификацию ко всем похожим строкам" onclick="impApplySimilar(${i})">⤳</button>
|
|
2589
|
+
<button class="icon" title="Сохранить как правило" onclick="impSaveRule(${i})">+</button></div></td>
|
|
2590
|
+
<td class="muted small" title="${esc(d.purpose)}">${esc(d.purpose.slice(0,70))}${d.dup?' <span class="tag lock">уже импортировано</span>':''}</td></tr>`;
|
|
2553
2591
|
});
|
|
2554
2592
|
html+=`</tbody></table></div></div></div><datalist id="impcats">${cats.map(c=>`<option>${esc(c)}</option>`).join('')}</datalist>`;
|
|
2555
2593
|
}
|
|
@@ -2557,15 +2595,15 @@ function renderImport(C){
|
|
|
2557
2595
|
html+=`<div class="panel"><div class="hd"><h3>Правила автокатегоризации</h3><div class="spacer"></div>
|
|
2558
2596
|
<button class="primary mini" onclick="impRuleAdd()">+ Правило</button></div>
|
|
2559
2597
|
<div class="bd" style="padding:0"><div class="tbl-wrap"><table>
|
|
2560
|
-
<thead><tr><th>Если встречается</th><th>Тип</th><th>Статья</th><th>Канал</th><th style="width:130px"></th></tr></thead><tbody>`;
|
|
2598
|
+
<thead><tr><th>Если встречается</th><th>Тип</th><th>Статья</th><th>Канал</th><th>Точка</th><th style="width:130px"></th></tr></thead><tbody>`;
|
|
2561
2599
|
(ST.impRules||[]).forEach((r,i)=>{ html+=`<tr><td class="small">${esc(r.m)}</td><td><span class="tag">${r.type}</span></td>
|
|
2562
|
-
<td>${esc(r.cat)}</td><td class="muted small">${r.ch?CH_NAME[r.ch]:'—'}</td>
|
|
2600
|
+
<td>${esc(r.cat)}</td><td class="muted small">${r.ch?CH_NAME[r.ch]:'—'}</td><td class="muted small">${r.point?esc(r.point):'—'}</td>
|
|
2563
2601
|
<td><div class="ctrls" style="opacity:1;justify-content:flex-start;gap:4px">
|
|
2564
2602
|
<button class="icon" ${i===0?'disabled':''} onclick="impRuleMove('${r.id}',-1)">↑</button>
|
|
2565
2603
|
<button class="icon" ${i===(ST.impRules.length-1)?'disabled':''} onclick="impRuleMove('${r.id}',1)">↓</button>
|
|
2566
2604
|
<button class="icon" onclick="impRuleDel('${r.id}')">✕</button></div></td></tr>`; });
|
|
2567
2605
|
html+=`</tbody></table></div></div>
|
|
2568
|
-
<p class="hint" style="padding:0 15px 12px;margin:0"
|
|
2606
|
+
<p class="hint" style="padding:0 15px 12px;margin:0">Правила проверяются сверху вниз, срабатывает первое совпадение. Быстрее всего разбирать выписку так: настроили строку → <b>⤳</b> применяет её классификацию ко всем похожим, <b>+</b> сохраняет правило, чтобы в следующий раз проставилось само. Эквайринг Сбера приходит двумя строками: валовая выручка (зачисление + комиссия) и отдельная строка комиссии — обе можно править и исключать по отдельности.</p></div>`;
|
|
2569
2607
|
|
|
2570
2608
|
C.innerHTML=html;
|
|
2571
2609
|
}
|