/* Модуль «Бюджет / ПНЛ» — финансовая сводка объекта. Бюджет (смета×(1+НДС)) · Получено (из ОПЛАЧЕННЫХ актов КС за вычетом удержания) · Потрачено (оплаты поставщикам) · Прибыль. Виден только финансовым ролям. */ window.RD = window.RD || { core: {}, modules: {} }; window.RD.modules.Budget = function Budget() { var store = RD.core.useStore(); var C = RD.core, money = C.money0, h = React.createElement; var perms = store.perms; if (!perms.seeFinance) { return h('div', { className: 'rd-main-content', style: { display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--rd-text-muted)', fontSize: 14 } }, 'Раздел «Бюджет / ПНЛ» недоступен для роли «' + perms.role + '»'); } var sections = store.raw.sections || [], acts = store.raw.acts || [], payments = store.raw.payments || []; var vat = store.vatRate; // бюджет = смета с НДС (построчно) var grand = 0, grandVat = 0; sections.forEach(function (s) { (s.rows || []).forEach(function (r) { var a = r.qty * r.price; grand += a; grandVat += C.lineVat(a, vat); }); }); var budget = grand + grandVat; // получено = оплаченные КС за вычетом гарантийного удержания var received = 0, retentionHeld = 0; acts.forEach(function (a) { var ret = Math.round(a.amount * ((a.retentionPct != null ? a.retentionPct : 5) / 100)); if (a.paid) received += (a.amount - ret); retentionHeld += ret; }); // потрачено = оплаты со статусом paid/partial var spent = payments.filter(function (p) { return p.status === 'paid' || p.status === 'partial'; }).reduce(function (a, p) { return a + (p.amount || 0); }, 0); var contracted = payments.reduce(function (a, p) { return a + (p.amount || 0); }, 0); var profit = received - spent; var remainToReceive = budget - received; var kpis = [ { l: 'Бюджет с НДС', v: money(budget) + ' ₽', c: 'var(--rd-accent)', sub: 'смета ' + money(grand) + ' + НДС ' + money(grandVat) }, { l: 'Получено (опл. КС)', v: money(received) + ' ₽', c: 'var(--rd-good)', sub: Math.round(received / budget * 100) + '% бюджета · удержано ' + money(retentionHeld) }, { l: 'Потрачено', v: money(spent) + ' ₽', c: 'var(--rd-warn,#c2790a)', sub: 'законтрактовано ' + money(contracted) }, { l: 'Операц. прибыль', v: money(profit) + ' ₽', c: profit >= 0 ? 'var(--rd-good)' : 'var(--rd-bad)', sub: 'получено − потрачено' } ]; var pnl = [ ['Выручка (получено по КС)', received, 'var(--rd-good)'], ['Прямые затраты (оплаты)', -spent, 'var(--rd-bad)'], ['Гарантийное удержание (фонд)', -retentionHeld, 'var(--rd-text-muted)'], ['Операционная прибыль', profit, profit >= 0 ? 'var(--rd-good)' : 'var(--rd-bad)'], ['Остаток к получению', remainToReceive, 'var(--rd-text-2)'] ]; return h('div', { className: 'rd-main-content', style: { background: 'var(--rd-surface)', overflow: 'auto' } }, h('div', { style: { padding: 18 } }, h('div', { style: { fontSize: 12, fontWeight: 600, color: 'var(--rd-text-muted)', margin: '0 0 12px 2px' } }, 'БЮДЖЕТ ОБЪЕКТА · ' + (store.raw.object || {}).unit), h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 13, marginBottom: 20 } }, kpis.map(function (k, i) { return h('div', { key: i, style: { border: '1px solid var(--rd-border)', borderLeft: '3px solid ' + k.c, borderRadius: 12, padding: '15px 16px', background: 'var(--rd-surface)' } }, h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginBottom: 6 } }, k.l), h('div', { className: 'mono', style: { fontSize: 23, fontWeight: 700, color: k.c, letterSpacing: '-.5px' } }, k.v), h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginTop: 4 } }, k.sub)); })), h('div', { style: { fontSize: 12, fontWeight: 600, color: 'var(--rd-text-muted)', margin: '0 0 10px 2px' } }, 'ОТЧЁТ О ПРИБЫЛЯХ И УБЫТКАХ (ПНЛ)'), h('div', { style: { border: '1px solid var(--rd-border)', borderRadius: 12, overflow: 'hidden', maxWidth: 560 } }, pnl.map(function (r, i) { var isTotal = r[0].indexOf('прибыль') >= 0; return h('div', { key: i, style: { display: 'flex', alignItems: 'center', padding: '11px 15px', borderBottom: i < pnl.length - 1 ? '1px solid var(--rd-border)' : 'none', background: isTotal ? 'var(--rd-surface-2)' : 'transparent' } }, h('span', { style: { flex: 1, fontSize: 12.5, fontWeight: isTotal ? 700 : 500, color: 'var(--rd-text)' } }, r[0]), h('span', { className: 'mono', style: { fontSize: isTotal ? 15 : 13, fontWeight: isTotal ? 700 : 600, color: r[2] } }, (r[1] < 0 ? '−' : '') + money(Math.abs(r[1])) + ' ₽')); }) ), h('div', { style: { fontSize: 11, color: 'var(--rd-text-muted)', marginTop: 12, maxWidth: 560, lineHeight: 1.5 } }, 'Получено считается из реально оплаченных актов КС (за вычетом гарантийного удержания), а не из коэффициента. Удержание возвращается по завершении гарантийного периода.') ) ); };