Files
income_calculator/backend/api/charts.py
2026-02-23 16:49:24 +03:00

165 lines
6.8 KiB
Python

from fastapi import APIRouter, Query
from backend.db.database import get_connection
from backend.services.settings_service import get_income_settings, get_exclude_transfers
router = APIRouter(prefix="/api/charts", tags=["charts"])
def _parse_bank_ids(bank_ids: str | None) -> list[int] | None:
if not bank_ids or not bank_ids.strip():
return None
try:
return [int(x.strip()) for x in bank_ids.split(",") if x.strip()]
except ValueError:
return None
@router.get("/income-expense")
def income_expense_by_month(
year: int | None = Query(None),
period_start: str | None = Query(None),
period_end: str | None = Query(None),
bank_ids: str | None = Query(None),
):
"""Доход и расход по месяцам. Фильтры: year или period_start/period_end, bank_ids."""
conn = get_connection()
try:
salary_id = conn.execute("SELECT id FROM banks WHERE is_salary = 1").fetchone()
salary_id = salary_id["id"] if salary_id else None
only_salary_card, salary_account_id = get_income_settings()
exclude_filter = " AND (t.excluded_from_balance = 0 OR t.excluded_from_balance IS NULL)"
if get_exclude_transfers():
exclude_filter += " AND t.description NOT LIKE '%Перевод%'"
bank_id_list = _parse_bank_ids(bank_ids)
bank_filter = ""
params = []
if bank_id_list:
placeholders = ",".join("?" * len(bank_id_list))
bank_filter = f" AND a.bank_id IN ({placeholders})"
params = list(bank_id_list)
date_filter = ""
if year is not None:
date_filter = " AND strftime('%Y', t.operation_date) = ?"
params = [str(year)] + params
elif period_start or period_end:
if period_start:
date_filter += " AND t.operation_date >= ?"
params.append(period_start)
if period_end:
date_filter += " AND t.operation_date <= ?"
params.append(period_end)
rows = conn.execute("""
SELECT strftime('%Y-%m', t.operation_date) as month,
SUM(CASE WHEN t.amount > 0 THEN t.amount ELSE 0 END) as debit,
SUM(CASE WHEN t.amount < 0 THEN -t.amount ELSE 0 END) as credit,
a.bank_id, a.id as account_id
FROM transactions t
JOIN accounts a ON t.account_id = a.id
WHERE 1=1
""" + exclude_filter + bank_filter + date_filter + """
GROUP BY month, a.bank_id, a.id
ORDER BY month
""", params).fetchall()
# Агрегируем по месяцам; доход — по зарплатному банку или только по выбранной карте
by_month = {}
for r in rows:
m = r["month"]
if m not in by_month:
by_month[m] = {"month": m, "debit": 0.0, "credit": 0.0, "income": 0.0}
by_month[m]["debit"] += r["debit"]
by_month[m]["credit"] += r["credit"]
if only_salary_card and salary_account_id and r["account_id"] == salary_account_id:
by_month[m]["income"] += r["debit"]
elif not only_salary_card and salary_id and r["bank_id"] == salary_id:
by_month[m]["income"] += r["debit"]
return list(by_month.values())
finally:
conn.close()
@router.get("/balance-dynamics")
def balance_dynamics(
period_start: str | None = Query(None),
period_end: str | None = Query(None),
bank_ids: str | None = Query(None),
):
"""Суммарный остаток на конец каждого дня (накопленно). bank_ids — фильтр по банкам."""
conn = get_connection()
try:
bank_id_list = _parse_bank_ids(bank_ids)
if bank_id_list:
placeholders = ",".join("?" * len(bank_id_list))
acc_filter = f" AND account_id IN (SELECT id FROM accounts WHERE bank_id IN ({placeholders}))"
params = list(bank_id_list)
else:
acc_filter = ""
params = []
transfer_filter = " AND description NOT LIKE '%Перевод%'" if get_exclude_transfers() else ""
sql = """
SELECT date(operation_date) as day, SUM(amount) as daily_total
FROM transactions
WHERE (excluded_from_balance = 0 OR excluded_from_balance IS NULL)
""" + transfer_filter + acc_filter
if period_start:
sql += " AND operation_date >= ?"
params.append(period_start)
if period_end:
sql += " AND operation_date <= ?"
params.append(period_end)
sql += " GROUP BY day ORDER BY day"
rows = conn.execute(sql, params).fetchall()
# Накопленный итог
cumul = 0.0
result = []
for r in rows:
cumul += r["daily_total"]
result.append({"date": r["day"], "balance": round(cumul, 2)})
return result
finally:
conn.close()
@router.get("/savings-dynamics")
def savings_dynamics(
period_start: str | None = Query(None),
period_end: str | None = Query(None),
bank_ids: str | None = Query(None),
):
"""Динамика копилки: накопленная сумма операций «Перевод между счетами одного клиента» по дням."""
conn = get_connection()
try:
bank_id_list = _parse_bank_ids(bank_ids)
if bank_id_list:
placeholders = ",".join("?" * len(bank_id_list))
acc_filter = f" AND account_id IN (SELECT id FROM accounts WHERE bank_id IN ({placeholders}))"
params = ["%Перевод между счетами одного клиента%"] + list(bank_id_list)
else:
acc_filter = ""
params = ["%Перевод между счетами одного клиента%"]
transfer_filter = " AND description NOT LIKE '%Перевод%'" if get_exclude_transfers() else ""
sql = """
SELECT date(operation_date) as day, SUM(-amount) as daily_savings
FROM transactions
WHERE description LIKE ?
AND (excluded_from_balance = 0 OR excluded_from_balance IS NULL)
""" + transfer_filter + acc_filter
if period_start:
sql += " AND operation_date >= ?"
params.append(period_start)
if period_end:
sql += " AND operation_date <= ?"
params.append(period_end)
sql += " GROUP BY day ORDER BY day"
rows = conn.execute(sql, params).fetchall()
cumul = 0.0
result = []
for r in rows:
cumul += r["daily_savings"]
result.append({"date": r["day"], "savings": round(cumul, 2)})
return result
finally:
conn.close()