add: проверка корректности расчетов в шаблоне
This commit is contained in:
parent
c479db6503
commit
7761ea7c7e
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
86
|
||||
78
|
||||
|
||||
13
AGENTS.md
13
AGENTS.md
@ -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
312
check-calculations.js
Normal 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
305
check-calculations.py
Normal 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)
|
||||
BIN
Отчет_о_проверке_расчетов.xlsx
Normal file
BIN
Отчет_о_проверке_расчетов.xlsx
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user