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)]