Цель:
Научиться анализировать взаимосвязь затрат, объема продаж и прибыли, а также оценивать операционный риск компании через DOL.
Задачи:
Изучить логику CVP-анализа.
Рассчитать точку безубыточности и маржу безопасности.
Понять, как структура постоянных и переменных затрат влияет на прибыль.
Проанализировать DOL как показатель чувствительности прибыли к изменению выручки.
Инструменты:
Excel / Google Sheets, Google Colab, Python, GPT/Gemini, CVP-модель, график безубыточности, сценарный анализ.
Результат:
Участники смогут построить CVP-модель, определить запас финансовой прочности, рассчитать DOL и объяснить, как изменение продаж влияет на прибыль и риск бизнеса.
Ключевая логика урока:
CVP → точка безубыточности → маржа безопасности → DOL → управленческое решение.
Ты — senior fullstack developer и expert Google Apps Script engineer.
Создай production-ready веб-приложение на Google Apps Script для финансового анализа компании.
Формат:
- 2 файла:
1) Code.gs
2) Index.html
Технологии:
- HTML
- CSS
- Vanilla JavaScript
- Chart.js
- xlsx-js-style
- Google Apps Script HTML Service
-----------------------------------
НАЗВАНИЕ ПРИЛОЖЕНИЯ
-----------------------------------
CVP-анализ: безубыточность, маржа безопасности и левериджи
-----------------------------------
ЦЕЛЬ ПРИЛОЖЕНИЯ
-----------------------------------
Приложение анализирует:
1. Безубыточность компании
2. Маржу безопасности
3. Операционный леверидж (DOL)
4. Финансовый леверидж (DFL)
5. Совокупный леверидж (DTL)
6. Прогноз EBIT
7. Прогноз EPS
Модель должна сравнивать:
- AS-IS
- TO-BE
-----------------------------------
ДИЗАЙН
-----------------------------------
Стиль:
- dark graphite
- neon fintech
- футуристический premium UI
- минимализм
- компактные блоки
- светло-серый текст
- темный фон
- тонкие линии
- glassmorphism элементы
Цвета:
- фон: очень темный графит
- линии: синий и красный
- текст: светло-серый
- акценты: cyan neon
Интерфейс должен помещаться примерно в 2 экрана ноутбука без огромных отступов.
-----------------------------------
СТРУКТУРА ИНТЕРФЕЙСА
-----------------------------------
1. Header
Название приложения
Подзаголовок:
AS-IS слева · TO-BE справа
-----------------------------------
2. Блок переменных
-----------------------------------
Две колонки:
- AS-IS
- TO-BE
Переменные:
- Продажи S
- Цена P
- AVC
- TFC
- PD
- IP
- Налог %
- Акции N
- Прирост продаж %
Поля должны быть:
- предзаполнены значениями
- компактными
- аккуратно выровненными
-----------------------------------
3. Кнопки
-----------------------------------
Под переменными разместить:
- Рассчитать
- Сброс
- Загрузить .xlsx
Сброс:
- очищает результаты
- очищает графики
- НЕ перезагружает страницу
- НЕ очищает переменные
-----------------------------------
4. Блок безубыточности
-----------------------------------
Структура:
СЛЕВА:
- диаграмма CVP / Break-even
СПРАВА:
- таблица
- аналитический вывод
Диаграмма:
- на темном фоне
- линии тонкие и элегантные
- синие и красные линии
- подписи прямо на линиях
- динамически меняется после расчета
На графике должны быть:
- TR
- TC
- FC
- QBE
- SM
Обязательно:
- подписи значений
- подписи линий
- оси
- сетка
-----------------------------------
5. Блок левериджей
-----------------------------------
Отдельная карточка.
Структура:
- сверху таблица
- снизу гистограмма
- внизу аналитический вывод
Гистограмма:
- одинаковой высоты с прогнозным блоком
- компактная
- на темном фоне
Показатели:
- DOL
- DFL
- DTL
-----------------------------------
6. Блок прогноза
-----------------------------------
Отдельная карточка справа от блока левериджей.
Структура:
- сверху таблица
- снизу гистограмма
- внизу аналитический вывод
Показатели:
- EBIT forecast
- EPS forecast
- EBIT growth
- EPS growth
Гистограмма:
- одинаковой высоты с блоком левериджей
- выровнена по одной линии
-----------------------------------
АНАЛИТИЧЕСКИЕ ВЫВОДЫ
-----------------------------------
Выводы должны генерироваться автоматически.
Отдельно:
1. Безубыточность
2. Левериджи
3. Прогноз
Выводы должны учитывать:
- рост
- снижение
- риски
- устойчивость
- чувствительность прибыли
- изменение маржи безопасности
- изменение QBE
- изменение DTL
Использовать:
- управленческий стиль
- CFO style
- короткие аналитические абзацы
-----------------------------------
РАСЧЕТЫ
-----------------------------------
Формулы:
TR = S × P
VC = S × AVC
CM = TR − VC
TC = VC + FC
EBIT = TR − TC
EBT = EBIT − IP
EAT = EBT × (1 − TAX)
EPS = (EAT − PD) / N
QBE = FC / (P − AVC)
SBE = QBE × P
SM cash = TR − SBE
SM % = SM cash / TR
ROS = EBIT / TR
DOL = CM / EBIT
DFL = EBIT / (EBIT − IP)
DTL = DOL × DFL
EBIT growth = DOL × Sales growth
EPS growth = DTL × Sales growth
Forecast EBIT = EBIT × (1 + EBIT growth)
Forecast EPS = EPS × (1 + EPS growth)
-----------------------------------
EXCEL
-----------------------------------
Кнопка:
Загрузить .xlsx
Excel должен быть:
- premium style
- Big4 style
- dark headers
- жирные заголовки
- перенос текста
- выравнивание
- границы
- все на одном листе
Excel должен содержать:
1. Исходные данные
2. Безубыточность
3. Маржу безопасности
4. Левериджи
5. Прогноз EBIT/EPS
-----------------------------------
-----------------------------------
Ниже разместить:
- поле email
- кнопку Отправить
При отправке:
- Excel файл отправляется через MailApp
- attachment .xlsx
-----------------------------------
ТРЕБОВАНИЯ К КОДУ
-----------------------------------
- Production-ready
- Чистая архитектура
- Без библиотек кроме Chart.js и xlsx-js-style
- Полный готовый код
- Без сокращений
- Без псевдокода
- Только готовое приложение
Сначала выдай:
1. Code.gs
2. Полный Index.html
function doGet() {
return HtmlService
.createHtmlOutputFromFile('Index')
.setTitle('CVP-анализ')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function sendXlsxToEmail(payload) {
if (!payload || !payload.email || !payload.base64) {
throw new Error('Нет email или файла.');
}
const bytes = Utilities.base64Decode(payload.base64);
const blob = Utilities.newBlob(
bytes,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
payload.filename || 'cvp_leverage_dashboard.xlsx'
);
MailApp.sendEmail({
to: payload.email,
subject: 'CVP-анализ: отчет по безубыточности и левериджам',
body: 'Во вложении Excel-отчет по CVP-анализу, безубыточности, марже безопасности, левериджам и прогнозу EBIT/EPS.',
attachments: [blob]
});
return 'Файл отправлен на почту: ' + payload.email;
}
<!DOCTYPE html>
<html lang="ru">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CVP-анализ</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx-js-style/dist/xlsx.bundle.js"></script>
<style>
:root {
--bg: #070b10;
--card: #101820;
--panel: #0b121a;
--line: #24d8ff;
--blue: #2f80ed;
--red: #ff4d6d;
--green: #16e07f;
--violet: #a855f7;
--text: #d8e0e8;
--muted: #9aa8b6;
--border: #26384a;
--yellow: #ffd166;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 12px;
}
.app {
max-width: 1540px;
margin: 0 auto;
padding: 14px;
}
.hero {
padding: 14px 18px;
border: 1px solid rgba(36,216,255,.35);
border-radius: 16px;
background: linear-gradient(135deg,#0d141c,#152232);
margin-bottom: 12px;
}
h1 {
margin: 0;
font-size: 24px;
text-transform: uppercase;
letter-spacing: .5px;
color: #eef3f8;
}
.subtitle {
color: var(--muted);
margin-top: 4px;
font-size: 12px;
}
.section {
background: var(--card);
border: 1px solid rgba(36,216,255,.22);
border-radius: 16px;
padding: 12px;
margin-bottom: 12px;
}
h2 {
margin: 0 0 10px;
color: var(--line);
text-transform: uppercase;
font-size: 15px;
}
h3 {
margin: 0 0 8px;
font-size: 13px;
color: #eef3f8;
}
.vars {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.side-title {
font-size: 13px;
font-weight: 900;
text-align: center;
color: #eef3f8;
background: #1d2b3a;
border-radius: 10px;
padding: 7px;
margin-bottom: 8px;
}
.var-grid {
display: grid;
grid-template-columns: 150px 1fr;
gap: 6px;
align-items: center;
}
.metric {
color: var(--muted);
font-weight: 700;
}
input {
width: 100%;
background: #071018;
color: #eef3f8;
border: 1px solid #31475c;
border-radius: 8px;
padding: 7px 8px;
font-size: 12px;
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
button {
border: 0;
border-radius: 9px;
padding: 8px 14px;
font-size: 11px;
font-weight: 900;
text-transform: uppercase;
cursor: pointer;
}
.btn-main { background: var(--line); color: #031018; }
.btn-secondary { background: #263545; color: #eef3f8; }
.btn-purple { background: var(--violet); color: #fff; }
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 10px;
}
.cvp-layout {
display: grid;
grid-template-columns: 1.15fr .85fr;
gap: 12px;
align-items: stretch;
}
#cvpCanvas {
width: 100%;
height: 430px;
background: #080d13;
border-radius: 12px;
}
.two-blocks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-items: stretch;
}
.analysis-card {
display: grid;
grid-template-rows: auto 118px 230px auto;
gap: 8px;
min-height: 440px;
}
.table-zone {
height: 118px;
overflow: auto;
}
.chart-zone {
height: 230px;
}
.chart-zone canvas {
width: 100% !important;
height: 230px !important;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
}
th {
background: #1f2e3d;
color: var(--line);
padding: 6px;
border: 1px solid #33485e;
}
td {
border: 1px solid #26384a;
padding: 5px 6px;
color: var(--text);
}
.num { text-align: right; white-space: nowrap; }
.pos { color: var(--green); font-weight: 900; }
.neg { color: var(--red); font-weight: 900; }
.summary {
margin-top: 8px;
padding: 8px;
border-left: 3px solid var(--line);
background: #0a1118;
line-height: 1.42;
font-size: 11px;
color: var(--text);
}
.summary b { color: var(--green); }
.risk { color: var(--red); font-weight: 900; }
.footer {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
}
.status {
color: var(--yellow);
font-weight: 800;
font-size: 11px;
}
@media(max-width:1100px) {
.vars, .cvp-layout, .two-blocks, .footer {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app">
<header class="hero">
<h1>CVP-анализ: безубыточность, маржа безопасности и левериджи</h1>
<div class="subtitle">AS-IS слева · TO-BE справа · прогноз EBIT и EPS на основе DOL / DFL / DTL</div>
</header>
<section class="section">
<h2>Переменные модели</h2>
<div class="vars">
<div>
<div class="side-title">AS-IS</div>
<div class="var-grid">
<div class="metric">Продажи S</div><input id="s1" value="8000">
<div class="metric">Цена P</div><input id="p1" value="23000">
<div class="metric">AVC</div><input id="avc1" value="9000">
<div class="metric">TFC</div><input id="fc1" value="80000000">
<div class="metric">PD</div><input id="pd1" value="3000000">
<div class="metric">IP</div><input id="ip1" value="2000000">
<div class="metric">Налог, %</div><input id="tax1" value="20">
<div class="metric">Акции N</div><input id="n1" value="1000000">
<div class="metric">Прирост продаж, %</div><input id="growth1" value="5">
</div>
</div>
<div>
<div class="side-title">TO-BE</div>
<div class="var-grid">
<div class="metric">Продажи S</div><input id="s2" value="8000">
<div class="metric">Цена P</div><input id="p2" value="23000">
<div class="metric">AVC</div><input id="avc2" value="7200">
<div class="metric">TFC</div><input id="fc2" value="88000000">
<div class="metric">PD</div><input id="pd2" value="3000000">
<div class="metric">IP</div><input id="ip2" value="2100000">
<div class="metric">Налог, %</div><input id="tax2" value="20">
<div class="metric">Акции N</div><input id="n2" value="1000000">
<div class="metric">Прирост продаж, %</div><input id="growth2" value="5">
</div>
</div>
</div>
<div class="actions">
<button class="btn-main" onclick="calculateAll()">Рассчитать</button>
<button class="btn-secondary" onclick="clearResults()">Сброс</button>
<button class="btn-purple" onclick="downloadExcel()">Загрузить .xlsx</button>
</div>
</section>
<section class="section">
<h2>Оценка безубыточности и маржи безопасности</h2>
<div class="cvp-layout">
<div class="panel">
<canvas id="cvpCanvas" width="900" height="430"></canvas>
</div>
<div class="panel">
<h3>Расчет AS-IS / TO-BE</h3>
<div id="cvpTable"></div>
<div class="summary" id="cvpSummary">Нажмите «Рассчитать».</div>
</div>
</div>
</section>
<section class="section">
<div class="two-blocks">
<div class="panel analysis-card">
<h2>Оценка силы левериджа</h2>
<div class="table-zone" id="levTable"></div>
<div class="chart-zone">
<canvas id="leverageChart"></canvas>
</div>
<div class="summary" id="levSummary">Нажмите «Рассчитать».</div>
</div>
<div class="panel analysis-card">
<h2>Прогноз EBIT и EPS</h2>
<div class="table-zone" id="forecastTable"></div>
<div class="chart-zone">
<canvas id="forecastChart"></canvas>
</div>
<div class="summary" id="forecastSummary">Нажмите «Рассчитать».</div>
</div>
</div>
</section>
<section class="section footer">
<button class="btn-purple" onclick="downloadExcel()">Загрузить .xlsx</button>
<input id="email" type="email" placeholder="Введите email для отправки отчета">
<button class="btn-main" onclick="sendExcel()">Отправить</button>
<div class="status" id="status"></div>
</section>
</div>
<script>
let leverageChart = null;
let forecastChart = null;
let lastAsIs = null;
let lastToBe = null;
function n(id) {
return Number(String(document.getElementById(id).value).replace(/\s/g,'').replace(',','.')) || 0;
}
function money(v) {
return '$ ' + Math.round(v).toLocaleString('ru-RU');
}
function units(v) {
return Math.round(v).toLocaleString('ru-RU') + ' шт.';
}
function pct(v) {
return (v * 100).toFixed(1).replace('.',',') + '%';
}
function deltaClass(v) {
return v >= 0 ? 'pos' : 'neg';
}
function calc(side) {
const S = n(side === 1 ? 's1':'s2');
const P = n(side === 1 ? 'p1':'p2');
const AVC = n(side === 1 ? 'avc1':'avc2');
const FC = n(side === 1 ? 'fc1':'fc2');
const PD = n(side === 1 ? 'pd1':'pd2');
const IP = n(side === 1 ? 'ip1':'ip2');
const TAX = n(side === 1 ? 'tax1':'tax2') / 100;
const N = n(side === 1 ? 'n1':'n2');
const G = n(side === 1 ? 'growth1':'growth2') / 100;
const TR = S * P;
const VC = S * AVC;
const CM = TR - VC;
const TC = FC + VC;
const EBIT = TR - TC;
const EBT = EBIT - IP;
const EAT = EBT * (1 - TAX);
const EPS = (EAT - PD) / N;
const QBE = FC / (P - AVC);
const SBE = QBE * P;
const SMcash = TR - SBE;
const SM = SMcash / TR;
const ROS = EBIT / TR;
const DOL = CM / EBIT;
const DFL = EBIT / (EBIT - IP);
const DTL = DOL * DFL;
const EBITgrowth = DOL * G;
const EPSgrowth = DTL * G;
const EBITforecast = EBIT * (1 + EBITgrowth);
const EPSforecast = EPS * (1 + EPSgrowth);
return {S,P,AVC,FC,PD,IP,TAX,N,G,TR,VC,CM,TC,EBIT,EBT,EAT,EPS,QBE,SBE,SMcash,SM,ROS,DOL,DFL,DTL,EBITgrowth,EPSgrowth,EBITforecast,EPSforecast};
}
function calculateAll() {
const a = calc(1);
const b = calc(2);
lastAsIs = a;
lastToBe = b;
renderCVP(a,b);
renderLeverage(a,b);
renderForecast(a,b);
}
function clearResults() {
lastAsIs = null;
lastToBe = null;
document.getElementById('cvpTable').innerHTML = '';
document.getElementById('levTable').innerHTML = '';
document.getElementById('forecastTable').innerHTML = '';
document.getElementById('cvpSummary').innerHTML = 'Нажмите «Рассчитать».';
document.getElementById('levSummary').innerHTML = 'Нажмите «Рассчитать».';
document.getElementById('forecastSummary').innerHTML = 'Нажмите «Рассчитать».';
const c = document.getElementById('cvpCanvas');
c.getContext('2d').clearRect(0,0,c.width,c.height);
if (leverageChart) {
leverageChart.destroy();
leverageChart = null;
}
if (forecastChart) {
forecastChart.destroy();
forecastChart = null;
}
document.getElementById('status').textContent = '';
}
function row(name,a,b,isMoney,mode) {
const d = b - a;
function f(v) {
if (isMoney) return money(v);
if (mode === '%') return pct(v);
if (mode === 'шт.') return units(v);
return v.toFixed(2);
}
return `
<tr>
<td>${name}</td>
<td class="num">${f(a)}</td>
<td class="num">${f(b)}</td>
<td class="num ${deltaClass(d)}">${mode === '%' ? pct(d) : mode === 'шт.' ? units(d) : isMoney ? money(d) : d.toFixed(2)}</td>
</tr>
`;
}
function renderCVP(a,b) {
document.getElementById('cvpTable').innerHTML = `
<table>
<thead>
<tr>
<th>Показатель</th>
<th>AS-IS</th>
<th>TO-BE</th>
<th>∆</th>
</tr>
</thead>
<tbody>
${row('TR', a.TR, b.TR, true)}
${row('VC', a.VC, b.VC, true)}
${row('FC', a.FC, b.FC, true)}
${row('TC', a.TC, b.TC, true)}
${row('EBIT', a.EBIT, b.EBIT, true)}
${row('QBE', a.QBE, b.QBE, false, 'шт.')}
${row('SM %', a.SM, b.SM, false, '%')}
${row('ROS', a.ROS, b.ROS, false, '%')}
</tbody>
</table>
`;
let t = '';
t += b.QBE < a.QBE
? `QBE снизилась с <b>${units(a.QBE)}</b> до <b>${units(b.QBE)}</b>. Это улучшает устойчивость бизнеса.<br>`
: `QBE выросла с <b>${units(a.QBE)}</b> до <b>${units(b.QBE)}</b>. Это повышает риск безубыточности.<br>`;
t += b.SM > a.SM
? `Маржа безопасности выросла с <b>${pct(a.SM)}</b> до <b>${pct(b.SM)}</b>. Запас прочности продаж увеличился.<br>`
: `Маржа безопасности снизилась с <b>${pct(a.SM)}</b> до <b>${pct(b.SM)}</b>. Запас прочности продаж ухудшился.<br>`;
t += b.EBIT > a.EBIT
? `EBIT вырос: операционная эффективность улучшилась.<br>`
: `EBIT снизился: операционный результат ухудшился.<br>`;
t += `<span class="risk">Риск:</span> рост FC, высокая QBE и снижение SM могут усилить чувствительность прибыли к падению продаж.`;
document.getElementById('cvpSummary').innerHTML = t;
drawCVPChart(a,b);
}
function drawCVPChart(a,b) {
const canvas = document.getElementById('cvpCanvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0,0,canvas.width,canvas.height);
const W = canvas.width;
const H = canvas.height;
const L = 70;
const R = 34;
const T = 24;
const B = 46;
const maxX = Math.max(a.S,b.S,a.QBE,b.QBE) * 1.25;
const maxY = Math.max(a.TR,b.TR,a.TC,b.TC,a.FC,b.FC) * 1.18;
const X = q => L + q / maxX * (W - L - R);
const Y = v => H - B - v / maxY * (H - T - B);
ctx.fillStyle = '#080d13';
ctx.fillRect(0,0,W,H);
ctx.strokeStyle = '#203040';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const yy = T + i * (H - T - B) / 5;
ctx.beginPath();
ctx.moveTo(L,yy);
ctx.lineTo(W-R,yy);
ctx.stroke();
}
ctx.strokeStyle = '#d8e0e8';
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.moveTo(L,T);
ctx.lineTo(L,H-B);
ctx.lineTo(W-R,H-B);
ctx.stroke();
drawScenario(a,'AS-IS', '#2f80ed', -8);
drawScenario(b,'TO-BE', '#ff4d6d', 12);
label('$',24,42,'#d8e0e8',13);
label('шт.',W-50,H-18,'#d8e0e8',13);
function line(q1,v1,q2,v2,color,width,dash) {
ctx.save();
ctx.beginPath();
ctx.setLineDash(dash || []);
ctx.moveTo(X(q1),Y(v1));
ctx.lineTo(X(q2),Y(v2));
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
ctx.restore();
}
function label(text,x,y,color,size=10) {
ctx.fillStyle = color;
ctx.font = `700 ${size}px Arial`;
ctx.fillText(text,x,y);
}
function dot(q,v,color) {
ctx.beginPath();
ctx.arc(X(q),Y(v),4,0,Math.PI*2);
ctx.fillStyle = color;
ctx.fill();
}
function drawScenario(d,title,color,offset) {
line(0,0,maxX,d.P*maxX,color,1.9);
line(0,d.FC,maxX,d.FC+d.AVC*maxX,color,1.5,[6,4]);
line(0,d.FC,maxX,d.FC,color,1.3,[3,4]);
line(d.QBE,0,d.QBE,d.SBE,color,1,[4,4]);
line(0,d.SBE,d.QBE,d.SBE,color,1,[4,4]);
line(d.S,0,d.S,d.TR,color,1,[3,4]);
dot(d.QBE,d.SBE,color);
dot(d.S,d.TR,color);
label(`${title} TR ${money(d.TR)}`, X(d.S)-118, Y(d.TR)-8+offset, color);
label(`${title} TC ${money(d.TC)}`, X(d.S)-118, Y(d.TC)+15+offset, color);
label(`QBE ${units(d.QBE)}`, X(d.QBE)+10, Y(d.SBE)+12+offset, color);
label(`SM ${pct(d.SM)}`, X(d.S)+10, Y(d.TR)+15+offset, color);
label(`FC ${money(d.FC)}`, X(maxX*.72), Y(d.FC)-5+offset, color);
}
}
function renderLeverage(a,b) {
document.getElementById('levTable').innerHTML = `
<table>
<thead>
<tr>
<th>Показатель</th>
<th>AS-IS</th>
<th>TO-BE</th>
<th>Смысл</th>
</tr>
</thead>
<tbody>
<tr><td>DOL</td><td class="num">${a.DOL.toFixed(2)}</td><td class="num">${b.DOL.toFixed(2)}</td><td>Операционный риск</td></tr>
<tr><td>DFL</td><td class="num">${a.DFL.toFixed(2)}</td><td class="num">${b.DFL.toFixed(2)}</td><td>Финансовый риск</td></tr>
<tr><td>DTL</td><td class="num">${a.DTL.toFixed(2)}</td><td class="num">${b.DTL.toFixed(2)}</td><td>Совокупный риск</td></tr>
</tbody>
</table>
`;
let t = '';
t += b.DOL < a.DOL ? `DOL снизился: чувствительность EBIT к продажам уменьшилась.<br>` : `DOL вырос: операционный риск усилился.<br>`;
t += b.DFL < a.DFL ? `DFL снизился: финансовый риск уменьшился.<br>` : `DFL вырос: финансовый риск усилился.<br>`;
t += b.DTL < a.DTL ? `DTL снизился: совокупный риск стал ниже.<br>` : `DTL вырос: совокупный риск стал выше.<br>`;
t += `<span class="risk">Контроль:</span> DOL и DTL нужно отслеживать при изменении объема продаж.`;
document.getElementById('levSummary').innerHTML = t;
if (leverageChart) leverageChart.destroy();
leverageChart = new Chart(document.getElementById('leverageChart'), {
type: 'bar',
data: {
labels: ['DOL','DFL','DTL'],
datasets: [
{ label: 'AS-IS', data: [a.DOL,a.DFL,a.DTL], backgroundColor: '#2f80ed' },
{ label: 'TO-BE', data: [b.DOL,b.DFL,b.DTL], backgroundColor: '#ff4d6d' }
]
},
options: darkChartOptions()
});
}
function renderForecast(a,b) {
document.getElementById('forecastTable').innerHTML = `
<table>
<thead>
<tr>
<th>Показатель</th>
<th>AS-IS</th>
<th>TO-BE</th>
</tr>
</thead>
<tbody>
<tr><td>Прирост продаж</td><td class="num">${pct(a.G)}</td><td class="num">${pct(b.G)}</td></tr>
<tr><td>Прирост EBIT</td><td class="num">${pct(a.EBITgrowth)}</td><td class="num">${pct(b.EBITgrowth)}</td></tr>
<tr><td>Прогноз EBIT</td><td class="num">${money(a.EBITforecast)}</td><td class="num">${money(b.EBITforecast)}</td></tr>
<tr><td>Прирост EPS</td><td class="num">${pct(a.EPSgrowth)}</td><td class="num">${pct(b.EPSgrowth)}</td></tr>
<tr><td>EPS</td><td class="num">${a.EPS.toFixed(2)}</td><td class="num">${b.EPS.toFixed(2)}</td></tr>
<tr><td>Прогноз EPS</td><td class="num">${a.EPSforecast.toFixed(2)}</td><td class="num">${b.EPSforecast.toFixed(2)}</td></tr>
</tbody>
</table>
`;
let t = '';
t += `При росте продаж на <b>${pct(b.G)}</b> прогнозный EBIT TO-BE составит <b>${money(b.EBITforecast)}</b>.<br>`;
t += `Прогноз EPS TO-BE составит <b>${b.EPSforecast.toFixed(2)}</b>.<br>`;
t += b.EPSforecast > a.EPSforecast
? `TO-BE дает более высокий прогноз EPS, что улучшает результат для акционеров.<br>`
: `TO-BE не улучшает прогноз EPS относительно AS-IS.<br>`;
t += `<span class="risk">Риск:</span> при снижении продаж эффект DTL работает в обратную сторону и может резко снизить EPS.`;
document.getElementById('forecastSummary').innerHTML = t;
if (forecastChart) forecastChart.destroy();
forecastChart = new Chart(document.getElementById('forecastChart'), {
type: 'bar',
data: {
labels: ['EBIT forecast, млн', 'EPS forecast'],
datasets: [
{ label: 'AS-IS', data: [a.EBITforecast/1000000,a.EPSforecast], backgroundColor: '#2f80ed' },
{ label: 'TO-BE', data: [b.EBITforecast/1000000,b.EPSforecast], backgroundColor: '#ff4d6d' }
]
},
options: darkChartOptions()
});
}
function darkChartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#d8e0e8', font: { size: 10 } } }
},
scales: {
x: { ticks: { color: '#d8e0e8', font: { size: 10 } }, grid: { color: '#26384a' } },
y: { ticks: { color: '#d8e0e8', font: { size: 10 } }, grid: { color: '#26384a' } }
}
};
}
function makeWorkbook() {
if (!lastAsIs || !lastToBe) calculateAll();
const a = lastAsIs;
const b = lastToBe;
const rows = [
['CVP-анализ: безубыточность, маржа безопасности и левериджи'],
[''],
['Исходные данные'],
['Показатель','AS-IS','TO-BE'],
['Продажи S',a.S,b.S],
['Цена P',a.P,b.P],
['AVC',a.AVC,b.AVC],
['TFC',a.FC,b.FC],
['PD',a.PD,b.PD],
['IP',a.IP,b.IP],
['Налог',a.TAX,b.TAX],
['Акции N',a.N,b.N],
['Прирост продаж',a.G,b.G],
[''],
['Оценка безубыточности и маржи безопасности'],
['Показатель','AS-IS','TO-BE'],
['TR',a.TR,b.TR],
['VC',a.VC,b.VC],
['FC',a.FC,b.FC],
['TC',a.TC,b.TC],
['EBIT',a.EBIT,b.EBIT],
['QBE',a.QBE,b.QBE],
['SBE',a.SBE,b.SBE],
['SM cash',a.SMcash,b.SMcash],
['SM %',a.SM,b.SM],
['ROS',a.ROS,b.ROS],
[''],
['Оценка силы левериджа'],
['Показатель','AS-IS','TO-BE'],
['DOL',a.DOL,b.DOL],
['DFL',a.DFL,b.DFL],
['DTL',a.DTL,b.DTL],
[''],
['Прогноз EBIT и EPS'],
['Показатель','AS-IS','TO-BE'],
['Прирост EBIT',a.EBITgrowth,b.EBITgrowth],
['Прогноз EBIT',a.EBITforecast,b.EBITforecast],
['EPS',a.EPS,b.EPS],
['Прирост EPS',a.EPSgrowth,b.EPSgrowth],
['Прогноз EPS',a.EPSforecast,b.EPSforecast]
];
const ws = XLSX.utils.aoa_to_sheet(rows);
ws['!cols'] = [{wch:42},{wch:22},{wch:22}];
const sectionRows = [2,14,27,33];
const headerRows = [3,15,28,34];
const range = XLSX.utils.decode_range(ws['!ref']);
for (let r = 0; r <= range.e.r; r++) {
for (let c = 0; c <= range.e.c; c++) {
const ref = XLSX.utils.encode_cell({r,c});
if (!ws[ref]) continue;
const isTitle = r === 0;
const isSection = sectionRows.includes(r);
const isHeader = headerRows.includes(r);
ws[ref].s = {
font: {
name: 'Arial',
sz: isTitle ? 16 : 11,
bold: isTitle || isSection || isHeader,
color: { rgb: isTitle || isSection || isHeader ? 'FFFFFF' : '111827' }
},
fill: {
fgColor: {
rgb: isTitle ? '0B1F33' : isSection ? '123B5D' : isHeader ? '1F2937' : 'FFFFFF'
}
},
alignment: {
wrapText: true,
vertical: 'center',
horizontal: isTitle || isSection || isHeader ? 'center' : 'left'
},
border: {
top: {style:'thin',color:{rgb:'D1D5DB'}},
bottom: {style:'thin',color:{rgb:'D1D5DB'}},
left: {style:'thin',color:{rgb:'D1D5DB'}},
right: {style:'thin',color:{rgb:'D1D5DB'}}
}
};
}
}
ws['!merges'] = [
XLSX.utils.decode_range('A1:C1')
];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb,ws,'Dashboard');
return wb;
}
function downloadExcel() {
const wb = makeWorkbook();
XLSX.writeFile(wb,'cvp_leverage_dashboard.xlsx');
}
function sendExcel() {
const email = document.getElementById('email').value.trim();
if (!email) {
document.getElementById('status').textContent = 'Введите email.';
return;
}
const wb = makeWorkbook();
const base64 = XLSX.write(wb,{bookType:'xlsx',type:'base64'});
document.getElementById('status').textContent = 'Отправка...';
google.script.run
.withSuccessHandler(msg => document.getElementById('status').textContent = msg)
.withFailureHandler(err => document.getElementById('status').textContent = 'Ошибка: ' + err.message)
.sendXlsxToEmail({
email: email,
base64: base64,
filename: 'cvp_leverage_dashboard.xlsx'
});
}
</script>
</body>
</html>