Initial income_calculator project
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
164
backend/api/charts.py
Normal file
164
backend/api/charts.py
Normal file
@@ -0,0 +1,164 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user