116 lines
6.2 KiB
Python
116 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from decimal import Decimal
|
|
|
|
from sqlalchemy import extract, func
|
|
|
|
from ..models import Budget, Expense
|
|
from .i18n import get_locale
|
|
|
|
|
|
def _uncategorized() -> str:
|
|
return 'Uncategorized' if get_locale() == 'en' else 'Bez kategorii'
|
|
|
|
|
|
def _base_user_query(user_id: int, year: int, month: int | None = None):
|
|
query = Expense.query.filter_by(user_id=user_id, is_deleted=False).filter(extract('year', Expense.purchase_date) == year)
|
|
if month:
|
|
query = query.filter(extract('month', Expense.purchase_date) == month)
|
|
return query
|
|
|
|
|
|
def monthly_summary(user_id: int, year: int, month: int):
|
|
expenses = _base_user_query(user_id, year, month).order_by(Expense.purchase_date.desc(), Expense.id.desc()).all()
|
|
total = sum((expense.amount for expense in expenses), Decimal('0.00'))
|
|
category_totals = defaultdict(Decimal)
|
|
for expense in expenses:
|
|
category_name = expense.category.localized_name(get_locale()) if expense.category else _uncategorized()
|
|
category_totals[category_name] += expense.amount
|
|
budgets = Budget.query.filter_by(user_id=user_id, year=year, month=month).all()
|
|
budget_map = {budget.category_id: budget for budget in budgets}
|
|
alerts = []
|
|
for expense in expenses:
|
|
budget = budget_map.get(expense.category_id)
|
|
if budget and expense.category:
|
|
spent = category_totals.get(expense.category.localized_name(get_locale()), Decimal('0.00'))
|
|
ratio = (spent / budget.amount * 100) if budget.amount else 0
|
|
if ratio >= budget.alert_percent:
|
|
alerts.append({'category': expense.category.localized_name(get_locale()), 'ratio': float(ratio), 'budget': float(budget.amount)})
|
|
return expenses, total, category_totals, alerts
|
|
|
|
|
|
def yearly_totals(user_id: int, year: int, month: int | None = None):
|
|
if month:
|
|
return daily_totals(user_id, year, month)
|
|
rows = _base_user_query(user_id, year).with_entities(extract('month', Expense.purchase_date).label('month'), func.sum(Expense.amount)).group_by('month').order_by('month').all()
|
|
return [{'month': int(month), 'amount': float(amount)} for month, amount in rows]
|
|
|
|
|
|
def daily_totals(user_id: int, year: int, month: int | None = None):
|
|
rows = _base_user_query(user_id, year, month).with_entities(extract('day', Expense.purchase_date).label('day'), func.sum(Expense.amount)).group_by('day').order_by('day').all()
|
|
return [{'month': int(day), 'amount': float(amount)} for day, amount in rows]
|
|
|
|
|
|
def yearly_category_totals(user_id: int, year: int, month: int | None = None):
|
|
expenses = _base_user_query(user_id, year, month).all()
|
|
grouped = defaultdict(Decimal)
|
|
for expense in expenses:
|
|
name = expense.category.localized_name(get_locale()) if expense.category else _uncategorized()
|
|
grouped[name] += expense.amount
|
|
return [{'category': name, 'amount': float(amount)} for name, amount in grouped.items()]
|
|
|
|
|
|
def payment_method_totals(user_id: int, year: int, month: int | None = None):
|
|
rows = _base_user_query(user_id, year, month).with_entities(Expense.payment_method, func.sum(Expense.amount)).group_by(Expense.payment_method).all()
|
|
return [{'method': method, 'amount': float(amount)} for method, amount in rows]
|
|
|
|
|
|
def top_expenses(user_id: int, year: int, month: int | None = None, limit: int = 10):
|
|
rows = _base_user_query(user_id, year, month).order_by(Expense.amount.desc()).limit(limit).all()
|
|
return [{'title': row.title, 'amount': float(row.amount), 'date': row.purchase_date.isoformat()} for row in rows]
|
|
|
|
|
|
def yearly_overview(user_id: int, year: int, month: int | None = None):
|
|
expenses = _base_user_query(user_id, year, month).all()
|
|
total = sum((expense.amount for expense in expenses), Decimal('0.00'))
|
|
count = len(expenses)
|
|
average = (total / count) if count else Decimal('0.00')
|
|
refunds = sum((expense.amount for expense in expenses if expense.is_refund), Decimal('0.00'))
|
|
business_total = sum((expense.amount for expense in expenses if expense.is_business), Decimal('0.00'))
|
|
return {'total': float(total), 'count': count, 'average': float(average), 'refunds': float(refunds), 'business_total': float(business_total)}
|
|
|
|
|
|
def compare_years(user_id: int, year: int, month: int | None = None):
|
|
current = yearly_overview(user_id, year, month)
|
|
previous = yearly_overview(user_id, year - 1, month)
|
|
diff = current['total'] - previous['total']
|
|
pct = ((diff / previous['total']) * 100) if previous['total'] else 0
|
|
return {'current_year': year, 'previous_year': year - 1, 'current_total': current['total'], 'previous_total': previous['total'], 'difference': diff, 'percent_change': pct}
|
|
|
|
|
|
def range_totals(user_id: int, start_year: int, end_year: int, month: int | None = None):
|
|
rows = Expense.query.with_entities(extract('year', Expense.purchase_date).label('year'), func.sum(Expense.amount)).filter_by(user_id=user_id, is_deleted=False).filter(extract('year', Expense.purchase_date) >= start_year, extract('year', Expense.purchase_date) <= end_year)
|
|
if month:
|
|
rows = rows.filter(extract('month', Expense.purchase_date) == month)
|
|
rows = rows.group_by('year').order_by('year').all()
|
|
return [{'year': int(year), 'amount': float(amount)} for year, amount in rows]
|
|
|
|
|
|
def quarterly_totals(user_id: int, year: int, month: int | None = None):
|
|
expenses = _base_user_query(user_id, year, month).all()
|
|
quarters = {1: Decimal('0.00'), 2: Decimal('0.00'), 3: Decimal('0.00'), 4: Decimal('0.00')}
|
|
for expense in expenses:
|
|
quarter = ((expense.purchase_date.month - 1) // 3) + 1
|
|
quarters[quarter] += expense.amount
|
|
return [{'quarter': f'Q{quarter}', 'amount': float(amount)} for quarter, amount in quarters.items() if amount > 0]
|
|
|
|
|
|
def weekday_totals(user_id: int, year: int, month: int | None = None):
|
|
expenses = _base_user_query(user_id, year, month).all()
|
|
labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] if get_locale() == 'en' else ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Niedz']
|
|
totals = [Decimal('0.00') for _ in range(7)]
|
|
for expense in expenses:
|
|
totals[expense.purchase_date.weekday()] += expense.amount
|
|
return [{'day': labels[index], 'amount': float(amount)} for index, amount in enumerate(totals)]
|