581 lines
22 KiB
Python
581 lines
22 KiB
Python
from flask import Blueprint, render_template, request, jsonify
|
|
from flask_login import login_required, current_user
|
|
from app import db
|
|
from app.models import Expense, Category, Income
|
|
from sqlalchemy import func, extract
|
|
from datetime import datetime, timedelta
|
|
from collections import defaultdict
|
|
|
|
bp = Blueprint('main', __name__)
|
|
|
|
@bp.route('/')
|
|
def index():
|
|
if current_user.is_authenticated:
|
|
return render_template('dashboard.html')
|
|
return render_template('landing.html')
|
|
|
|
|
|
@bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
return render_template('dashboard.html')
|
|
|
|
|
|
@bp.route('/transactions')
|
|
@login_required
|
|
def transactions():
|
|
return render_template('transactions.html')
|
|
|
|
|
|
@bp.route('/reports')
|
|
@login_required
|
|
def reports():
|
|
return render_template('reports.html')
|
|
|
|
|
|
@bp.route('/settings')
|
|
@login_required
|
|
def settings():
|
|
return render_template('settings.html')
|
|
|
|
|
|
@bp.route('/documents')
|
|
@login_required
|
|
def documents():
|
|
return render_template('documents.html')
|
|
|
|
|
|
@bp.route('/recurring')
|
|
@login_required
|
|
def recurring():
|
|
return render_template('recurring.html')
|
|
|
|
|
|
@bp.route('/import')
|
|
@login_required
|
|
def import_page():
|
|
return render_template('import.html')
|
|
|
|
|
|
@bp.route('/income')
|
|
@login_required
|
|
def income():
|
|
return render_template('income.html')
|
|
|
|
|
|
@bp.route('/admin')
|
|
@login_required
|
|
def admin():
|
|
if not current_user.is_admin:
|
|
return render_template('404.html'), 404
|
|
return render_template('admin.html')
|
|
|
|
|
|
@bp.route('/api/dashboard-stats')
|
|
@login_required
|
|
def dashboard_stats():
|
|
now = datetime.utcnow()
|
|
|
|
# Current month stats
|
|
current_month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# Previous month stats
|
|
if now.month == 1:
|
|
prev_month_start = now.replace(year=now.year-1, month=12, day=1)
|
|
else:
|
|
prev_month_start = current_month_start.replace(month=current_month_start.month-1)
|
|
|
|
# Total spent this month (all currencies - show user's preferred currency)
|
|
current_month_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= current_month_start
|
|
).all()
|
|
current_month_total = sum(exp.amount for exp in current_month_expenses)
|
|
|
|
# Previous month total
|
|
prev_month_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= prev_month_start,
|
|
Expense.date < current_month_start
|
|
).all()
|
|
prev_month_total = sum(exp.amount for exp in prev_month_expenses)
|
|
|
|
# Current month income
|
|
current_month_income = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= current_month_start
|
|
).all()
|
|
current_income_total = sum(inc.amount for inc in current_month_income)
|
|
|
|
# Previous month income
|
|
prev_month_income = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= prev_month_start,
|
|
Income.date < current_month_start
|
|
).all()
|
|
prev_income_total = sum(inc.amount for inc in prev_month_income)
|
|
|
|
# Calculate profit/loss
|
|
current_profit = current_income_total - current_month_total
|
|
prev_profit = prev_income_total - prev_month_total
|
|
|
|
# Calculate percentage change
|
|
if prev_month_total > 0:
|
|
percent_change = ((current_month_total - prev_month_total) / prev_month_total) * 100
|
|
else:
|
|
percent_change = 100 if current_month_total > 0 else 0
|
|
|
|
# Active categories
|
|
active_categories = Category.query.filter_by(user_id=current_user.id).count()
|
|
|
|
# Total transactions this month
|
|
total_transactions = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= current_month_start
|
|
).count()
|
|
|
|
# Category breakdown for entire current year (all currencies)
|
|
current_year_start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
category_stats = db.session.query(
|
|
Category.id,
|
|
Category.name,
|
|
Category.color,
|
|
Category.icon,
|
|
func.sum(Expense.amount).label('total'),
|
|
func.count(Expense.id).label('count')
|
|
).join(Expense).filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= current_year_start
|
|
).group_by(Category.id).order_by(Category.display_order, Category.created_at).all()
|
|
|
|
# Monthly breakdown (all 12 months of current year) - including income
|
|
monthly_data = []
|
|
for month_num in range(1, 13):
|
|
month_start = now.replace(month=month_num, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
if month_num == 12:
|
|
month_end = now.replace(year=now.year+1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
else:
|
|
month_end = now.replace(month=month_num+1, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
month_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= month_start,
|
|
Expense.date < month_end
|
|
).all()
|
|
month_total = sum(exp.amount for exp in month_expenses)
|
|
|
|
month_income_list = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= month_start,
|
|
Income.date < month_end
|
|
).all()
|
|
month_income = sum(inc.amount for inc in month_income_list)
|
|
|
|
monthly_data.append({
|
|
'month': month_start.strftime('%b'),
|
|
'expenses': float(month_total),
|
|
'income': float(month_income),
|
|
'profit': float(month_income - month_total)
|
|
})
|
|
|
|
# Add budget status to category breakdown
|
|
category_breakdown = []
|
|
for stat in category_stats:
|
|
cat = Category.query.get(stat[0])
|
|
cat_data = {
|
|
'id': stat[0],
|
|
'name': stat[1],
|
|
'color': stat[2],
|
|
'icon': stat[3],
|
|
'total': float(stat[4]),
|
|
'count': stat[5]
|
|
}
|
|
if cat:
|
|
cat_data['budget_status'] = cat.get_budget_status()
|
|
cat_data['monthly_budget'] = cat.monthly_budget
|
|
cat_data['budget_alert_threshold'] = cat.budget_alert_threshold
|
|
category_breakdown.append(cat_data)
|
|
|
|
return jsonify({
|
|
'total_spent': float(current_month_total),
|
|
'total_income': float(current_income_total),
|
|
'profit_loss': float(current_profit),
|
|
'percent_change': round(percent_change, 1),
|
|
'active_categories': active_categories,
|
|
'total_transactions': total_transactions,
|
|
'currency': current_user.currency,
|
|
'category_breakdown': category_breakdown,
|
|
'monthly_data': monthly_data
|
|
})
|
|
|
|
|
|
@bp.route('/api/recent-transactions')
|
|
@login_required
|
|
def recent_transactions():
|
|
limit = request.args.get('limit', 10, type=int)
|
|
|
|
expenses = Expense.query.filter_by(user_id=current_user.id)\
|
|
.order_by(Expense.date.desc())\
|
|
.limit(limit)\
|
|
.all()
|
|
|
|
return jsonify({
|
|
'transactions': [expense.to_dict() for expense in expenses]
|
|
})
|
|
|
|
|
|
@bp.route('/api/reports-stats')
|
|
@login_required
|
|
def reports_stats():
|
|
"""
|
|
Generate comprehensive financial reports including income tracking
|
|
Security: Only returns data for current_user (enforced by user_id filter)
|
|
"""
|
|
period = request.args.get('period', '30') # days
|
|
category_filter = request.args.get('category_id', type=int)
|
|
|
|
try:
|
|
days = int(period)
|
|
except ValueError:
|
|
days = 30
|
|
|
|
now = datetime.utcnow()
|
|
period_start = now - timedelta(days=days)
|
|
|
|
# Query expenses with security filter
|
|
query = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= period_start
|
|
)
|
|
|
|
if category_filter:
|
|
query = query.filter_by(category_id=category_filter)
|
|
|
|
expenses = query.all()
|
|
|
|
# Query income for the same period
|
|
income_query = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= period_start
|
|
)
|
|
incomes = income_query.all()
|
|
|
|
# Total spent and earned in period
|
|
total_spent = sum(exp.amount for exp in expenses)
|
|
total_income = sum(inc.amount for inc in incomes)
|
|
|
|
# Previous period comparison for expenses and income
|
|
prev_period_start = period_start - timedelta(days=days)
|
|
prev_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= prev_period_start,
|
|
Expense.date < period_start
|
|
).all()
|
|
prev_total = sum(exp.amount for exp in prev_expenses)
|
|
|
|
prev_incomes = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= prev_period_start,
|
|
Income.date < period_start
|
|
).all()
|
|
prev_income_total = sum(inc.amount for inc in prev_incomes)
|
|
|
|
# Calculate profit/loss
|
|
current_profit = total_income - total_spent
|
|
prev_profit = prev_income_total - prev_total
|
|
|
|
percent_change = 0
|
|
if prev_total > 0:
|
|
percent_change = ((total_spent - prev_total) / prev_total) * 100
|
|
elif total_spent > 0:
|
|
percent_change = 100
|
|
|
|
# Income change percentage
|
|
income_percent_change = 0
|
|
if prev_income_total > 0:
|
|
income_percent_change = ((total_income - prev_income_total) / prev_income_total) * 100
|
|
elif total_income > 0:
|
|
income_percent_change = 100
|
|
|
|
# Profit/loss change percentage
|
|
profit_percent_change = 0
|
|
if prev_profit != 0:
|
|
profit_percent_change = ((current_profit - prev_profit) / abs(prev_profit)) * 100
|
|
elif current_profit != 0:
|
|
profit_percent_change = 100
|
|
|
|
# Top category (all currencies)
|
|
category_totals = {}
|
|
for exp in expenses:
|
|
cat_name = exp.category.name
|
|
category_totals[cat_name] = category_totals.get(cat_name, 0) + exp.amount
|
|
|
|
top_category = max(category_totals.items(), key=lambda x: x[1]) if category_totals else ('None', 0)
|
|
|
|
# Average daily spending
|
|
avg_daily = total_spent / days if days > 0 else 0
|
|
prev_avg_daily = prev_total / days if days > 0 else 0
|
|
avg_change = 0
|
|
if prev_avg_daily > 0:
|
|
avg_change = ((avg_daily - prev_avg_daily) / prev_avg_daily) * 100
|
|
elif avg_daily > 0:
|
|
avg_change = 100
|
|
|
|
# Savings rate calculation based on income (more accurate than budget)
|
|
if total_income > 0:
|
|
savings_amount = total_income - total_spent
|
|
savings_rate = (savings_amount / total_income) * 100
|
|
savings_rate = max(-100, min(100, savings_rate)) # Clamp between -100% and 100%
|
|
else:
|
|
# Fallback to budget if no income data
|
|
if current_user.monthly_budget and current_user.monthly_budget > 0:
|
|
savings_amount = current_user.monthly_budget - total_spent
|
|
savings_rate = (savings_amount / current_user.monthly_budget) * 100
|
|
savings_rate = max(0, min(100, savings_rate))
|
|
else:
|
|
savings_rate = 0
|
|
|
|
# Previous period savings rate
|
|
if prev_income_total > 0:
|
|
prev_savings_amount = prev_income_total - prev_total
|
|
prev_savings_rate = (prev_savings_amount / prev_income_total) * 100
|
|
prev_savings_rate = max(-100, min(100, prev_savings_rate))
|
|
else:
|
|
if current_user.monthly_budget and current_user.monthly_budget > 0:
|
|
prev_savings_amount = current_user.monthly_budget - prev_total
|
|
prev_savings_rate = (prev_savings_amount / current_user.monthly_budget) * 100
|
|
prev_savings_rate = max(0, min(100, prev_savings_rate))
|
|
else:
|
|
prev_savings_rate = 0
|
|
|
|
savings_rate_change = savings_rate - prev_savings_rate
|
|
|
|
# Category breakdown for pie chart
|
|
category_breakdown = []
|
|
for cat_name, amount in sorted(category_totals.items(), key=lambda x: x[1], reverse=True):
|
|
category = Category.query.filter_by(user_id=current_user.id, name=cat_name).first()
|
|
if category:
|
|
percentage = (amount / total_spent * 100) if total_spent > 0 else 0
|
|
category_breakdown.append({
|
|
'name': cat_name,
|
|
'color': category.color,
|
|
'amount': float(amount),
|
|
'percentage': round(percentage, 1)
|
|
})
|
|
|
|
# Daily spending and income trend (last 30 days)
|
|
daily_trend = []
|
|
for i in range(min(30, days)):
|
|
day_date = now - timedelta(days=i)
|
|
day_start = day_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
day_end = day_start + timedelta(days=1)
|
|
|
|
day_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= day_start,
|
|
Expense.date < day_end
|
|
).all()
|
|
day_total = sum(exp.amount for exp in day_expenses)
|
|
|
|
day_incomes = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= day_start,
|
|
Income.date < day_end
|
|
).all()
|
|
day_income = sum(inc.amount for inc in day_incomes)
|
|
|
|
daily_trend.insert(0, {
|
|
'date': day_date.strftime('%d %b'),
|
|
'expenses': float(day_total),
|
|
'income': float(day_income),
|
|
'profit': float(day_income - day_total)
|
|
})
|
|
|
|
# Monthly comparison with income (all 12 months of current year)
|
|
monthly_comparison = []
|
|
current_year = now.year
|
|
for month in range(1, 13):
|
|
month_start = datetime(current_year, month, 1)
|
|
if month == 12:
|
|
month_end = datetime(current_year + 1, 1, 1)
|
|
else:
|
|
month_end = datetime(current_year, month + 1, 1)
|
|
|
|
month_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= month_start,
|
|
Expense.date < month_end
|
|
).all()
|
|
month_total = sum(exp.amount for exp in month_expenses)
|
|
|
|
month_incomes = Income.query.filter(
|
|
Income.user_id == current_user.id,
|
|
Income.date >= month_start,
|
|
Income.date < month_end
|
|
).all()
|
|
month_income = sum(inc.amount for inc in month_incomes)
|
|
|
|
monthly_comparison.append({
|
|
'month': month_start.strftime('%b'),
|
|
'expenses': float(month_total),
|
|
'income': float(month_income),
|
|
'profit': float(month_income - month_total)
|
|
})
|
|
|
|
# Income sources breakdown
|
|
income_by_source = {}
|
|
for inc in incomes:
|
|
source = inc.source
|
|
income_by_source[source] = income_by_source.get(source, 0) + inc.amount
|
|
|
|
income_breakdown = [{
|
|
'source': source,
|
|
'amount': float(amount),
|
|
'percentage': round((amount / total_income * 100) if total_income > 0 else 0, 1)
|
|
} for source, amount in sorted(income_by_source.items(), key=lambda x: x[1], reverse=True)]
|
|
|
|
return jsonify({
|
|
'total_spent': float(total_spent),
|
|
'total_income': float(total_income),
|
|
'profit_loss': float(current_profit),
|
|
'percent_change': round(percent_change, 1),
|
|
'income_percent_change': round(income_percent_change, 1),
|
|
'profit_percent_change': round(profit_percent_change, 1),
|
|
'top_category': {'name': top_category[0], 'amount': float(top_category[1])},
|
|
'avg_daily': float(avg_daily),
|
|
'avg_daily_change': round(avg_change, 1),
|
|
'savings_rate': round(savings_rate, 1),
|
|
'savings_rate_change': round(savings_rate_change, 1),
|
|
'category_breakdown': category_breakdown,
|
|
'income_breakdown': income_breakdown,
|
|
'daily_trend': daily_trend,
|
|
'monthly_comparison': monthly_comparison,
|
|
'currency': current_user.currency,
|
|
'period_days': days
|
|
})
|
|
|
|
|
|
@bp.route('/api/smart-recommendations')
|
|
@login_required
|
|
def smart_recommendations():
|
|
"""
|
|
Generate smart financial recommendations based on user spending patterns
|
|
Security: Only returns recommendations for current_user
|
|
"""
|
|
now = datetime.utcnow()
|
|
|
|
# Get data for last 30 and 60 days for comparison
|
|
period_30 = now - timedelta(days=30)
|
|
period_60 = now - timedelta(days=60)
|
|
period_30_start = period_60
|
|
|
|
# Current period expenses (all currencies)
|
|
current_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= period_30
|
|
).all()
|
|
|
|
# Previous period expenses (all currencies)
|
|
previous_expenses = Expense.query.filter(
|
|
Expense.user_id == current_user.id,
|
|
Expense.date >= period_60,
|
|
Expense.date < period_30
|
|
).all()
|
|
|
|
current_total = sum(exp.amount for exp in current_expenses)
|
|
previous_total = sum(exp.amount for exp in previous_expenses)
|
|
|
|
# Category analysis
|
|
current_by_category = defaultdict(float)
|
|
previous_by_category = defaultdict(float)
|
|
|
|
for exp in current_expenses:
|
|
current_by_category[exp.category.name] += exp.amount
|
|
|
|
for exp in previous_expenses:
|
|
previous_by_category[exp.category.name] += exp.amount
|
|
|
|
recommendations = []
|
|
|
|
# Recommendation 1: Budget vs Spending
|
|
if current_user.monthly_budget and current_user.monthly_budget > 0:
|
|
budget_used_percent = (current_total / current_user.monthly_budget) * 100
|
|
remaining = current_user.monthly_budget - current_total
|
|
|
|
if budget_used_percent > 90:
|
|
recommendations.append({
|
|
'type': 'warning',
|
|
'icon': 'warning',
|
|
'color': 'red',
|
|
'title': 'Budget Alert' if current_user.language == 'en' else 'Alertă Buget',
|
|
'description': f'You\'ve used {budget_used_percent:.1f}% of your monthly budget. Only {abs(remaining):.2f} {current_user.currency} remaining.' if current_user.language == 'en' else f'Ai folosit {budget_used_percent:.1f}% din bugetul lunar. Mai rămân doar {abs(remaining):.2f} {current_user.currency}.'
|
|
})
|
|
elif budget_used_percent < 70 and remaining > 0:
|
|
recommendations.append({
|
|
'type': 'success',
|
|
'icon': 'trending_up',
|
|
'color': 'green',
|
|
'title': 'Great Savings Opportunity' if current_user.language == 'en' else 'Oportunitate de Economisire',
|
|
'description': f'You have {remaining:.2f} {current_user.currency} remaining from your budget. Consider saving or investing it.' if current_user.language == 'en' else f'Mai ai {remaining:.2f} {current_user.currency} din buget. Consideră să economisești sau să investești.'
|
|
})
|
|
|
|
# Recommendation 2: Category spending changes
|
|
for category_name, current_amount in current_by_category.items():
|
|
if category_name in previous_by_category:
|
|
previous_amount = previous_by_category[category_name]
|
|
if previous_amount > 0:
|
|
change_percent = ((current_amount - previous_amount) / previous_amount) * 100
|
|
|
|
if change_percent > 50: # 50% increase
|
|
recommendations.append({
|
|
'type': 'warning',
|
|
'icon': 'trending_up',
|
|
'color': 'yellow',
|
|
'title': f'{category_name} Spending Up' if current_user.language == 'en' else f'Cheltuieli {category_name} în Creștere',
|
|
'description': f'Your {category_name} spending increased by {change_percent:.0f}%. Review recent transactions.' if current_user.language == 'en' else f'Cheltuielile pentru {category_name} au crescut cu {change_percent:.0f}%. Revizuiește tranzacțiile recente.'
|
|
})
|
|
elif change_percent < -30: # 30% decrease
|
|
recommendations.append({
|
|
'type': 'success',
|
|
'icon': 'trending_down',
|
|
'color': 'green',
|
|
'title': f'{category_name} Savings' if current_user.language == 'en' else f'Economii {category_name}',
|
|
'description': f'Great job! You reduced {category_name} spending by {abs(change_percent):.0f}%.' if current_user.language == 'en' else f'Foarte bine! Ai redus cheltuielile pentru {category_name} cu {abs(change_percent):.0f}%.'
|
|
})
|
|
|
|
# Recommendation 3: Unusual transactions
|
|
if current_expenses:
|
|
category_averages = {}
|
|
for category_name, amount in current_by_category.items():
|
|
count = sum(1 for exp in current_expenses if exp.category.name == category_name)
|
|
category_averages[category_name] = amount / count if count > 0 else 0
|
|
|
|
for exp in current_expenses[-10:]: # Check last 10 transactions
|
|
category_avg = category_averages.get(exp.category.name, 0)
|
|
if category_avg > 0 and exp.amount > category_avg * 2: # 200% of average
|
|
recommendations.append({
|
|
'type': 'info',
|
|
'icon': 'info',
|
|
'color': 'blue',
|
|
'title': 'Unusual Transaction' if current_user.language == 'en' else 'Tranzacție Neobișnuită',
|
|
'description': f'A transaction of {exp.amount:.2f} {current_user.currency} in {exp.category.name} is higher than usual.' if current_user.language == 'en' else f'O tranzacție de {exp.amount:.2f} {current_user.currency} în {exp.category.name} este mai mare decât de obicei.'
|
|
})
|
|
break # Only show one unusual transaction warning
|
|
|
|
# Limit to top 3 recommendations
|
|
recommendations = recommendations[:3]
|
|
|
|
# If no recommendations, add a positive message
|
|
if not recommendations:
|
|
recommendations.append({
|
|
'type': 'success',
|
|
'icon': 'check_circle',
|
|
'color': 'green',
|
|
'title': 'Spending on Track' if current_user.language == 'en' else 'Cheltuieli sub Control',
|
|
'description': 'Your spending patterns look healthy. Keep up the good work!' if current_user.language == 'en' else 'Obiceiurile tale de cheltuieli arată bine. Continuă așa!'
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'recommendations': recommendations
|
|
})
|