add: проверка корректности расчетов в шаблоне

This commit is contained in:
Kalinina.e 2026-09-15 06:11:57 +00:00
parent c479db6503
commit 7761ea7c7e
7 changed files with 705 additions and 6 deletions

View File

@ -84,3 +84,82 @@ npm notice
📈 Всего листов сравнено: 43
⚠️ Листов с различиями: 21
📋 Листов без различий: 4
--- run Tue Sep 15 06:10:50 AM UTC 2026 :: npm start --silent
Файл 1 листы: [
'0.Краткое содержание',
'1.Резюме проекта',
'2.1.Текущее состояние',
'2.2 Описание НП',
'2.3.Спецификации по этапам',
'Капекс СМЦ',
'2.3.1 КОФ',
'2.3.4 Норма расчета на 1 т.п.',
'2.3.6 нормативы ДУП',
'2.4.Основные показатели проекта',
'2.5.Этапы инвестирования',
'3.Входные данные',
'4.БКВ',
'5.Амортизация',
'6. Доходы',
'6.1 Доходы',
'7.Расходы',
'Opex',
'ФОТ',
'План подключения камер',
'7.1 Аренда каналов и реклама',
'8.ОДДС',
'9 КПЭ',
'10.Фин.пок-ли',
'11.Заключение',
'12.Анализ чувствительности',
'13. Риски проекта',
'13.1 Карта рисков_2024',
'13.2 Карта рисков_2027',
'13.3 Премия за специфич-й риск',
'14. Методика ПМ',
'Доп информация >',
'Лизинг'
]
Файл 2 листы: [
'0.Краткое содержание',
'1.Резюме проекта',
'2.1.Текущее состояние',
'2.2 Описание НП',
'2.3.Спецификации по этапам',
'2.3.1 КОФ',
'2.3.2.1 Маркет цена',
'2.3.2.2 Оборудование',
'2.3.3 СМР',
'2.3.4.1 Норма расчета 1т.- ДРБ',
'2.3.4.2 Норма расчет 1т.- ДКБ',
'2.3.5. Уд. Capex и ЗИП',
'2.3.6 нормативы ДУП',
'2.4.Основные показатели проекта',
'2.5.Этапы инвестирования',
'3.Входные данные',
'4.БКВ',
'5.Амортизация',
'6. Доходы',
'6.1 Доходы',
'7.Расходы',
'7.1 Аренда каналов и реклама',
'Займы',
'8.ОДДС',
'9 КПЭ',
'10.Фин.пок-ли',
'11.Заключение',
'12.Анализ чувствительности',
'13. Риски проекта',
'13.1 Карта рисков',
'13.2 Премия за специфич риск',
'13.3 Заявление о риск аппетите',
'14. Методика ПМ',
'Доп информация >',
'Лизинг'
]
✅ Сравнение завершено!
📊 Файл с результатами: Сравнение_БП_2026_НСВМ_и_Шаблон_БП_БКВ_2027.xlsx
📈 Всего листов сравнено: 43
⚠️ Листов с различиями: 21
📋 Листов без различий: 4

View File

@ -1 +1 @@
86
78

View File

@ -1,4 +1,4 @@
<!-- vibe42-agents-version: v39-skills-2026-09-11 -->
<!-- vibe42-agents-version: v41-relpaths-2026-09-15 -->
# Vibe42 — учебная песочница: сайты, боты и первые приложения
Workspace юзера `Kalinina.e`. Это **учебная среда**, где обычные люди (не разработчики) делают свой первый настоящий проект: сайт, telegram-бота или простое приложение.
@ -23,7 +23,8 @@ Workspace юзера `Kalinina.e`. Это **учебная среда**, где
|----------------|---------------|--------|
| Сайт, лендинг, визитка, портфолио, меню, афиша, waitlist | **Статика:** в проекте УЖЕ лежит стартовый `index.html` на дизайн-системе KT AI — открой его (`read`) и правь (`edit`), не пиши с нуля; при необходимости + `script.js`. Ванильный JS, БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. БЕЗ Tailwind/Bootstrap/Google Fonts и без своих hex-цветов — только классы `kt-ai-*` и токены `var(--kt-ai-…)`. | навык `landing-templates` (шаблоны секций A–E) + `design.md` → правка готового `index.html`, публикация в `pages` |
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | навык `backend-run` → Рецепт T |
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навык `backend-run` → Рецепт B |
| Простое приложение с сервером: форма→сохраняет, API, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | навык `backend-run` → Рецепт B |
| **Полноценное приложение:** кабинет, панель, CRM-lite, несколько экранов, живые данные — юзер явно просит «приложение/систему», а не страницу | **Express + Vite/React (фронт собирается в `dist/`, сервер его раздаёт с одного порта) + `data.json`.** Бюджет контейнера **512 МБ / 1 CPU**: зависимости — только express, react, react-dom, vite, @vitejs/plugin-react; НИКАКИХ next/nuxt/nest/prisma/mongoose/pg/tailwind. Сборку и установку делает `run` (scripts.build + scripts.start), сам в шелле не собирай | навык `backend-run` → Рецепт A |
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | навык `backend-run` → блок «ИИ» |
| Корпоративный агент Alem внутри проекта | тот же Node-скелет + вызов Alem по `process.env.ALEM_*` | навык `backend-run` → раздел «Агент Alem» |
| **Презентация, слайды, «сделай презу», PowerPoint, pptx, доклад** | **СТАТИКА: ОДИН `index.html` + pptxgenjs с CDN. Бэкенд НЕ поднимать, ИИ в рантайме НЕ звать** | навык `presentations` — бери оттуда скелет целиком |
@ -39,6 +40,7 @@ Workspace юзера `Kalinina.e`. Это **учебная среда**, где
1. Зависимости — ТОЛЬКО в `package.json` → `dependencies`. Их поставит `run`. НИКОГДА не пиши `npm install` в шелле.
2. Порт — всегда `process.env.PORT || 3000`. Не хардкодь другой.
3. Запуск бэкенда — ТОЛЬКО командой `run`. Никогда сам `node ...` / `npm start` в шелле.
4. **Пути во фронтенде — ТОЛЬКО относительные, без ведущего `/`:** `fetch("api/register")`, `src="script.js"`, `href="list.html"`. Приложение юзера открывается под под-путём (`…/proxy/3000/`), и `fetch("/api/…")` уходит мимо сервера — форма «не отправляется». В сервере наоборот сравнивай `url` с `/api/...` как обычно.
---
## 🍳 Готовые рецепты кода → навык `backend-run`
@ -100,7 +102,7 @@ Workspace юзера `Kalinina.e`. Это **учебная среда**, где
Файл памяти: `/srv/opencode/workspaces/users/Kalinina.e/.mimo/memory.md` (dot-папка `.mimo` не видна в Предпросмотре — так и задумано). Папка `.mimo/` уже создана, тебе нужно только читать/писать `memory.md`.
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/Kalinina.e/.mimo/memory.md` (если есть) ПЕРЕД первым ответом. Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
**В НАЧАЛЕ каждой сессии** — прочитай `/srv/opencode/workspaces/users/Kalinina.e/.mimo/memory.md` ПЕРЕД первым ответом (файл всегда существует; если он пустой — просто продолжай, не сообщай об этом). Если там есть имя/проекты — поздоровайся персонально: «С возвращением, <имя>! Продолжим <последний проект> или сделаем новый?». Если файла нет — это новый юзер, работай по обычному сценарию.
**ОБНОВЛЯЙ файл молча** (никогда не говори «я записал в память»), когда узнаёшь:
- как обращаться к юзеру (имя/ник);
@ -280,15 +282,16 @@ git push origin HEAD:pages
- После существенных правок уже опубликованного сайта НАПОМНИ юзеру нажать «Опубликовать» ещё раз — иначе на сайте останется старая версия.
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- **Публикация — только кнопкой «Опубликовать» в интерфейсе; API git-хостинга (Gitea/GitLab Pages) не вызывай** — публикацией управляет Pages-шлюз платформы, из проекта достаточно push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ `npm install` руками в шелле и тяжёлые зависимости (mongoose/pg/prisma/next/nuxt/nest) — контейнер юзера 512 МБ
- ❌ Поднимать бэкенд там, где хватает статики (лендинг, визитка, презентация) — сверься с таблицей стека
- ❌ Ставить прод-фреймворки (express / nest / next / django) — сервер только на `node:http`
- ❌ Ставить тяжёлые фреймворки (nest / next / nuxt / django) — сервер на `node:http`, а для полноценного приложения (Рецепт A) — express
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»

312
check-calculations.js Normal file
View File

@ -0,0 +1,312 @@
const XLSX = require("xlsx");
const wb = XLSX.readFile("Шаблон БП_ БКВ 2027 с займами.xlsx");
const sheets = wb.SheetNames;
console.log("📊 ПРОВЕРКА КОРРЕКТНОСТИ РАСЧЕТОВ В ШАБЛОНЕ\n");
console.log("Файл:", "Шаблон БП_ БКВ 2027 с займами.xlsx");
console.log("Листов:", sheets.length);
console.log("=" .repeat(70));
const errors = [];
const warnings = [];
const successChecks = [];
// Проверка 1: Анализ чувствительности (лист 12)
console.log("\n1️⃣ ПРОВЕРКА: Анализ чувствительности (лист 12.Анализ чувствительности)");
if (sheets.includes("12.Анализ чувствительности")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["12.Анализ чувствительности"], { header: 1 });
// Проверяем NPV базового сценария
const baseNPV = sheet[3]?.[2]; // Ячейка C4 (0-indexed: row 3, col 2)
const down10Revenue = sheet[4]?.[2]; // C5
const up10Expenses = sheet[4]?.[4]; // E5
const up10CAPEX = sheet[4]?.[6]; // G5
console.log(` Базовый NPV: ${baseNPV}`);
console.log(` NPV при -10% доходов: ${down10Revenue}`);
console.log(` NPV при +10% расходов: ${up10Expenses}`);
console.log(` NPV при +10% CAPEX: ${up10CAPEX}`);
// Проверка: NPV при снижении доходов должен быть меньше базового
if (baseNPV && down10Revenue) {
if (down10Revenue >= baseNPV) {
errors.push(`❌ Анализ чувствительности: NPV при -10% доходов (${down10Revenue}) >= базового NPV (${baseNPV})`);
} else {
successChecks.push(`✓ NPV корректно реагирует на снижение доходов`);
}
}
// Проверка: NPV должен становиться отрицательным при критическом снижении
if (baseNPV && baseNPV > 0) {
warnings.push(`⚠ Базовый NPV положительный (${baseNPV}), но в анализе чувствительности есть отрицательные значения`);
}
} else {
errors.push("❌ Лист '12.Анализ чувствительности' отсутствует");
}
// Проверка 2: EBITDA и риск-аппетит (лист 13.1 Карта рисков)
console.log("\n2️⃣ ПРОВЕРКА: Расчет риск-аппетита (лист 13.1 Карта рисков)");
if (sheets.includes("13.1 Карта рисков")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["13.1 Карта рисков"], { header: 1 });
const ebitda = sheet[0]?.[17]; // R1 (EBITDA)
const riskAppetitePercent = sheet[0]?.[18]; // S1 (7%)
const riskAppetiteValue = sheet[0]?.[19]; // T1 (3.86 млрд)
console.log(` EBITDA: ${ebitda} тыс. тг`);
console.log(` Риск-аппетит: ${riskAppetitePercent * 100}%`);
console.log(` Расчетное значение: ${riskAppetiteValue} млрд. тг`);
if (ebitda && riskAppetitePercent && riskAppetiteValue) {
const calculatedRiskAppetite = (ebitda * riskAppetitePercent) / 1000000000; // в млрд
const diff = Math.abs(calculatedRiskAppetite - riskAppetiteValue);
if (diff > 0.01) {
errors.push(`❌ Риск-аппетит: расчетное значение ${calculatedRiskAppetite.toFixed(3)} млрд ≠ ${riskAppetiteValue} млрд`);
} else {
successChecks.push(`✓ Риск-аппетит рассчитан верно: ${riskAppetiteValue} млрд. тг`);
}
}
} else {
errors.push("❌ Лист '13.1 Карта рисков' отсутствует");
}
// Проверка 3: Премия за специфичный риск (лист 13.2)
console.log("\n3️⃣ ПРОВЕРКА: Премия за специфичный риск (лист 13.2 Премия за специфич риск)");
if (sheets.includes("13.2 Премия за специфич риск")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["13.2 Премия за специфич риск"], { header: 1 });
// Ищем строки с оценками
let totalRiskPremium = 0;
let riskFactorsFound = 0;
for (let i = 6; i < Math.min(sheet.length, 35); i++) {
const row = sheet[i];
if (row && row[4] !== undefined) {
const riskValue = row[4]; // Столбец E - значение %
if (typeof riskValue === 'number' && riskValue > 0) {
totalRiskPremium += riskValue;
riskFactorsFound++;
console.log(` Фактор ${riskFactorsFound}: ${riskValue * 100}%`);
}
}
}
console.log(` Итого премия за риск: ${(totalRiskPremium * 100).toFixed(2)}%`);
// Проверка методики: сумма не должна превышать 5% (максимум по 4 факторам по 1%)
if (totalRiskPremium > 0.05) {
warnings.push(`⚠ Премия за специфичный риск ${(totalRiskPremium * 100).toFixed(2)}% превышает разумный предел (5%)`);
} else if (totalRiskPremium === 0) {
warnings.push("⚠ Премия за специфичный риск не рассчитана (все факторы = 0)");
} else {
successChecks.push(`✓ Премия за специфичный риск: ${(totalRiskPremium * 100).toFixed(2)}%`);
}
} else {
errors.push("❌ Лист '13.2 Премия за специфич риск' отсутствует");
}
// Проверка 4: Заявление о риск-аппетите (лист 13.3)
console.log("\n4️⃣ ПРОВЕРКА: Заявление о риск-аппетите (лист 13.3)");
if (sheets.includes("13.3 Заявление о риск аппетите")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["13.3 Заявление о риск аппетите"], { header: 1 });
// Проверяем наличие ошибок #DIV/0!
let divErrors = 0;
for (let i = 5; i < Math.min(sheet.length, 15); i++) {
const row = sheet[i];
if (row) {
for (let j = 0; j < row.length; j++) {
if (row[j] === "#DIV/0!") {
divErrors++;
}
}
}
}
if (divErrors > 0) {
errors.push(`❌ В листе '13.3 Заявление о риск аппетите' найдено ${divErrors} ошибок #DIV/0! - формулы не рассчитаны`);
} else {
successChecks.push(`✓ Заявление о риск-аппетите: формулы рассчитаны`);
}
} else {
errors.push("❌ Лист '13.3 Заявление о риск аппетите' отсутствует");
}
// Проверка 5: Лизинг (лист Лизинг)
console.log("\n5️⃣ ПРОВЕРКА: Расчеты лизинга (лист Лизинг)");
if (sheets.includes("Лизинг")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["Лизинг"], { header: 1 });
// Проверяем первый договор лизинга
const totalAmount1 = sheet[7]?.[5]; // F8 - Общая сумма первого договора
const paymentsSum = sheet[52]?.[4]; // E53 - Итого платежей
console.log(` Общая сумма договора 1: ${totalAmount1}`);
console.log(` Сумма платежей (график): ${paymentsSum}`);
if (totalAmount1 && totalAmount1 > 0) {
if (paymentsSum === 0 || paymentsSum === undefined) {
errors.push(`❌ Лизинг: сумма платежей (${paymentsSum}) не равна общей сумме договора (${totalAmount1})`);
} else {
successChecks.push(`✓ Лизинг: график платежей заполнен`);
}
} else {
warnings.push("⚠ Лизинг: общая сумма договора не заполнена или равна 0");
}
// Проверка: все ли платежи рассчитаны
let zeroPayments = 0;
for (let i = 10; i < 52; i++) {
if (sheet[i] && sheet[i][4] === 0) {
zeroPayments++;
}
}
if (zeroPayments > 40) {
warnings.push(`⚠ Лизинг: ${zeroPayments} платежей равны 0 - возможно, формулы не настроены`);
}
} else {
errors.push("❌ Лист 'Лизинг' отсутствует");
}
// Проверка 6: Финансовые показатели (лист 10.Фин.пок-ли)
console.log("\n6️⃣ ПРОВЕРКА: Финансовые показатели (лист 10.Фин.пок-ли)");
if (sheets.includes("10.Фин.пок-ли")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["10.Фин.пок-ли"], { header: 1 });
const discountRate = sheet[0]?.[19]; // T1 - ставка дисконтирования
const wacc = sheet[1]?.[19]; // T2 - WACC
console.log(` Ставка дисконтирования: ${discountRate ? (discountRate * 100).toFixed(2) + '%' : 'не заполнена'}`);
console.log(` WACC: ${wacc ? (wacc * 100).toFixed(2) + '%' : 'не заполнен'}`);
if (discountRate && discountRate > 0.3) {
warnings.push(`⚠ Ставка дисконтирования {(discountRate * 100).toFixed(1)}% выглядит завышенной`);
}
if (!discountRate || discountRate === 0) {
errors.push("❌ Ставка дисконтирования не заполнена или равна 0");
} else {
successChecks.push(`✓ Ставка дисконтирования: ${(discountRate * 100).toFixed(2)}%`);
}
} else {
errors.push("❌ Лист '10.Фин.пок-ли' отсутствует");
}
// Проверка 7: NPV, IRR, PI (лист 11.Заключение)
console.log("\n7️⃣ ПРОВЕРКА: Показатели эффективности (лист 11.Заключение)");
if (sheets.includes("11.Заключение")) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets["11.Заключение"], { header: 1 });
const npv = sheet[5]?.[19]; // T6 - NPV
const irr = sheet[6]?.[19]; // T7 - IRR
const pi = sheet[7]?.[19]; // T8 - PI
const pbp = sheet[8]?.[19]; // T9 - PBP
console.log(` NPV: ${npv}`);
console.log(` IRR: ${irr ? (irr * 100).toFixed(2) + '%' : 'не заполнен'}`);
console.log(` PI: ${pi}`);
console.log(` PBP: ${pbp} лет`);
// Проверка согласованности
if (npv !== undefined && npv < 0) {
warnings.push(`⚠ NPV отрицательный (${npv}) - проект неэффективен`);
}
if (irr && discountRate && irr < discountRate) {
errors.push(`❌ IRR (${(irr * 100).toFixed(2)}%) < ставки дисконтирования (${(discountRate * 100).toFixed(2)}%) - проект неэффективен`);
}
if (npv && npv > 0) {
successChecks.push(`✓ NPV положительный: ${npv} тыс. тг`);
}
} else {
errors.push("❌ Лист '11.Заключение' отсутствует");
}
// Проверка 8: Проверка на пустые ячейки в ключевых разделах
console.log("\n8️⃣ ПРОВЕРКА: Заполненность ключевых разделов");
const criticalSheets = ["1.Резюме проекта", "4.БКВ", "8.ОДДС", "9 КПЭ"];
criticalSheets.forEach(sheetName => {
if (sheets.includes(sheetName)) {
const sheet = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header: 1 });
let emptyCells = 0;
let totalCells = 0;
for (let i = 0; i < Math.min(sheet.length, 20); i++) {
for (let j = 0; j < (sheet[i]?.length || 0); j++) {
totalCells++;
if (sheet[i][j] === undefined || sheet[i][j] === null || sheet[i][j] === "") {
emptyCells++;
}
}
}
const emptyPercent = totalCells > 0 ? (emptyCells / totalCells * 100).toFixed(1) : 0;
if (parseFloat(emptyPercent) > 50) {
warnings.push(`⚠ Лист '${sheetName}': ${emptyPercent}% пустых ячеек в первых 20 строках`);
} else {
successChecks.push(`✓ Лист '${sheetName}': заполненность ${100 - parseFloat(emptyPercent)}%`);
}
}
});
// ВЫВОД РЕЗУЛЬТАТОВ
console.log("\n" + "=".repeat(70));
console.log("📋 ИТОГОВЫЙ ОТЧЕТ");
console.log("=".repeat(70));
if (errors.length > 0) {
console.log("\n❌ КРИТИЧЕСКИЕ ОШИБКИ (" + errors.length + "):");
errors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
}
if (warnings.length > 0) {
console.log("\n⚠ ПРЕДУПРЕЖДЕНИЯ (" + warnings.length + "):");
warnings.forEach((w, i) => console.log(` ${i + 1}. ${w}`));
}
if (successChecks.length > 0) {
console.log("\n✅ КОРРЕКТНЫЕ РАСЧЕТЫ (" + successChecks.length + "):");
successChecks.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
}
console.log("\n" + "=".repeat(70));
console.log("ВСЕГО НАЙДЕНО:");
console.log(` Ошибки: ${errors.length}`);
console.log(` Предупреждения: ${warnings.length}`);
console.log(` Корректных расчетов: ${successChecks.length}`);
console.log("=".repeat(70));
// Сохраняем отчет в файл
const reportData = [
["ПРОВЕРКА КОРРЕКТНОСТИ РАСЧЕТОВ В ШАБЛОНЕ"],
["Файл", "Шаблон БП_ БКВ 2027 с займами.xlsx"],
["Дата проверки", new Date().toLocaleString("ru-RU")],
[],
["КРИТИЧЕСКИЕ ОШИБКИ"],
...errors.map(e => [e]),
[],
["ПРЕДУПРЕЖДЕНИЯ"],
...warnings.map(w => [w]),
[],
["КОРРЕКТНЫЕ РАСЧЕТЫ"],
...successChecks.map(s => [s])
];
const reportWb = XLSX.utils.book_new();
const reportSheet = XLSX.utils.json_to_sheet(reportData.slice(5), { skipHeader: false });
XLSX.utils.sheet_add_aoa(reportSheet, reportData.slice(0, 5), { origin: "A1" });
XLSX.utils.book_append_sheet(reportWb, reportSheet, "Отчет о проверке");
const reportFile = "Отчет_о_проверке_расчетов.xlsx";
XLSX.writeFile(reportWb, reportFile);
console.log(`\n📁 Подробный отчет сохранен: ${reportFile}`);
// Выход с кодом ошибки если есть критические ошибки
if (errors.length > 0) {
process.exit(1);
}

305
check-calculations.py Normal file
View File

@ -0,0 +1,305 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import openpyxl
from datetime import datetime
print("📊 ПРОВЕРКА КОРРЕКТНОСТИ РАСЧЕТОВ В ШАБЛОНЕ\n")
print("Файл:", "Шаблон БП_ БКВ 2027 с займами.xlsx")
wb = openpyxl.load_workbook("Шаблон БП_ БКВ 2027 с займами.xlsx", data_only=True)
sheets = wb.sheetnames
print(f"Листов: {len(sheets)}")
print("=" * 70)
errors = []
warnings = []
success_checks = []
# Проверка 1: Финансовые показатели (лист 10.Фин.пок-ли)
print("\n1️⃣ ПРОВЕРКА: Финансовые показатели (лист 10.Фин.пок-ли)")
if "10.Фин.пок-ли" in sheets:
sheet = wb["10.Фин.пок-ли"]
# T1 - ставка дисконтирования, T2 - WACC
discount_rate = sheet.cell(row=1, column=20).value # T1
wacc = sheet.cell(row=2, column=20).value # T2
print(f" Ставка дисконтирования: {discount_rate}")
print(f" WACC: {wacc}")
if discount_rate is None or discount_rate == 0:
errors.append("❌ Ставка дисконтирования не заполнена или равна 0")
elif discount_rate > 0.3:
warnings.append(f"⚠ Ставка дисконтирования {discount_rate*100:.1f}% выглядит завышенной")
else:
success_checks.append(f"✓ Ставка дисконтирования: {discount_rate*100:.2f}%")
if wacc and wacc > discount_rate:
warnings.append(f"⚠ WACC ({wacc*100:.2f}%) > ставки дисконтирования ({discount_rate*100:.2f}%)")
else:
errors.append("❌ Лист '10.Фин.пок-ли' отсутствует")
# Проверка 2: NPV, IRR, PI (лист 11.Заключение)
print("\n2️⃣ ПРОВЕРКА: Показатели эффективности (лист 11.Заключение)")
if "11.Заключение" in sheets:
sheet = wb["11.Заключение"]
npv = sheet.cell(row=6, column=20).value # T6
irr = sheet.cell(row=7, column=20).value # T7
pi = sheet.cell(row=8, column=20).value # T8
pbp = sheet.cell(row=9, column=20).value # T9
print(f" NPV: {npv} тыс. тг")
print(f" IRR: {irr}")
print(f" PI: {pi}")
print(f" PBP: {pbp} лет")
if npv is None:
errors.append("❌ NPV не заполнен")
elif npv < 0:
warnings.append(f"⚠ NPV отрицательный ({npv}) - проект неэффективен")
else:
success_checks.append(f"✓ NPV положительный: {npv} тыс. тг")
if irr is None:
errors.append("❌ IRR не заполнен")
elif discount_rate and irr < discount_rate:
errors.append(f"❌ IRR ({irr*100:.2f}%) < ставки дисконтирования ({discount_rate*100:.2f}%) - проект неэффективен")
else:
success_checks.append(f"✓ IRR: {irr*100:.2f}%")
if pi is None:
errors.append("❌ PI не заполнен")
elif pi < 1:
warnings.append(f"⚠ PI < 1 ({pi}) - проект неэффективен")
else:
success_checks.append(f"✓ PI: {pi:.3f}")
else:
errors.append("❌ Лист '11.Заключение' отсутствует")
# Проверка 3: Анализ чувствительности (лист 12.Анализ чувствительности)
print("\n3️⃣ ПРОВЕРКА: Анализ чувствительности")
if "12.Анализ чувствительности" in sheets:
sheet = wb["12.Анализ чувствительности"]
base_npv = sheet.cell(row=4, column=3).value # C4
down_10_rev = sheet.cell(row=5, column=3).value # C5 (-10% доходы)
up_10_exp = sheet.cell(row=5, column=5).value # E5 (+10% расходы)
up_10_capex = sheet.cell(row=5, column=7).value # G5 (+10% CAPEX)
print(f" Базовый NPV: {base_npv}")
print(f" NPV при -10% доходов: {down_10_rev}")
print(f" NPV при +10% расходов: {up_10_exp}")
print(f" NPV при +10% CAPEX: {up_10_capex}")
if base_npv and down_10_rev:
if down_10_rev >= base_npv:
errors.append(f"❌ Анализ чувствительности: NPV при -10% доходов ({down_10_rev}) >= базового NPV ({base_npv})")
else:
success_checks.append("✓ NPV корректно реагирует на снижение доходов")
if base_npv and base_npv > 0:
if down_10_rev and down_10_rev < 0:
success_checks.append("✓ При -10% доходов NPV становится отрицательным (высокая чувствительность)")
else:
errors.append("❌ Лист '12.Анализ чувствительности' отсутствует")
# Проверка 4: EBITDA и риск-аппетит (лист 13.1 Карта рисков)
print("\n4️⃣ ПРОВЕРКА: Расчет риск-аппетита (лист 13.1 Карта рисков)")
if "13.1 Карта рисков" in sheets:
sheet = wb["13.1 Карта рисков"]
ebitda = sheet.cell(row=1, column=18).value # R1
risk_appetite_pct = sheet.cell(row=1, column=19).value # S1
risk_appetite_val = sheet.cell(row=1, column=20).value # T1
print(f" EBITDA: {ebitda} тыс. тг")
print(f" Риск-аппетит: {risk_appetite_pct*100 if risk_appetite_pct else None}%")
print(f" Расчетное значение: {risk_appetite_val} млрд. тг")
if ebitda and risk_appetite_pct and risk_appetite_val:
calculated = (ebitda * risk_appetite_pct) / 1_000_000_000
diff = abs(calculated - risk_appetite_val)
if diff > 0.1:
errors.append(f"❌ Риск-аппетит: расчетное значение {calculated:.3f} млрд ≠ {risk_appetite_val} млрд")
else:
success_checks.append(f"✓ Риск-аппетит рассчитан верно: {risk_appetite_val:.3f} млрд. тг")
else:
errors.append("❌ Лист '13.1 Карта рисков' отсутствует")
# Проверка 5: Премия за специфичный риск (лист 13.2)
print("\n5️⃣ ПРОВЕРКА: Премия за специфичный риск (лист 13.2)")
if "13.2 Премия за специфич риск" in sheets:
sheet = wb["13.2 Премия за специфич риск"]
total_risk_premium = 0
risk_factors = 0
for row in range(7, 35):
cell_value = sheet.cell(row=row, column=5).value # E7:E34
if cell_value and isinstance(cell_value, (int, float)) and cell_value > 0:
total_risk_premium += cell_value
risk_factors += 1
print(f" Фактор {risk_factors}: {cell_value*100:.2f}%")
print(f" Итого премия за риск: {total_risk_premium*100:.2f}%")
if total_risk_premium == 0:
warnings.append("⚠ Премия за специфичный риск не рассчитана (все факторы = 0)")
elif total_risk_premium > 0.05:
warnings.append(f"⚠ Премия за специфичный риск {total_risk_premium*100:.2f}% превышает разумный предел (5%)")
else:
success_checks.append(f"✓ Премия за специфичный риск: {total_risk_premium*100:.2f}%")
else:
errors.append("❌ Лист '13.2 Премия за специфич риск' отсутствует")
# Проверка 6: Заявление о риск-аппетите (лист 13.3)
print("\n6️⃣ ПРОВЕРКА: Заявление о риск-аппетите (лист 13.3)")
if "13.3 Заявление о риск аппетите" in sheets:
sheet = wb["13.3 Заявление о риск аппетите"]
div_errors = 0
for row in range(6, 15):
for col in range(1, 10):
cell_value = sheet.cell(row=row, column=col).value
if cell_value == "#DIV/0!":
div_errors += 1
if div_errors > 0:
errors.append(f"❌ В листе '13.3 Заявление о риск аппетите' найдено {div_errors} ошибок #DIV/0!")
else:
success_checks.append("✓ Заявление о риск-аппетите: формулы рассчитаны")
else:
errors.append("❌ Лист '13.3 Заявление о риск аппетите' отсутствует")
# Проверка 7: Лизинг (лист Лизинг)
print("\n7️⃣ ПРОВЕРКА: Расчеты лизинга")
if "Лизинг" in sheets:
sheet = wb["Лизинг"]
total_amount = sheet.cell(row=8, column=6).value # F8
payments_sum = sheet.cell(row=53, column=5).value # E53 (Итого)
print(f" Общая сумма договора: {total_amount}")
print(f" Сумма платежей по графику: {payments_sum}")
if total_amount and total_amount > 0:
if not payments_sum or payments_sum == 0:
errors.append(f"❌ Лизинг: сумма платежей ({payments_sum}) не равна общей сумме ({total_amount})")
else:
success_checks.append("✓ Лизинг: график платежей заполнен")
else:
warnings.append("⚠ Лизинг: общая сумма договора не заполнена или равна 0")
zero_payments = 0
for row in range(11, 52):
payment = sheet.cell(row=row, column=5).value
if payment == 0:
zero_payments += 1
if zero_payments > 40:
warnings.append(f"⚠ Лизинг: {zero_payments} платежей равны 0 - формулы не настроены")
else:
errors.append("❌ Лист 'Лизинг' отсутствует")
# Проверка 8: Заполненность БКВ (лист 4.БКВ)
print("\n8️⃣ ПРОВЕРКА: Заполненность БКВ")
if "4.БКВ" in sheets:
sheet = wb["4.БКВ"]
empty_cells = 0
total_cells = 0
for row in range(1, min(21, sheet.max_row + 1)):
for col in range(1, min(21, sheet.max_column + 1)):
total_cells += 1
cell_value = sheet.cell(row=row, column=col).value
if cell_value is None or cell_value == "":
empty_cells += 1
empty_pct = (empty_cells / total_cells * 100) if total_cells > 0 else 0
print(f" Пустых ячеек в первых 20 строках: {empty_pct:.1f}%")
if empty_pct > 50:
warnings.append(f"⚠ Лист '4.БКВ': {empty_pct:.1f}% пустых ячеек")
else:
success_checks.append(f"✓ Лист '4.БКВ': заполненность {100-empty_pct:.1f}%")
else:
errors.append("❌ Лист '4.БКВ' отсутствует")
# Проверка 9: ОДДС (лист 8.ОДДС)
print("\n9️⃣ ПРОВЕРКА: Отчет о движении денежных средств (8.ОДДС)")
if "8.ОДДС" in sheets:
sheet = wb["8.ОДДС"]
# Проверяем наличие отрицательных накопленных потоков в начале проекта
negative_dcf = 0
for col in range(5, 20): # Квартальные столбцы
dcf = sheet.cell(row=10, column=col).value # Накопленный дисконтированный поток
if dcf and dcf < 0:
negative_dcf += 1
if negative_dcf > 0:
success_checks.append(f"✓ ОДДС: {negative_dcf} кварталов с отрицательным накопленным DCF (нормально для начала проекта)")
else:
errors.append("❌ Лист '8.ОДДС' отсутствует")
# ВЫВОД РЕЗУЛЬТАТОВ
print("\n" + "=" * 70)
print("📋 ИТОГОВЫЙ ОТЧЕТ")
print("=" * 70)
if errors:
print(f"\n❌ КРИТИЧЕСКИЕ ОШИБКИ ({len(errors)}):")
for i, e in enumerate(errors, 1):
print(f" {i}. {e}")
if warnings:
print(f"\n⚠ ПРЕДУПРЕЖДЕНИЯ ({len(warnings)}):")
for i, w in enumerate(warnings, 1):
print(f" {i}. {w}")
if success_checks:
print(f"\n✅ КОРРЕКТНЫЕ РАСЧЕТЫ ({len(success_checks)}):")
for i, s in enumerate(success_checks, 1):
print(f" {i}. {s}")
print("\n" + "=" * 70)
print("ВСЕГО НАЙДЕНО:")
print(f" Ошибки: {len(errors)}")
print(f" Предупреждения: {len(warnings)}")
print(f" Корректных расчетов: {len(success_checks)}")
print("=" * 70)
# Сохраняем отчет
report_wb = openpyxl.Workbook()
report_sheet = report_wb.active
report_sheet.title = "Отчет о проверке"
report_data = [
["ПРОВЕРКА КОРРЕКТНОСТИ РАСЧЕТОВ В ШАБЛОНЕ"],
["Файл", "Шаблон БП_ БКВ 2027 с займами.xlsx"],
["Дата проверки", datetime.now().strftime("%d.%m.%Y %H:%M")],
[],
["КРИТИЧЕСКИЕ ОШИБКИ"] + [len(errors)],
] + [[e] for e in errors] + [
[],
["ПРЕДУПРЕЖДЕНИЯ"] + [len(warnings)],
] + [[w] for w in warnings] + [
[],
["КОРРЕКТНЫЕ РАСЧЕТЫ"] + [len(success_checks)],
] + [[s] for s in success_checks]
for row_idx, row_data in enumerate(report_data, 1):
for col_idx, value in enumerate(row_data, 1):
report_sheet.cell(row=row_idx, column=col_idx, value=value)
report_file = "Отчет_о_проверке_расчетов.xlsx"
report_wb.save(report_file)
print(f"\n📁 Подробный отчет сохранен: {report_file}")
if errors:
exit(1)

Binary file not shown.