28 KiB
Budget App
Персональный бюджет в виде веб-приложения. Замена Excel-таблицы ~/Downloads/Budget.xlsx, в которой ведутся транзакции 2020–2024+ по нескольким счетам в разных валютах.
Solo на старте (только Alex), но код пишем generic — с прицелом на public SaaS позже (multi-tenant, конфиги отделены).
Источник: текущая Excel-таблица
Файл: ~/Downloads/Budget.xlsx (3.6 MB, 13 листов).
Листы
| Лист | Назначение |
|---|---|
транзакции |
Главное хранилище. ~34 462 строки, 28 колонок (A–AB). Все дебеты/кредиты по всем счетам подряд. |
курсы |
Справочник курсов: Currency, Rate, Date, computed-курс к выбранной валюте отчёта. До 9 467 строк (история по датам). |
категории |
Иерархия категорий: подкатегория → родитель с эмодзи (например Аренда → 🏢 Аренда). |
счета |
Справочник счетов: имя, валюта, активный (bool), льготный период, месячный платёж, курс к выбранной, name in form. |
2020 отчет…2024 отчет |
Годовые сводки: план/факт по доходам, расходам, инвестициям, разнице, накопленному капиталу. |
налоги 22-24 |
Учёт ИП-патентов, НДФЛ и пр. |
сводная |
Сводная таблица по годам. |
Наличные (форма) |
Hidden — форма быстрого ввода. |
test |
Песочница. |
Схема листа «транзакции»
Колонки (расшифрованы из shared strings + формул):
| Кол | Имя | Тип | Описание |
|---|---|---|---|
| A | дата | date | Дата операции. |
| B | сумма | number | Сумма в валюте счёта-источника. |
| C | дебет | ref→счета | Счёт, с которого ушло (источник). Пусто = доход извне. |
| D | кредит | ref→счета | Счёт, на который пришло (получатель). Пусто = расход вовне. |
| E | категория | ref→категории | Подкатегория (см. лист категории). |
| F | комментарий | text | Свободный комментарий. |
| G | курс | number | Кросс-курс дебет→кредит, если перевод между валютами. |
| H | входящ деб | number | Остаток на дебете до операции. |
| I | остаток деб | number | Остаток на дебете после. |
| J | входящ кред | formula | =IF(D<>"", B*IF(C<>"", G, 1), "") — сумма в валюте кредита. |
| K | остаток кред | number | Остаток на кредите до. |
| L | остаток кред | formula | =IF(D<>"", K+J, "") — остаток на кредите после. |
| M | вал. счёта | formula | VLOOKUP(C, счета, 2) — валюта дебета. |
| N | деб: курс к тек | number | Курс валюты дебета к выбранной валюте отчёта. |
| O | сумма в тек вал | number | Сумма в валюте отчёта (по курсу дебета). |
| P | — | — | Скрытая. |
| Q | крд: курс к тек | number | Курс валюты кредита к выбранной валюте отчёта. |
| R | сумма в тек | number | Сумма в валюте отчёта (по курсу кредита). |
| S | Налог | number | Сумма налога по этой операции. |
| T | Курс | number | Курс на дату операции. |
| U–AB | — | — | Резерв / служебные. |
Семантика двойной записи: одна строка = одно перемещение средств. Если C (дебет) пуст — это вход денег извне (доход). Если D (кредит) пуст — это выход вовне (расход). Если оба заполнены — внутренний перевод (между своими счетами, возможно с конверсией валют через G).
Категории (примеры родительских групп)
💰 Salary, 💸 Комиссии, 📟 Квартплата/Связь, 🧸 Дети, 🏢 Аренда, ⛽️ Транспорт/АЗС, 💊 Медицина, 🛒 Grocery, 🍣 Рестораны, 🧱 Стройка, 💷 Кредиты, 🏖️ Отдых, 💸 Налоги, 🛠️ Быт/Техника, 🚙 Авто, 👚 Одежда, 💰 Дивиденды, 💰 Депозиты.
Подкатегории — конкретные траты (Кафе и рестораны, АЗС, Кровля материалы, Salary 88 05.11, ...).
Счета (примеры)
Demir KGS (Сом), Нал USD (Доллар), Альфа (Рубль), Нал RUB (Рубль), Альфа Кредитка, Сбер, Demir ИП USD, Нал EUR (Евро), и т.д.
Валюты в обращении: RUB, USD, EUR, KGS, KZT, UZS, AED, CNY.
Что таблица умеет уже сейчас
- Учёт транзакций в любой валюте, с автопересчётом в выбранную валюту отчёта.
- Остатки по счетам в реальном времени (через
остаток деб/остаток кред). - Категоризация в иерархии (родитель/подкатегория) с эмодзи.
- Годовые отчёты: план vs факт, доход/расход/инвестиции, % отложено, накопленный капитал, долг.
- Курсы валют с историей (за каждую дату — свой курс).
- Налоговый учёт (отдельный лист
налоги 22-24). - Сводная — динамика по годам.
Болевые точки таблицы
- Excel на 34k+ строк уже тормозит на пересчёте формул (
VLOOKUPна массивы,IF-цепочки в каждой строке). - Ввод транзакций — ручной, через скрытую форму «Наличные», нет автоимпорта банковских выписок.
- Категоризация — ручная.
- Невозможно нормально работать с мобильного.
- Анализ ограничен сводными в Excel: нет нормальных графиков по подкатегориям, кросс-фильтрации, прогнозов.
- Нет ничего про FIRE: проекций пенсии, моделирования инвестиций, целевых процентов нормы сбережений.
- История курсов хранится в строках листа
курсы— это не нормализованная таблица. - Нет API: нельзя интегрировать с банковскими экспортами, нельзя автоматически тянуть в дашборд.
Концепция веб-приложения
Цель
Заменить Excel приложением, которое:
- Принимает все те же транзакции (multi-currency, multi-account, double-entry).
- Считает аналитику и сводки на лету через SQL, а не через
VLOOKUP. - Импортирует банковские выписки (Альфа, Сбер, Demir, …) автоматически + AI-резолвер для неоднозначных случаев.
- Даёт мобильный UI для быстрого ввода трат на ходу.
- Моделирует FIRE-сценарии (отдельная фаза после готового бюджетирования).
- Прогнозирует траты на N месяцев вперёд по сезонной модели (категории
🧱 Стройка,🏖️ Отдыхцикличны).
Доменная модель (первая итерация)
User (id, email, ...) -- для будущего multi-tenant SaaS
Settings (user_id, base_currency_id, ...) -- base currency выбирается в настройках
Currency (code, name, symbol)
Account (id, user_id, name, currency_id, type {cash, debit, credit, deposit, brokerage}, active, credit_limit, grace_period_days, monthly_payment)
CategoryGroup (id, user_id, name, emoji) -- родитель
Category (id, user_id, name, group_id) -- подкатегория
ExchangeRate (currency_id, date, rate_to_base, source {nbkr, manual, ...}) -- история по датам
Transaction (
id, user_id, date, amount,
source_account_id NULL, -- дебет (откуда); NULL = доход извне
dest_account_id NULL, -- кредит (куда); NULL = расход вовне
cross_rate NULL, -- курс при internal transfer
category_id, comment, tax_amount NULL,
import_id NULL -- ссылка на BankImport
)
TaxRecord (year, month, type {patent, ндфл, …}, amount, currency_id, paid_date NULL)
BankImport (id, user_id, account_id, source {alfa, sber, demir, ...}, fetched_at, raw_blob, status {pending, parsed, resolved, manual})
Alert (id, user_id, type {manual_resolve_needed, parse_failed, ...}, payload, sent_at, ack_at NULL) -- TG-уведомления
FireGoal (id, user_id, target_amount, target_currency_id, expected_return_rate, target_date NULL)
InvestmentSnapshot (date, account_id, value, currency_id)
Base currency не хардкодится — выбирается в Settings, и все пересчёты (остатки, отчёты, графики) автоматически приводятся к ней по ExchangeRate.rate_to_base на дату.
Архитектура
- Backend: Python + FastAPI + SQLAlchemy + Postgres.
- Frontend: Vue 3 + Vite + Pinia. Адаптив для мобильного (PWA).
- БД: отдельная БД
budget_appв существующем кластере Postgres на Eagle (тот же инстанс, что иpersonal_os— не отдельный контейнер). - Деплой: Docker Compose на Mac (Eagle). Конфиги в
~/docker/budget-app/(по конвенции остальных сервисов). Код проекта —~/Developer/budget-app/с git. - Внешний доступ:
budget.qentra.topчерез Cloudflare Tunnel (или существующий nginx-reverse-proxy). Auth — login/password + долгоживущий JWT-токен в localStorage/cookie. Один пользователь на старте, но схема готова под multi-tenant. - Импорт Excel: однократный скрипт миграции (
scripts/import-xlsx.py), читаетBudget.xlsx, заливает в БД. - Курсы валют: cron-джоб тянет курсы с НБ Кыргызстана (
nbkr.kgXML feed) по расписанию (раз в день). Если на какую-то дату курса нет — автоматически дозаполняет (back-fill ближайшего рабочего дня). Источник по умолчанию для KGS-базы; для других валют — ручной override или дополнительные источники (ЦБ РФ, ECB). - Импорт банков: автоматический fetch выписок (где есть API/HTML-парсинг) + парсеры CSV/XLS на Python (Альфа, Сбер, Demir — у каждого свой формат). После парсинга — прогон через AI-агента (LLM) для резолва неоднозначных строк: категоризация, merge дубликатов, идентификация контрагентов.
- Telegram-бот для алертов: оповещение, когда нужно ручное вмешательство (AI не смог однозначно категоризировать, парсер сломался на новом формате выписки, обнаружен дубль). Подтверждение/правка через инлайн-кнопки в TG → апдейт в БД.
- HTTP MCP-интеграция: приложение экспонирует MCP-сервер (HTTP transport), чтобы Eagle/Hermes мог запрашивать сводки, добавлять транзакции голосом, дёргать прогнозы из чата.
- Аналитика: SQL-агрегаты + Apache ECharts на фронте.
- FIRE / Инвестпортфели: отдельная фаза после того, как бюджетирование готово. Не в MVP.
Этапы
- Phase 0 — Discovery & schema. Полностью разобрать Excel, утвердить доменную модель. (в процессе — этот док)
- Phase 1 — Skeleton + import. Создать
~/Developer/budget-app/(git init), FastAPI + Vue в Docker Compose в~/docker/budget-app/с подключением к существующему Postgres-кластеру на хосте (новая БДbudget_app). Миграция xlsx → БД. Read-only viewer транзакций + остатков + годовые отчёты. Внешний доступ черезbudget.qentra.topс auth (FastAPI Users + JWT). - Phase 2 — Курсы и настройки. Cron-джоб для НБ КР с back-fill. Базовая валюта в настройках. Все пересчёты привязаны к ней.
- Phase 3 — Ручной ввод. Форма добавления транзакции, CRUD категорий/счетов, ручная правка курсов.
- Phase 4 — Импорт банков + AI-резолвер. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений.
- Phase 5 — Аналитика. Дашборды: расход по категориям, динамика, бёрндаун по бюджету. Прогнозы.
- Phase 6 — Mobile / PWA. Быстрый ввод с телефона.
- Phase 7 — MCP HTTP. Экспонировать MCP-эндпоинт для Hermes.
- Phase 8 — Инвестпортфели + FIRE. Тикеры/цены/дивы. Калькулятор FIRE. (отдельное планирование когда бюджетирование готово.)
Multi-tenant readiness (для будущего public SaaS)
Даже в solo-режиме на старте:
- Все таблицы доменной модели имеют
user_id(на solo — захардкожен на одного юзера). - Конфиги (домен, секреты, базовая валюта) — в
.env/ config-файле, не в коде. - AI-промпты, парсеры банков, источники курсов — pluggable, через интерфейсы/registry.
- Никаких личных данных Alex в коде (имена счетов, категорий, комментарии) — всё через миграцию данных в БД.
- Лицензия — выбрать перед публичным релизом (AGPL/MIT/коммерческая).
Открытые вопросы
Phase 0 закрыт — ключевые решения зафиксированы выше (auth, БД, raw blobs, импорт, multi-tenant readiness). Вопросы Phase 4 (AI-провайдер для резолвера, fallback на локальную LLM) — отложены до старта той фазы.
Phase 1 — Skeleton + import (детальный план)
Цель фазы
К концу Phase 1:
- FastAPI + Vue запущены в Docker Compose на Eagle.
- Все ~34 462 транзакции из
Budget.xlsxзалиты в БДbudget_app(в существующем кластере Postgres на хосте). - По адресу
https://budget.qentra.top(Cloudflare Tunnel) — login → read-only список транзакций, остатки по счетам, годовые отчёты. - Auth: FastAPI Users + JWT, один пользователь (Alex), долгоживущий токен (90 дней).
- Никакого CRUD пока — только просмотр. CRUD это Phase 3.
Структура репозитория ~/Developer/budget-app/
budget-app/
├── .env.example # шаблон конфига
├── .gitignore
├── README.md
├── docker-compose.yml # симлинк → ~/docker/budget-app/
├── backend/
│ ├── pyproject.toml # uv + ruff
│ ├── alembic.ini
│ ├── alembic/versions/ # миграции
│ ├── src/budget/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI app entry
│ │ ├── config.py # pydantic-settings из .env
│ │ ├── db.py # SQLAlchemy async engine
│ │ ├── auth/ # FastAPI Users
│ │ │ ├── models.py
│ │ │ ├── schemas.py
│ │ │ └── routes.py
│ │ ├── domain/ # SQLAlchemy модели
│ │ │ ├── user.py
│ │ │ ├── settings.py
│ │ │ ├── currency.py
│ │ │ ├── account.py
│ │ │ ├── category.py
│ │ │ ├── exchange_rate.py
│ │ │ └── transaction.py
│ │ ├── api/ # read-only роуты
│ │ │ ├── transactions.py
│ │ │ ├── accounts.py
│ │ │ └── reports.py
│ │ └── importers/
│ │ └── xlsx_import.py
│ └── tests/ # pytest
├── frontend/
│ ├── package.json # pnpm + Vue 3 + Vite + Pinia
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ └── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router.ts
│ ├── stores/auth.ts
│ ├── api/client.ts # axios + JWT interceptor
│ ├── pages/
│ │ ├── Login.vue
│ │ ├── Transactions.vue
│ │ ├── Accounts.vue
│ │ └── Reports.vue
│ └── components/
└── scripts/
├── import-xlsx.py # one-shot миграция
└── create-admin.sh # bootstrap первого юзера
Docker Compose (~/docker/budget-app/docker-compose.yml)
Три сервиса:
backend— FastAPI на uvicorn, порт 8400 (внутренний). Подключается к Postgres хоста черезhost.docker.internal:5432.frontend— nginx со статикой собранного Vite-билда, порт 8401. Проксирует/api/*наbackend:8400.tunnel—cloudflare/cloudflared, exposesbudget.qentra.top→frontend:80.
Volumes:
./data/imports/→/data/imports/(для будущих банковских raw blobs).~/Downloads/Budget.xlsx(read-only) →/data/import/Budget.xlsx(для миграции).
.env (через .env.example в репо)
# DB — подключение к существующему кластеру Postgres на хосте
DB_HOST=host.docker.internal
DB_PORT=5432
DB_NAME=budget_app
DB_USER=budget_app
DB_PASSWORD=...
# Auth
JWT_SECRET=... # openssl rand -hex 32
JWT_LIFETIME_SECONDS=7776000 # 90 дней (долгоживущий)
ADMIN_EMAIL=alex@...
ADMIN_PASSWORD=... # для первого юзера через create-admin.sh
# Settings
BASE_CURRENCY=KGS # дефолт, меняется в UI
# Cloudflare Tunnel
CLOUDFLARE_TUNNEL_TOKEN=...
Доменные модели для Phase 1 (минимум, чтобы залить xlsx и показать read-only)
| Таблица | Поля для MVP |
|---|---|
user |
id, email, hashed_password, is_active, is_superuser (FastAPI Users) |
settings |
id, user_id, base_currency_code |
currency |
code (PK), name, symbol |
account |
id, user_id, name, currency_code, type, active, credit_limit?, grace_period_days?, monthly_payment? |
category_group |
id, user_id, name, emoji |
category |
id, user_id, name, group_id |
exchange_rate |
currency_code, date, rate_to_base, source — PK по (currency_code, date, source) |
transaction |
id, user_id, date, amount, source_account_id?, dest_account_id?, cross_rate?, category_id?, comment, tax_amount?, import_id? (NULL для xlsx) |
Опускаем в Phase 1: tax_record, bank_import, alert, fire_goal, investment_snapshot — приедут в следующих фазах.
Шаги Phase 1 (порядок исполнения)
- Init репо:
~/Developer/budget-app/+ git init,.gitignore(python/node/docker/.env), README со ссылкой на этот док в vault. - Backend skeleton:
uv init, FastAPI app, конфиг через pydantic-settings, healthcheck/api/health. - Postgres bootstrap на Eagle: создать БД и юзера в существующем кластере —
Проверить, что кластер слушает
CREATE DATABASE budget_app; CREATE USER budget_app WITH PASSWORD '...'; GRANT ALL ON DATABASE budget_app TO budget_app;host.docker.internal(правкаpostgresql.conflisten_addresses+pg_hba.confесли нужно). - Alembic init + initial миграция: SQLAlchemy модели → таблицы.
- FastAPI Users: подключить, роуты
/api/auth/jwt/login,/api/auth/register(после bootstrap закрываем),/api/users/me. - Bootstrap первого юзера:
scripts/create-admin.shсоздаёт Alex из.env. - Импортёр xlsx (
scripts/import-xlsx.py):- Читает
~/Downloads/Budget.xlsxчерезopenpyxl(fallback наlxmlесли timeout'ы). - Парсит листы:
курсы→exchange_rate,категории→category_group/category,счета→account,транзакции→transaction. - Все записи привязываются к
user_idAlex. - Идемпотентно: повторный запуск с тем же файлом не дублирует (hash по
date+amount+source+dest+comment).
- Читает
- Read-only API:
GET /api/transactions?from=&to=&category=&account=&page=— пагинация.GET /api/accounts— счета с балансами (в валюте счёта + в base currency).GET /api/reports/yearly?year=— годовая сводка (план/факт по группам).
- Frontend skeleton:
pnpm create vite, Vue 3 + TS + Pinia, layout (header/sidebar/main), Login страница, JWT в localStorage. - Read-only страницы:
- Transactions: таблица с пагинацией, фильтры по дате/категории/счёту, поиск по комментарию.
- Accounts: список с текущими балансами в валюте счёта + в base currency.
- Reports: годовые сводки 2020–2024 по группам категорий, % сбережений.
- Docker Compose: написать в
~/docker/budget-app/, билдить backend/frontend, прокинутьhost.docker.internalдля Postgres. - Cloudflare Tunnel: создать DNS-запись
budget.qentra.top→ tunnel ID, прописать ingress в cloudflared конфиге. - Smoke-тест:
https://budget.qentra.top→ login → видишь все ~34k транзакций.GET /api/accountsвозвращает все счета с балансами, совпадающими с Excel (±погрешность округления).GET /api/reports/yearly?year=2024совпадает с листом2024 отчет.
Зависимости backend
fastapi,uvicorn[standard],pydantic-settingssqlalchemy[asyncio],asyncpg,alembicfastapi-users[sqlalchemy]openpyxl(xlsx-импорт)httpx(заготовка для cron курсов в Phase 2)python-multipart,passlib[bcrypt]- dev:
pytest,pytest-asyncio,ruff
Зависимости frontend
vue,vue-router,piniaaxios(JWT interceptor)@vueuse/coreecharts(скаффолд под Phase 5, не используем активно)- dev:
vite,typescript,eslint,prettier
Acceptance criteria Phase 1
https://budget.qentra.topоткрывается, login работает.- Все ~34 462 строки транзакций видны в UI с пагинацией.
- Балансы счетов совпадают с Excel (по последней транзакции каждого счёта).
- Годовые отчёты 2020–2024 совпадают с Excel-листами.
- Код в
~/Developer/budget-app/, БДbudget_appв существующем Postgres, Compose в~/docker/budget-app/. - Никаких хардкод-значений (имена счетов, категорий, комментарии) в коде — всё через xlsx-миграцию.
Риски и нюансы
- openpyxl на 34k строк: может быть медленно (>30 сек). Acceptable — миграция одноразовая. Fallback: прямой парс XML через
lxml. - Cloudflare Tunnel: первый раз нужен ручной шаг в Cloudflare dashboard (создать tunnel, получить токен).
- Долгоживущий JWT (90 дней): обычно не рекомендуется, но это явное требование. Документируем в README.
host.docker.internal+ Postgres: на macOS Postgres из brew обычно слушает толькоlocalhost. Нужно проверить и поправитьlisten_addresses = '*'вpostgresql.conf+ добавить запись вpg_hba.confдля подсети Docker (172.17.0.0/16/172.18.0.0/16).- Сетка Docker → DDG VPN: budget-app не нуждается в доступе к DDG-сетям, обычный bridge ок.
Следующий шаг
Старт Phase 1 — шаг 1 (init репо ~/Developer/budget-app/).
Создано: 2026-06-21 Статус: Phase 0 закрыт, Phase 1 спланирован