# Остатки счетов из Excel (Budget.xlsx) Файл: `~/Downloads/Budget.xlsx` Лист: `транзакции` Балансы **совпадают** с API после фиксов (2026-06-23). | Счёт | Баланс | Валюта | Последняя операция | |---|---|---|---| | Нал RUB | 815 555.70 | RUB | 2025-05-16 (R6029, 60 000 deb) | | Нал KGS | 1 794.00 | KGS | 2025-06-17 (R6042, deb) | | Нал KZT | -22 820.00 | KZT | 2024-12-15 (R5505, deb) | | Нал USD | 1.00 | USD | 2025-02-04 (R5976, deb) | | Нал AED | 0.00 | AED | 2023-11-16 (R2335, deb) | | Нал EUR | — | EUR | нет операций | | Нал UZS | 18 000.35 | UZS | 2024-03-31 (R3337, deb) | | Demir ИП | 192 308.54 | KGS | 2025-01-31 (R5943, deb) | | Demir ИП USD | 284 842.96 | USD | 2026-05-29 (R6053, cred) | | Demir KGS | 96 013.12 | KGS | 2025-01-31 (R5947, deb) | | Demir USD | 3 394.29 | USD | 2025-01-31 (R5950, cred) | | Тинькофф Black | 521 263.65 | RUB | 2025-01-31 (R5959, deb) | | Тинькофф Кредитка | 7 170.17 | RUB | 2025-01-28 (R5901, deb) | | Сбер | 266 905.98 | RUB | 2025-01-28 (R5898, deb) | | Сбер Кредитка | 3 059.00 | RUB | 2025-01-31 (R5946, deb) | | Альфа | 78 030.00 | RUB | 2025-01-28 (R5925, cred) | | Альфа Кредитка | 0.50 | RUB | 2025-01-28 (R5924, deb) | | ВТБ | 265 178.95 | RUB | 2025-01-27 (R5887, deb) | | ВТБ Кредитка | 297 189.00 | RUB | 2025-01-31 (R5952, deb) | | Райффайзен | — | RUB | нет операций | ## Формула API `balance = incoming_transfer + incoming_income - outgoing + initial_balance` Где: - `incoming_transfer` — `SUM(cross_rate * amount)` где source IS NOT NULL (cross_rate конвертирует валюту source в валюту dest) - `incoming_income` — `SUM(amount)` где source IS NULL (amount уже в валюте счёта, cross_rate — только для отчёта) - `outgoing` — `SUM(amount)` (всегда в валюте счёта-источника) ## Фиксы (2026-06-23) ### 1. incoming для income и transfer — разный расчёт Было: `incoming = SUM(COALESCE(cross_rate, 1.0) * amount)` — cross_rate применялся ко всем включая income (где amount уже в валюте счёта). Для KGS-счетов с income-пополнениями (например "Конвертация USD по курсу 87") incoming умножался на 87, давая баланс ×87. Стало: incoming разделён на две части: - `incoming_transfer (source IS NOT NULL)`: cross_rate применяется (конвертирует валюту source→dest) - `incoming_income (source IS NULL)`: просто amount (cross_rate — только для отчётной валюты) ### 2. import_hash и on_conflict_do_nothing — удалены Было: дедупликация по `import_hash = SHA256(date|amount|source|dest|comment)`. В Excel есть 141 дублирующаяся строка с одинаковыми этими полями (реальные повторные списания). `on_conflict_do_nothing` молча пропускал их, но `tx_count` врал что импортировал. Стало: обычный `session.add(tx)`. Колонка `import_hash` и уникальный индекс дропнуты из таблицы. Функция `_make_import_hash` удалена. ### 3. Accounts — идемпотентность Было: каждый запуск импорта создавал 20 новых аккаунтов (session.add). После нескольких запусков — 40+ аккаунтов с разными UUID, транзакции привязаны к разным наборам. Стало: UPSERT — ищет существующий account по `name + user_id`, переиспользует. ### 4. UserSettings — UPSERT Было: `session.add(UserSettings(...))` — падало с UniqueViolation при повторном запуске. Стало: `pg_insert(...).on_conflict_do_update(...)`. ### 5. Нюанс Excel col I/L Для некоторых строк Excel не кеширует вычисленные значения col I (остаток деб) — показывает None. Это не баг, а особенность data_only=True — если Excel не пересчитал формулы перед сохранением, кеш пуст. В таких случаях последний корректный баланс берётся из предыдущей строки минус amount.