Project 4: Personal Finance Tracker
Welcome to the capstone project of Python Mastery! We’re building a comprehensive personal finance management application that combines everything you’ve learned - from basic data structures to advanced web development, databases, data analysis, and user interfaces.
Project Overview
FinanceTracker is a full-featured personal finance application that helps users:
- 💰 Track income and expenses with categories and tags
- 📊 Generate financial reports and visualizations
- 🎯 Set and monitor budgets with alerts
- 💳 Manage multiple accounts (checking, savings, credit cards)
- 📈 Analyze spending patterns with trends and insights
- 🎯 Set financial goals and track progress
- 📧 Receive automated alerts for bills and budget limits
- 🔐 Secure user authentication and data privacy
- 📱 Responsive web interface for all devices
Learning Objectives
By the end of this project, you’ll be able to:
- Design and implement complex database schemas
- Build full-stack web applications with authentication
- Create data visualization dashboards
- Implement business logic for financial calculations
- Handle user sessions and security
- Generate automated reports and notifications
- Create RESTful APIs
- Deploy applications with proper configuration
Project Requirements
Core Features
-
Transaction Management
- Add, edit, delete income and expense transactions
- Categorize transactions (Food, Transportation, Entertainment, etc.)
- Tag transactions for flexible grouping
- Attach receipts and notes
- Recurring transaction support
-
Account Management
- Multiple account types (checking, savings, credit cards, investments)
- Account balances and reconciliation
- Transfer money between accounts
- Account-specific transaction views
-
Budgeting System
- Create monthly budgets by category
- Track spending against budgets
- Budget alerts and notifications
- Budget vs actual reports
-
Financial Reports
- Income vs expense summaries
- Category spending breakdowns
- Monthly and yearly trends
- Net worth calculations
- Cash flow analysis
Advanced Features
-
Financial Goals
- Set savings goals with target dates
- Track progress toward goals
- Goal-based savings recommendations
-
Data Analysis
- Spending pattern analysis
- Seasonal spending trends
- Expense forecasting
- Financial health scoring
-
Automation
- Email alerts for bills and budget limits
- Automated transaction categorization
- Recurring transaction management
- Data export and backup
Project Structure
financetracker/
├── app/
│ ├── __init__.py # Flask application factory
│ ├── models.py # Database models
│ ├── routes.py # Web routes
│ ├── auth.py # Authentication
│ ├── forms.py # Web forms
│ ├── finance.py # Financial calculations
│ ├── reports.py # Report generation
│ ├── notifications.py # Email alerts
│ └── utils.py # Helper functions
├── templates/
│ ├── base.html # Base template
│ ├── dashboard.html # Main dashboard
│ ├── transactions.html # Transaction management
│ ├── accounts.html # Account management
│ ├── budgets.html # Budget management
│ ├── reports.html # Financial reports
│ ├── goals.html # Financial goals
│ └── auth/ # Authentication templates
├── static/
│ ├── css/
│ │ ├── style.css # Main stylesheet
│ │ └── charts.css # Chart styling
│ ├── js/
│ │ ├── dashboard.js # Dashboard functionality
│ │ ├── transactions.js # Transaction management
│ │ ├── charts.js # Chart visualizations
│ │ └── app.js # General app functionality
│ └── img/
│ ├── icons/ # Financial icons
│ └── logo.png
├── migrations/ # Database migrations
├── tests/
│ ├── test_models.py
│ ├── test_routes.py
│ ├── test_finance.py
│ └── test_reports.py
├── config.py # Configuration
├── requirements.txt # Dependencies
├── run.py # Application entry point
└── README.md # Documentation
Step 1: Database Design
Create a comprehensive database schema for financial data.
# app/models.py
from datetime import datetime, date
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
class User(UserMixin, db.Model):
"""User model with authentication."""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
# User preferences
currency = db.Column(db.String(3), default='USD')
date_format = db.Column(db.String(20), default='MM/DD/YYYY')
theme = db.Column(db.String(10), default='light')
# Relationships
accounts = db.relationship('Account', backref='user', lazy='dynamic')
transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
budgets = db.relationship('Budget', backref='user', lazy='dynamic')
goals = db.relationship('FinancialGoal', backref='user', lazy='dynamic')
def set_password(self, password):
"""Set password hash."""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""Check password."""
return check_password_hash(self.password_hash, password)
def get_net_worth(self):
"""Calculate user's net worth."""
total_balance = sum(account.balance for account in self.accounts)
return total_balance
def get_monthly_income(self, year=None, month=None):
"""Get monthly income for specified period."""
query = self.transactions.filter_by(type='income')
if year and month:
start_date = date(year, month, 1)
if month == 12:
end_date = date(year + 1, 1, 1)
else:
end_date = date(year, month + 1, 1)
query = query.filter(Transaction.date >= start_date, Transaction.date < end_date)
return query.with_entities(db.func.sum(Transaction.amount)).scalar() or 0
def get_monthly_expenses(self, year=None, month=None):
"""Get monthly expenses for specified period."""
query = self.transactions.filter_by(type='expense')
if year and month:
start_date = date(year, month, 1)
if month == 12:
end_date = date(year + 1, 1, 1)
else:
end_date = date(year, month + 1, 1)
query = query.filter(Transaction.date >= start_date, Transaction.date < end_date)
return query.with_entities(db.func.sum(Transaction.amount)).scalar() or 0
class Account(db.Model):
"""Financial account model."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
account_type = db.Column(db.String(20), nullable=False) # checking, savings, credit_card, investment
balance = db.Column(db.Float, default=0.0)
currency = db.Column(db.String(3), default='USD')
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Account details
institution = db.Column(db.String(100)) # Bank name
account_number = db.Column(db.String(50)) # Masked account number
credit_limit = db.Column(db.Float) # For credit cards
interest_rate = db.Column(db.Float) # For savings/credit accounts
# Foreign key
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Relationships
transactions = db.relationship('Transaction', backref='account', lazy='dynamic')
def update_balance(self):
"""Recalculate account balance from transactions."""
total_income = self.transactions.filter_by(type='income').with_entities(
db.func.sum(Transaction.amount)).scalar() or 0
total_expenses = self.transactions.filter_by(type='expense').with_entities(
db.func.sum(Transaction.amount)).scalar() or 0
# For credit cards, balance is negative of available credit
if self.account_type == 'credit_card':
self.balance = -(total_expenses - total_income)
else:
self.balance = total_income - total_expenses
db.session.commit()
def get_available_balance(self):
"""Get available balance (considering credit limit for credit cards)."""
if self.account_type == 'credit_card' and self.credit_limit:
return self.credit_limit + self.balance # balance is negative for credit cards
return self.balance
class Category(db.Model):
"""Transaction category model."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
type = db.Column(db.String(10), nullable=False) # income, expense
color = db.Column(db.String(7), default='#3498db') # Hex color
icon = db.Column(db.String(50), default='fa-tag') # FontAwesome icon
is_default = db.Column(db.Boolean, default=False)
# Relationships
transactions = db.relationship('Transaction', backref='category_obj', lazy='dynamic')
@staticmethod
def get_default_categories():
"""Get default categories for new users."""
return [
# Income categories
{'name': 'Salary', 'type': 'income', 'color': '#27ae60', 'icon': 'fa-money-bill-wave'},
{'name': 'Freelance', 'type': 'income', 'color': '#2ecc71', 'icon': 'fa-laptop-code'},
{'name': 'Investment', 'type': 'income', 'color': '#f39c12', 'icon': 'fa-chart-line'},
{'name': 'Other Income', 'type': 'income', 'color': '#e67e22', 'icon': 'fa-plus-circle'},
# Expense categories
{'name': 'Food & Dining', 'type': 'expense', 'color': '#e74c3c', 'icon': 'fa-utensils'},
{'name': 'Transportation', 'type': 'expense', 'color': '#3498db', 'icon': 'fa-car'},
{'name': 'Shopping', 'type': 'expense', 'color': '#9b59b6', 'icon': 'fa-shopping-bag'},
{'name': 'Entertainment', 'type': 'expense', 'color': '#f39c12', 'icon': 'fa-film'},
{'name': 'Bills & Utilities', 'type': 'expense', 'color': '#e67e22', 'icon': 'fa-lightbulb'},
{'name': 'Healthcare', 'type': 'expense', 'color': '#c0392b', 'icon': 'fa-heartbeat'},
{'name': 'Education', 'type': 'expense', 'color': '#16a085', 'icon': 'fa-graduation-cap'},
{'name': 'Travel', 'type': 'expense', 'color': '#8e44ad', 'icon': 'fa-plane'},
{'name': 'Other Expense', 'type': 'expense', 'color': '#7f8c8d', 'icon': 'fa-tag'}
]
class Transaction(db.Model):
"""Transaction model."""
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Float, nullable=False)
type = db.Column(db.String(10), nullable=False) # income, expense, transfer
description = db.Column(db.String(200))
date = db.Column(db.Date, nullable=False, default=date.today)
category = db.Column(db.String(50)) # Store category name for flexibility
tags = db.Column(db.String(200)) # Comma-separated tags
notes = db.Column(db.Text)
receipt_path = db.Column(db.String(200)) # Path to receipt image
# Recurring transaction fields
is_recurring = db.Column(db.Boolean, default=False)
recurrence_pattern = db.Column(db.String(20)) # daily, weekly, monthly, yearly
recurrence_end_date = db.Column(db.Date)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
account_id = db.Column(db.Integer, db.ForeignKey('account.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# Transfer fields
transfer_to_account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
def get_tags_list(self):
"""Get tags as a list."""
return [tag.strip() for tag in (self.tags or '').split(',') if tag.strip()]
def set_tags_list(self, tags_list):
"""Set tags from a list."""
self.tags = ', '.join(tags_list)
class Budget(db.Model):
"""Budget model for expense tracking."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
amount = db.Column(db.Float, nullable=False)
spent = db.Column(db.Float, default=0.0)
period = db.Column(db.String(20), default='monthly') # monthly, yearly
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=False)
category = db.Column(db.String(50)) # Budget category
is_active = db.Column(db.Boolean, default=True)
# Alert settings
alert_threshold = db.Column(db.Float, default=80.0) # Percentage
alert_sent = db.Column(db.Boolean, default=False)
# Foreign key
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def get_remaining_amount(self):
"""Get remaining budget amount."""
return self.amount - self.spent
def get_spent_percentage(self):
"""Get percentage of budget spent."""
if self.amount == 0:
return 0
return (self.spent / self.amount) * 100
def should_alert(self):
"""Check if budget alert should be sent."""
return (not self.alert_sent and
self.get_spent_percentage() >= self.alert_threshold)
def update_spent_amount(self):
"""Update spent amount from transactions."""
from app import db
from datetime import datetime
# Get transactions in budget period and category
query = Transaction.query.filter(
Transaction.user_id == self.user_id,
Transaction.date >= self.start_date,
Transaction.date <= self.end_date,
Transaction.type == 'expense'
)
if self.category:
query = query.filter(Transaction.category == self.category)
self.spent = query.with_entities(db.func.sum(Transaction.amount)).scalar() or 0.0
db.session.commit()
class FinancialGoal(db.Model):
"""Financial goal model."""
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
target_amount = db.Column(db.Float, nullable=False)
current_amount = db.Column(db.Float, default=0.0)
target_date = db.Column(db.Date)
priority = db.Column(db.String(10), default='medium') # low, medium, high
is_completed = db.Column(db.Boolean, default=False)
completed_at = db.Column(db.DateTime)
# Foreign key
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def get_progress_percentage(self):
"""Get goal progress percentage."""
if self.target_amount == 0:
return 100.0
return min((self.current_amount / self.target_amount) * 100, 100.0)
def get_remaining_amount(self):
"""Get remaining amount to reach goal."""
return max(self.target_amount - self.current_amount, 0)
def get_months_remaining(self):
"""Get months remaining to target date."""
if not self.target_date:
return None
from datetime import date
today = date.today()
if self.target_date <= today:
return 0
# Calculate months between dates
months = (self.target_date.year - today.year) * 12 + (self.target_date.month - today.month)
return max(months, 0)
def get_monthly_savings_needed(self):
"""Get monthly savings needed to reach goal."""
months = self.get_months_remaining()
if months and months > 0:
return self.get_remaining_amount() / months
return 0
class Notification(db.Model):
"""Notification model for alerts."""
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(20), nullable=False) # budget_alert, bill_reminder, goal_progress
title = db.Column(db.String(100), nullable=False)
message = db.Column(db.Text, nullable=False)
is_read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Foreign key
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Related object IDs
budget_id = db.Column(db.Integer, db.ForeignKey('budget.id'))
goal_id = db.Column(db.Integer, db.ForeignKey('financial_goal.id'))
transaction_id = db.Column(db.Integer, db.ForeignKey('transaction.id'))
Step 2: Financial Calculations Engine
Create the core financial logic and calculations.
# app/finance.py
from datetime import datetime, date, timedelta
from typing import Dict, List, Tuple, Optional
from collections import defaultdict
import calendar
from app.models import User, Transaction, Budget, FinancialGoal, Account
from app import db
class FinanceCalculator:
"""Core financial calculations and analysis."""
def __init__(self, user: User):
self.user = user
def get_balance_summary(self) -> Dict:
"""Get comprehensive balance summary."""
accounts = self.user.accounts.all()
summary = {
'total_balance': 0.0,
'accounts': [],
'by_type': defaultdict(float),
'credit_utilization': 0.0,
'total_credit_limit': 0.0,
'total_credit_used': 0.0
}
for account in accounts:
account_data = {
'id': account.id,
'name': account.name,
'type': account.account_type,
'balance': account.balance,
'currency': account.currency,
'is_active': account.is_active
}
if account.account_type == 'credit_card':
account_data['available_credit'] = account.get_available_balance()
account_data['credit_limit'] = account.credit_limit or 0
summary['total_credit_limit'] += account.credit_limit or 0
summary['total_credit_used'] += abs(account.balance)
summary['accounts'].append(account_data)
summary['by_type'][account.account_type] += account.balance
summary['total_balance'] += account.balance
# Calculate credit utilization
if summary['total_credit_limit'] > 0:
summary['credit_utilization'] = (summary['total_credit_used'] / summary['total_credit_limit']) * 100
return summary
def get_income_expense_summary(self, start_date: date = None, end_date: date = None) -> Dict:
"""Get income and expense summary for date range."""
if not start_date:
# Default to current month
today = date.today()
start_date = date(today.year, today.month, 1)
end_date = date(today.year, today.month, calendar.monthrange(today.year, today.month)[1])
# Get transactions in date range
transactions = self.user.transactions.filter(
Transaction.date >= start_date,
Transaction.date <= end_date
).all()
summary = {
'period': {
'start': start_date.isoformat(),
'end': end_date.isoformat()
},
'income': {
'total': 0.0,
'count': 0,
'by_category': defaultdict(float)
},
'expense': {
'total': 0.0,
'count': 0,
'by_category': defaultdict(float)
},
'net': 0.0,
'transactions': len(transactions)
}
for transaction in transactions:
if transaction.type == 'income':
summary['income']['total'] += transaction.amount
summary['income']['count'] += 1
summary['income']['by_category'][transaction.category] += transaction.amount
elif transaction.type == 'expense':
summary['expense']['total'] += transaction.amount
summary['expense']['count'] += 1
summary['expense']['by_category'][transaction.category] += transaction.amount
summary['net'] = summary['income']['total'] - summary['expense']['total']
return summary
def get_monthly_trends(self, months: int = 12) -> Dict:
"""Get spending and income trends for last N months."""
today = date.today()
trends = []
for i in range(months - 1, -1, -1):
# Calculate month date
month_date = today.replace(day=1) - timedelta(days=1)
month_date = month_date.replace(day=1) - timedelta(days=i*30)
month_summary = self.get_income_expense_summary(
date(month_date.year, month_date.month, 1),
date(month_date.year, month_date.month,
calendar.monthrange(month_date.year, month_date.month)[1])
)
trends.append({
'month': month_date.strftime('%Y-%m'),
'month_name': month_date.strftime('%B %Y'),
'income': month_summary['income']['total'],
'expense': month_summary['expense']['total'],
'net': month_summary['net']
})
return {'trends': trends}
def get_category_analysis(self, start_date: date = None, end_date: date = None) -> Dict:
"""Analyze spending by category."""
if not start_date:
# Last 3 months
today = date.today()
start_date = today - timedelta(days=90)
end_date = today
transactions = self.user.transactions.filter(
Transaction.date >= start_date,
Transaction.date <= end_date,
Transaction.type == 'expense'
).all()
category_totals = defaultdict(float)
category_counts = defaultdict(int)
for transaction in transactions:
category_totals[transaction.category] += transaction.amount
category_counts[transaction.category] += 1
# Convert to sorted list
categories = []
total_expenses = sum(category_totals.values())
for category, amount in sorted(category_totals.items(), key=lambda x: x[1], reverse=True):
categories.append({
'name': category,
'amount': amount,
'count': category_counts[category],
'percentage': (amount / total_expenses * 100) if total_expenses > 0 else 0
})
return {
'period': {'start': start_date.isoformat(), 'end': end_date.isoformat()},
'total_expenses': total_expenses,
'categories': categories,
'transaction_count': len(transactions)
}
def get_budget_analysis(self) -> Dict:
"""Analyze budget performance."""
budgets = self.user.budgets.filter_by(is_active=True).all()
analysis = []
for budget in budgets:
budget.update_spent_amount() # Refresh spent amount
analysis.append({
'id': budget.id,
'name': budget.name,
'category': budget.category,
'budgeted': budget.amount,
'spent': budget.spent,
'remaining': budget.get_remaining_amount(),
'percentage': budget.get_spent_percentage(),
'status': 'over_budget' if budget.spent > budget.amount else 'on_track',
'period': f"{budget.start_date} to {budget.end_date}"
})
return {
'budgets': analysis,
'total_budgeted': sum(b.budgeted for b in analysis),
'total_spent': sum(b.spent for b in analysis),
'over_budget_count': sum(1 for b in analysis if b['status'] == 'over_budget')
}
def get_financial_goals_progress(self) -> Dict:
"""Get financial goals progress."""
goals = self.user.goals.all()
progress_data = []
for goal in goals:
progress_data.append({
'id': goal.id,
'name': goal.name,
'target_amount': goal.target_amount,
'current_amount': goal.current_amount,
'remaining': goal.get_remaining_amount(),
'percentage': goal.get_progress_percentage(),
'target_date': goal.target_date.isoformat() if goal.target_date else None,
'months_remaining': goal.get_months_remaining(),
'monthly_needed': goal.get_monthly_savings_needed(),
'is_completed': goal.is_completed,
'priority': goal.priority
})
return {
'goals': progress_data,
'total_goals': len(goals),
'completed_goals': sum(1 for g in progress_data if g['is_completed']),
'total_target': sum(g['target_amount'] for g in progress_data),
'total_saved': sum(g['current_amount'] for g in progress_data)
}
def calculate_financial_health_score(self) -> Dict:
"""Calculate overall financial health score."""
# Get various metrics
balance_summary = self.get_balance_summary()
monthly_summary = self.get_income_expense_summary()
budget_analysis = self.get_budget_analysis()
goals_progress = self.get_financial_goals_progress()
score = 0
max_score = 100
factors = []
# Emergency fund factor (20 points)
emergency_fund_months = 3 # Recommended months
monthly_expenses = monthly_summary['expense']['total']
emergency_fund_needed = monthly_expenses * emergency_fund_months
liquid_assets = balance_summary['by_type']['checking'] + balance_summary['by_type']['savings']
emergency_fund_ratio = min(liquid_assets / emergency_fund_needed, 1.0) if emergency_fund_needed > 0 else 1.0
emergency_score = emergency_fund_ratio * 20
score += emergency_score
factors.append({
'name': 'Emergency Fund',
'score': emergency_score,
'max_score': 20,
'description': f"{emergency_fund_ratio*100:.1f}% of recommended 3-month fund"
})
# Savings rate factor (20 points)
monthly_income = monthly_summary['income']['total']
savings_rate = ((monthly_income - monthly_expenses) / monthly_income * 100) if monthly_income > 0 else 0
target_savings_rate = 20 # 20% target
savings_score = min(savings_rate / target_savings_rate * 20, 20)
score += savings_score
factors.append({
'name': 'Savings Rate',
'score': savings_score,
'max_score': 20,
'description': f"{savings_rate:.1f}% monthly savings rate"
})
# Budget adherence factor (20 points)
budget_score = 20
if budget_analysis['budgets']:
over_budget_count = budget_analysis['over_budget_count']
budget_adherence = 1 - (over_budget_count / len(budget_analysis['budgets']))
budget_score = budget_adherence * 20
score += budget_score
factors.append({
'name': 'Budget Adherence',
'score': budget_score,
'max_score': 20,
'description': f"{budget_analysis['over_budget_count']} budgets over limit"
})
# Debt-to-income ratio factor (20 points)
monthly_debt_payments = 0 # Would need to calculate from transactions
dti_ratio = (monthly_debt_payments / monthly_income * 100) if monthly_income > 0 else 0
target_dti = 36 # 36% target
dti_score = max(0, (1 - dti_ratio/target_dti) * 20) if dti_ratio <= target_dti else 0
score += dti_score
factors.append({
'name': 'Debt-to-Income',
'score': dti_score,
'max_score': 20,
'description': f"{dti_ratio:.1f}% debt-to-income ratio"
})
# Goal progress factor (20 points)
goal_completion_rate = goals_progress['completed_goals'] / goals_progress['total_goals'] if goals_progress['total_goals'] > 0 else 1
goal_score = goal_completion_rate * 20
score += goal_score
factors.append({
'name': 'Goal Achievement',
'score': goal_score,
'max_score': 20,
'description': f"{goals_progress['completed_goals']}/{goals_progress['total_goals']} goals completed"
})
return {
'overall_score': min(score, max_score),
'max_score': max_score,
'grade': self._get_score_grade(score),
'factors': factors,
'recommendations': self._get_score_recommendations(score, factors)
}
def _get_score_grade(self, score: float) -> str:
"""Convert score to letter grade."""
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def _get_score_recommendations(self, score: float, factors: List[Dict]) -> List[str]:
"""Get personalized recommendations based on score."""
recommendations = []
# Emergency fund recommendation
emergency_factor = next((f for f in factors if f['name'] == 'Emergency Fund'), None)
if emergency_factor and emergency_factor['score'] < 15:
recommendations.append("Build an emergency fund covering 3-6 months of expenses")
# Savings recommendation
savings_factor = next((f for f in factors if f['name'] == 'Savings Rate'), None)
if savings_factor and savings_factor['score'] < 15:
recommendations.append("Aim to save at least 20% of your income each month")
# Budget recommendation
budget_factor = next((f for f in factors if f['name'] == 'Budget Adherence'), None)
if budget_factor and budget_factor['score'] < 15:
recommendations.append("Create and stick to a monthly budget for all expense categories")
# General recommendations
if score < 70:
recommendations.append("Track all income and expenses consistently")
recommendations.append("Set specific financial goals with target dates")
recommendations.append("Review your spending patterns monthly")
return recommendations
class BudgetManager:
"""Budget creation and management."""
@staticmethod
def create_monthly_budget(user: User, name: str, amount: float, category: str = None) -> Budget:
"""Create a monthly budget."""
today = date.today()
start_date = date(today.year, today.month, 1)
end_date = date(today.year, today.month, calendar.monthrange(today.year, today.month)[1])
budget = Budget(
name=name,
amount=amount,
period='monthly',
start_date=start_date,
end_date=end_date,
category=category,
user_id=user.id
)
db.session.add(budget)
db.session.commit()
return budget
@staticmethod
def check_budget_alerts(user: User) -> List[Dict]:
"""Check for budget alerts that need to be sent."""
alerts = []
budgets = user.budgets.filter_by(is_active=True).all()
for budget in budgets:
budget.update_spent_amount()
if budget.should_alert():
alerts.append({
'budget_id': budget.id,
'type': 'budget_alert',
'title': f"Budget Alert: {budget.name}",
'message': f"You've spent {budget.get_spent_percentage():.1f}% of your {budget.name} budget (${budget.spent:.2f} of ${budget.amount:.2f})",
'threshold': budget.alert_threshold
})
budget.alert_sent = True
db.session.commit()
return alerts
class GoalManager:
"""Financial goal management."""
@staticmethod
def create_goal(user: User, name: str, target_amount: float,
target_date: date = None, description: str = "") -> FinancialGoal:
"""Create a financial goal."""
goal = FinancialGoal(
name=name,
target_amount=target_amount,
target_date=target_date,
description=description,
user_id=user.id
)
db.session.add(goal)
db.session.commit()
return goal
@staticmethod
def update_goal_progress(goal: FinancialGoal, amount: float):
"""Update goal progress."""
goal.current_amount += amount
if goal.current_amount >= goal.target_amount and not goal.is_completed:
goal.is_completed = True
goal.completed_at = datetime.utcnow()
db.session.commit()
Step 3: Web Application Routes
Create the Flask routes for the web interface.
# app/routes.py
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
from flask_login import login_required, current_user, login_user, logout_user
from datetime import datetime, date, timedelta
from app.forms import LoginForm, RegistrationForm, TransactionForm, AccountForm, BudgetForm, GoalForm
from app.models import User, Account, Transaction, Budget, FinancialGoal, Category, Notification, db
from app.finance import FinanceCalculator, BudgetManager, GoalManager
from app.notifications import NotificationManager
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
@login_required
def dashboard():
"""Main dashboard with financial overview."""
calculator = FinanceCalculator(current_user)
# Get dashboard data
balance_summary = calculator.get_balance_summary()
monthly_summary = calculator.get_income_expense_summary()
budget_analysis = calculator.get_budget_analysis()
goals_progress = calculator.get_financial_goals_progress()
financial_health = calculator.calculate_financial_health_score()
# Get recent transactions
recent_transactions = current_user.transactions.order_by(
Transaction.date.desc()
).limit(10).all()
# Get upcoming bills (transactions with future dates)
upcoming_bills = current_user.transactions.filter(
Transaction.date > date.today(),
Transaction.type == 'expense'
).order_by(Transaction.date).limit(5).all()
return render_template('dashboard.html',
balance_summary=balance_summary,
monthly_summary=monthly_summary,
budget_analysis=budget_analysis,
goals_progress=goals_progress,
financial_health=financial_health,
recent_transactions=recent_transactions,
upcoming_bills=upcoming_bills)
@main_bp.route('/transactions')
@login_required
def transactions():
"""Transaction management page."""
page = request.args.get('page', 1, type=int)
per_page = 20
# Get filter parameters
account_id = request.args.get('account', type=int)
category = request.args.get('category')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
transaction_type = request.args.get('type')
# Build query
query = current_user.transactions
if account_id:
query = query.filter_by(account_id=account_id)
if category:
query = query.filter_by(category=category)
if transaction_type:
query = query.filter_by(type=transaction_type)
if start_date:
query = query.filter(Transaction.date >= datetime.strptime(start_date, '%Y-%m-%d').date())
if end_date:
query = query.filter(Transaction.date <= datetime.strptime(end_date, '%Y-%m-%d').date())
# Paginate results
transactions_paginated = query.order_by(Transaction.date.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
# Get accounts and categories for filters
accounts = current_user.accounts.all()
categories = db.session.query(Transaction.category).filter(
Transaction.user_id == current_user.id
).distinct().all()
categories = [cat[0] for cat in categories if cat[0]]
return render_template('transactions.html',
transactions=transactions_paginated,
accounts=accounts,
categories=categories)
@main_bp.route('/add_transaction', methods=['GET', 'POST'])
@login_required
def add_transaction():
"""Add new transaction."""
form = TransactionForm()
form.account_id.choices = [(a.id, a.name) for a in current_user.accounts.all()]
# Get categories for current user
income_categories = Category.query.filter_by(type='income').all()
expense_categories = Category.query.filter_by(type='expense').all()
if form.validate_on_submit():
transaction = Transaction(
amount=form.amount.data,
type=form.transaction_type.data,
description=form.description.data,
date=form.date.data,
category=form.category.data,
notes=form.notes.data,
user_id=current_user.id,
account_id=form.account_id.data
)
# Set tags if provided
if form.tags.data:
transaction.set_tags_list(form.tags.data.split(','))
db.session.add(transaction)
db.session.commit()
# Update account balance
account = Account.query.get(form.account_id.data)
account.update_balance()
# Update budget if expense
if form.transaction_type.data == 'expense':
BudgetManager.check_budget_alerts(current_user)
# Update goal progress if income
if form.transaction_type.data == 'income':
# Could implement logic to allocate income to goals
pass
flash('Transaction added successfully!', 'success')
return redirect(url_for('main.transactions'))
return render_template('add_transaction.html',
form=form,
income_categories=income_categories,
expense_categories=expense_categories)
@main_bp.route('/accounts')
@login_required
def accounts():
"""Account management page."""
accounts = current_user.accounts.all()
calculator = FinanceCalculator(current_user)
balance_summary = calculator.get_balance_summary()
return render_template('accounts.html',
accounts=accounts,
balance_summary=balance_summary)
@main_bp.route('/add_account', methods=['GET', 'POST'])
@login_required
def add_account():
"""Add new account."""
form = AccountForm()
if form.validate_on_submit():
account = Account(
name=form.name.data,
account_type=form.account_type.data,
balance=form.balance.data,
currency=form.currency.data,
institution=form.institution.data,
user_id=current_user.id
)
# Set credit-specific fields
if form.account_type.data == 'credit_card':
account.credit_limit = form.credit_limit.data
db.session.add(account)
db.session.commit()
flash('Account added successfully!', 'success')
return redirect(url_for('main.accounts'))
return render_template('add_account.html', form=form)
@main_bp.route('/budgets')
@login_required
def budgets():
"""Budget management page."""
calculator = FinanceCalculator(current_user)
budget_analysis = calculator.get_budget_analysis()
return render_template('budgets.html', budget_analysis=budget_analysis)
@main_bp.route('/add_budget', methods=['GET', 'POST'])
@login_required
def add_budget():
"""Add new budget."""
form = BudgetForm()
# Get expense categories for budget
expense_categories = db.session.query(Transaction.category).filter(
Transaction.user_id == current_user.id,
Transaction.type == 'expense'
).distinct().all()
form.category.choices = [('', 'All Expenses')] + [(cat[0], cat[0]) for cat in expense_categories if cat[0]]
if form.validate_on_submit():
budget = BudgetManager.create_monthly_budget(
current_user,
form.name.data,
form.amount.data,
form.category.data or None
)
flash('Budget created successfully!', 'success')
return redirect(url_for('main.budgets'))
return render_template('add_budget.html', form=form)
@main_bp.route('/goals')
@login_required
def goals():
"""Financial goals page."""
calculator = FinanceCalculator(current_user)
goals_progress = calculator.get_financial_goals_progress()
return render_template('goals.html', goals_progress=goals_progress)
@main_bp.route('/add_goal', methods=['GET', 'POST'])
@login_required
def add_goal():
"""Add new financial goal."""
form = GoalForm()
if form.validate_on_submit():
goal = GoalManager.create_goal(
current_user,
form.name.data,
form.target_amount.data,
form.target_date.data,
form.description.data
)
flash('Goal created successfully!', 'success')
return redirect(url_for('main.goals'))
return render_template('add_goal.html', form=form)
@main_bp.route('/reports')
@login_required
def reports():
"""Financial reports page."""
calculator = FinanceCalculator(current_user)
# Get various report data
monthly_trends = calculator.get_monthly_trends(12)
category_analysis = calculator.get_category_analysis()
income_expense_summary = calculator.get_income_expense_summary()
return render_template('reports.html',
monthly_trends=monthly_trends,
category_analysis=category_analysis,
income_expense_summary=income_expense_summary)
@main_bp.route('/api/transactions')
@login_required
def api_transactions():
"""API endpoint for transaction data."""
# Return transaction data for charts
calculator = FinanceCalculator(current_user)
monthly_trends = calculator.get_monthly_trends(12)
return jsonify({
'monthly_trends': monthly_trends['trends'],
'category_analysis': calculator.get_category_analysis()
})
@main_bp.route('/settings', methods=['GET', 'POST'])
@login_required
def settings():
"""User settings page."""
if request.method == 'POST':
# Update user preferences
currency = request.form.get('currency')
theme = request.form.get('theme')
date_format = request.form.get('date_format')
if currency in ['USD', 'EUR', 'GBP', 'JPY']:
current_user.currency = currency
if theme in ['light', 'dark']:
current_user.theme = theme
if date_format:
current_user.date_format = date_format
db.session.commit()
flash('Settings updated successfully!', 'success')
return render_template('settings.html')
# Authentication routes would go here...
Step 4: Data Visualization
Create interactive charts and visualizations.
<!-- templates/dashboard.html -->
{% extends "base.html" %}
{% block title %}Dashboard - FinanceTracker{% endblock %}
{% block head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Financial Health Score -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-line"></i> Financial Health Score
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<div class="score-circle" data-score="{{ financial_health.overall_score }}">
<span class="score-number">{{ "%.0f"|format(financial_health.overall_score) }}</span>
<span class="score-grade">{{ financial_health.grade }}</span>
</div>
<p class="mt-2">Overall Score</p>
</div>
</div>
<div class="col-md-9">
<div class="score-factors">
{% for factor in financial_health.factors %}
<div class="factor-item">
<div class="factor-name">{{ factor.name }}</div>
<div class="progress">
<div class="progress-bar" role="progressbar"
style="width: {{ (factor.score / factor.max_score * 100)|round }}%">
{{ "%.0f"|format(factor.score) }}/{{ factor.max_score }}
</div>
</div>
<small class="text-muted">{{ factor.description }}</small>
</div>
{% endfor %}
</div>
</div>
</div>
{% if financial_health.recommendations %}
<div class="mt-3">
<h6>Recommendations:</h6>
<ul class="text-muted">
{% for rec in financial_health.recommendations %}
<li>{{ rec }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Balance Summary -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-piggy-bank"></i> Account Balances
</h5>
</div>
<div class="card-body">
<div class="balance-grid">
{% for account in balance_summary.accounts %}
<div class="balance-item account-type-{{ account.type }}">
<div class="account-name">{{ account.name }}</div>
<div class="account-balance">${{ "%.2f"|format(account.balance) }}</div>
<div class="account-type">{{ account.type|title|replace('_', ' ') }}</div>
</div>
{% endfor %}
</div>
<div class="total-balance mt-3">
<strong>Total Balance: ${{ "%.2f"|format(balance_summary.total_balance) }}</strong>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-pie"></i> Balance by Type
</h5>
</div>
<div class="card-body">
<canvas id="balanceChart" width="300" height="300"></canvas>
</div>
</div>
</div>
</div>
<!-- Monthly Summary -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-month"></i> This Month's Summary
</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="summary-item income">
<div class="summary-value">${{ "%.2f"|format(monthly_summary.income.total) }}</div>
<div class="summary-label">Income</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-item expense">
<div class="summary-value">${{ "%.2f"|format(monthly_summary.expense.total) }}</div>
<div class="summary-label">Expenses</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-item net">
<div class="summary-value ${{ 'positive' if monthly_summary.net >= 0 else 'negative' }}">
${{ "%.2f"|format(monthly_summary.net) }}
</div>
<div class="summary-label">Net</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-item transactions">
<div class="summary-value">{{ monthly_summary.transactions }}</div>
<div class="summary-label">Transactions</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Budget Overview -->
{% if budget_analysis.budgets %}
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-target"></i> Budget Overview
</h5>
</div>
<div class="card-body">
<div class="budget-grid">
{% for budget in budget_analysis.budgets %}
<div class="budget-item">
<div class="budget-name">{{ budget.name }}</div>
<div class="budget-progress">
<div class="progress">
<div class="progress-bar {{ 'bg-danger' if budget.percentage > 100 else 'bg-warning' if budget.percentage > 80 else 'bg-success' }}"
role="progressbar"
style="width: {{ [budget.percentage, 100]|min }}%">
{{ "%.1f"|format(budget.percentage) }}%
</div>
</div>
<small class="text-muted">
${{ "%.2f"|format(budget.spent) }} of ${{ "%.2f"|format(budget.budgeted) }}
</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Recent Transactions & Goals -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-clock"></i> Recent Transactions
</h5>
</div>
<div class="card-body">
{% if recent_transactions %}
<div class="transaction-list">
{% for transaction in recent_transactions %}
<div class="transaction-item">
<div class="transaction-info">
<div class="transaction-description">{{ transaction.description }}</div>
<small class="text-muted">{{ transaction.date }} • {{ transaction.category }}</small>
</div>
<div class="transaction-amount {{ 'income' if transaction.type == 'income' else 'expense' }}">
{{ '+' if transaction.type == 'income' else '-' }}${{ "%.2f"|format(transaction.amount) }}
</div>
</div>
{% endfor %}
</div>
<div class="text-center mt-3">
<a href="{{ url_for('main.transactions') }}" class="btn btn-outline-primary btn-sm">
View All Transactions
</a>
</div>
{% else %}
<p class="text-muted text-center">No transactions yet.
<a href="{{ url_for('main.add_transaction') }}">Add your first transaction</a>.
</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-bullseye"></i> Financial Goals
</h5>
</div>
<div class="card-body">
{% if goals_progress.goals %}
<div class="goals-list">
{% for goal in goals_progress.goals %}
<div class="goal-item">
<div class="goal-info">
<div class="goal-name">{{ goal.name }}</div>
<div class="goal-progress">
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ goal.percentage }}%">
</div>
</div>
<small class="text-muted">
${{ "%.2f"|format(goal.current_amount) }} / ${{ "%.2f"|format(goal.target_amount) }}
({{ "%.1f"|format(goal.percentage) }}%)
</small>
</div>
</div>
{% if goal.is_completed %}
<div class="goal-status completed">
<i class="fas fa-check-circle"></i>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted text-center">No goals set yet.
<a href="{{ url_for('main.add_goal') }}">Create your first goal</a>.
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% endblock %}
Summary
FinanceTracker represents the culmination of the Python Mastery course:
Complete Technology Stack:
- Flask web framework with application factory pattern
- SQLAlchemy ORM with complex relationships
- User authentication and session management
- RESTful API endpoints
- Interactive data visualizations
- Email notifications and alerts
- Comprehensive database schema
- Business logic layer for financial calculations
Advanced Features:
- Financial health scoring algorithm
- Budget tracking with alerts
- Goal setting and progress monitoring
- Multi-account balance management
- Transaction categorization and tagging
- Comprehensive reporting and analytics
- Responsive web design
- Data export and backup capabilities
Production-Ready Elements:
- Error handling and logging
- Database migrations
- Configuration management
- Security best practices
- Scalable architecture
- Automated testing structure
- Documentation framework
Real-World Application:
- Handles real financial data
- Implements industry-standard practices
- Provides actionable financial insights
- Scales to handle multiple users
- Includes backup and recovery
- Professional user interface
Congratulations! 🎉 You’ve completed Python Mastery!
This capstone project demonstrates mastery of:
- Full-Stack Development - From database to user interface
- Complex Problem Solving - Financial calculations and algorithms
- Professional Architecture - Modular, maintainable code
- Real-World Application - Complete, deployable software
- Industry Best Practices - Security, testing, documentation
Your Python journey has transformed you from beginner to professional developer! 🚀
Next Steps:
- Deploy your FinanceTracker to the cloud
- Add more advanced features (investments, forecasting, etc.)
- Share your project on GitHub and LinkedIn
- Start building your next big project!
- Consider contributing to open-source projects
- Apply for Python developer positions
The world of programming awaits you! 🌟