Complete Project 3: Expense Tracker

This project combines file handling (CSV/JSON), data analysis, reporting, and user interface design. We'll create a comprehensive expense tracking system with features like expense categorization, monthly summaries, budgeting, data visualization, and export capabilities.

# EXPENSE TRACKER APPLICATION
import json
import csv
import os
from datetime import datetime, timedelta
from collections import defaultdict

print("EXPENSE TRACKER")
print("=" * 50)

class ExpenseTracker:
    """A comprehensive expense tracking application"""
    
    def __init__(self, data_file="expenses.json"):
        self.data_file = data_file
        self.expenses = self.load_expenses()
        self.categories = [
            "Food", "Transportation", "Housing", "Utilities", 
            "Entertainment", "Healthcare", "Shopping", "Education", 
            "Travel", "Other"
        ]
        self.budget = self.load_budget()
    
    def load_expenses(self):
        """Load expenses from JSON file"""
        if os.path.exists(self.data_file):
            try:
                with open(self.data_file, 'r') as file:
                    return json.load(file)
            except (json.JSONDecodeError, IOError):
                print("Warning: Could not load expenses. Starting fresh.")
                return []
        return []
    
    def save_expenses(self):
        """Save expenses to JSON file"""
        try:
            with open(self.data_file, 'w') as file:
                json.dump(self.expenses, file, indent=2)
        except IOError:
            print("Error: Could not save expenses.")
    
    def load_budget(self):
        """Load budget from file"""
        budget_file = "budget.json"
        if os.path.exists(budget_file):
            try:
                with open(budget_file, 'r') as file:
                    return json.load(file)
            except (json.JSONDecodeError, IOError):
                return {category: 0 for category in self.categories}
        return {category: 0 for category in self.categories}
    
    def save_budget(self):
        """Save budget to file"""
        try:
            with open("budget.json", 'w') as file:
                json.dump(self.budget, file, indent=2)
        except IOError:
            print("Error: Could not save budget.")
    
    def display_menu(self):
        """Display main menu"""
        print("\n" + "šŸ’µ" * 20)
        print("EXPENSE TRACKER MENU")
        print("šŸ’µ" * 20)
        print("1. Add New Expense")
        print("2. View All Expenses")
        print("3. View Expenses by Category")
        print("4. View Expenses by Month")
        print("5. Set/View Budget")
        print("6. Generate Reports")
        print("7. Search Expenses")
        print("8. Export Data")
        print("9. Delete Expense")
        print("10. Exit")
        print("-" * 40)
        
        # Show quick summary
        if self.expenses:
            today = datetime.now().date()
            month_start = today.replace(day=1)
            month_expenses = [e for e in self.expenses 
                             if datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start]
            
            if month_expenses:
                total_month = sum(e["amount"] for e in month_expenses)
                print(f"This month's spending: ${total_month:.2f}")
    
    def add_expense(self):
        """Add a new expense"""
        print("\n" + "-" * 40)
        print("ADD NEW EXPENSE")
        print("-" * 40)
        
        # Get expense details
        description = input("Description: ").strip()
        if not description:
            print("Description cannot be empty!")
            return
        
        amount = self.get_amount()
        category = self.get_category()
        date = self.get_date()
        payment_method = input("Payment method (cash/card/online): ").strip().lower() or "card"
        
        # Create expense record
        expense = {
            "id": len(self.expenses) + 1,
            "description": description,
            "amount": amount,
            "category": category,
            "date": date,
            "payment_method": payment_method,
            "added_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
        
        self.expenses.append(expense)
        self.save_expenses()
        
        print(f"\nāœ… Expense added successfully!")
        print(f"   {description}: ${amount:.2f} under {category}")
        
        # Check if over budget for this category
        self.check_budget_alert(category, amount)
    
    def get_amount(self):
        """Get amount with validation"""
        while True:
            try:
                amount = float(input("Amount: $").strip())
                if amount <= 0:
                    print("Amount must be greater than 0")
                    continue
                return amount
            except ValueError:
                print("Please enter a valid number")
    
    def get_category(self):
        """Get category with validation"""
        print("\nCategories:")
        for i, category in enumerate(self.categories, 1):
            print(f"  {i}. {category}")
        
        while True:
            try:
                choice = input("\nSelect category (1-{}): ".format(len(self.categories))).strip()
                if not choice:
                    return "Other"
                
                idx = int(choice) - 1
                if 0 <= idx < len(self.categories):
                    return self.categories[idx]
                else:
                    print(f"Please enter 1-{len(self.categories)}")
            except ValueError:
                print("Please enter a valid number")
    
    def get_date(self):
        """Get date with validation"""
        while True:
            date_str = input("Date (YYYY-MM-DD, Enter for today): ").strip()
            if not date_str:
                return datetime.now().strftime("%Y-%m-%d")
            
            try:
                datetime.strptime(date_str, "%Y-%m-%d")
                return date_str
            except ValueError:
                print("Invalid date format. Please use YYYY-MM-DD")
    
    def view_all_expenses(self, expenses=None):
        """View all expenses"""
        if expenses is None:
            expenses = self.expenses
        
        if not expenses:
            print("\nNo expenses found.")
            return
        
        # Sort by date (newest first)
        sorted_expenses = sorted(expenses, 
                                key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"), 
                                reverse=True)
        
        print("\n" + "=" * 80)
        print(f"ALL EXPENSES ({len(sorted_expenses)} total)")
        print("=" * 80)
        print(f"{'Date':12} {'Category':15} {'Description':25} {'Amount':10} {'Payment':10}")
        print("-" * 80)
        
        total = 0
        for expense in sorted_expenses:
            date = expense["date"]
            category = expense["category"]
            description = expense["description"][:23] + "..." if len(expense["description"]) > 23 else expense["description"]
            amount = expense["amount"]
            payment = expense["payment_method"]
            
            print(f"{date:12} {category:15} {description:25} ${amount:8.2f} {payment:10}")
            total += amount
        
        print("-" * 80)
        print(f"{'TOTAL':54} ${total:8.2f}")
        print("=" * 80)
    
    def view_by_category(self):
        """View expenses grouped by category"""
        if not self.expenses:
            print("\nNo expenses found.")
            return
        
        # Group by category
        category_totals = defaultdict(float)
        category_expenses = defaultdict(list)
        
        for expense in self.expenses:
            category = expense["category"]
            category_totals[category] += expense["amount"]
            category_expenses[category].append(expense)
        
        print("\n" + "=" * 60)
        print("EXPENSES BY CATEGORY")
        print("=" * 60)
        
        grand_total = 0
        for category in sorted(category_totals.keys()):
            total = category_totals[category]
            count = len(category_expenses[category])
            percentage = (total / sum(category_totals.values()) * 100) if sum(category_totals.values()) > 0 else 0
            
            print(f"\nšŸ“ {category.upper()}:")
            print(f"   Total: ${total:.2f} ({percentage:.1f}%)")
            print(f"   Count: {count} expense(s)")
            
            # Show bar chart
            max_bar_length = 30
            bar_length = int((percentage / 100) * max_bar_length)
            bar = "ā–ˆ" * bar_length
            print(f"   {bar} {percentage:.1f}%")
            
            # Show top 3 expenses in this category
            sorted_category = sorted(category_expenses[category], 
                                   key=lambda x: x["amount"], 
                                   reverse=True)[:3]
            for exp in sorted_category:
                print(f"      • {exp['description'][:20]}: ${exp['amount']:.2f} ({exp['date']})")
            
            grand_total += total
        
        print("\n" + "=" * 60)
        print(f"GRAND TOTAL: ${grand_total:.2f}")
        print("=" * 60)
    
    def view_by_month(self):
        """View expenses grouped by month"""
        if not self.expenses:
            print("\nNo expenses found.")
            return
        
        # Group by month
        month_totals = defaultdict(float)
        month_expenses = defaultdict(list)
        
        for expense in self.expenses:
            date_obj = datetime.strptime(expense["date"], "%Y-%m-%d")
            month_key = date_obj.strftime("%Y-%m")
            month_totals[month_key] += expense["amount"]
            month_expenses[month_key].append(expense)
        
        print("\n" + "=" * 60)
        print("EXPENSES BY MONTH")
        print("=" * 60)
        
        for month in sorted(month_totals.keys(), reverse=True):
            total = month_totals[month]
            count = len(month_expenses[month])
            
            # Format month name
            month_obj = datetime.strptime(month + "-01", "%Y-%m-%d")
            month_name = month_obj.strftime("%B %Y")
            
            print(f"\nšŸ“… {month_name}:")
            print(f"   Total: ${total:.2f}")
            print(f"   Count: {count} expense(s)")
            
            # Show categories for this month
            month_cats = defaultdict(float)
            for exp in month_expenses[month]:
                month_cats[exp["category"]] += exp["amount"]
            
            # Show top 3 categories
            sorted_cats = sorted(month_cats.items(), key=lambda x: x[1], reverse=True)[:3]
            for cat, amount in sorted_cats:
                percentage = (amount / total * 100) if total > 0 else 0
                print(f"      • {cat}: ${amount:.2f} ({percentage:.1f}%)")
        
        print("\n" + "=" * 60)
    
    def manage_budget(self):
        """Set and view budget"""
        print("\n" + "=" * 50)
        print("BUDGET MANAGEMENT")
        print("=" * 50)
        
        print("\nCurrent Budget:")
        print("-" * 40)
        total_budget = 0
        for category in self.categories:
            budget_amount = self.budget.get(category, 0)
            if budget_amount > 0:
                print(f"{category:15}: ${budget_amount:.2f}")
                total_budget += budget_amount
        
        if total_budget == 0:
            print("No budget set yet.")
        else:
            print(f"\nTotal Monthly Budget: ${total_budget:.2f}")
        
        print("\nOptions:")
        print("1. Set budget for a category")
        print("2. View budget vs actual")
        print("3. Clear all budgets")
        print("4. Back to main menu")
        
        choice = input("\nEnter choice: ").strip()
        
        if choice == "1":
            print("\nSelect category to set budget:")
            for i, category in enumerate(self.categories, 1):
                current = self.budget.get(category, 0)
                print(f"  {i}. {category}: ${current:.2f}")
            
            try:
                cat_idx = int(input("\nCategory number: ")) - 1
                if 0 <= cat_idx < len(self.categories):
                    category = self.categories[cat_idx]
                    amount = float(input(f"Monthly budget for {category}: $"))
                    self.budget[category] = amount
                    self.save_budget()
                    print(f"āœ… Budget for {category} set to ${amount:.2f}")
                else:
                    print("Invalid category number.")
            except ValueError:
                print("Please enter a valid number.")
        
        elif choice == "2":
            self.show_budget_vs_actual()
        
        elif choice == "3":
            confirm = input("Are you sure you want to clear all budgets? (y/n): ")
            if confirm.lower() == 'y':
                self.budget = {category: 0 for category in self.categories}
                self.save_budget()
                print("āœ… All budgets cleared.")
        
        elif choice == "4":
            return
        
        else:
            print("Invalid choice.")
    
    def check_budget_alert(self, category, amount):
        """Check if expense exceeds budget"""
        budget_amount = self.budget.get(category, 0)
        if budget_amount == 0:
            return
        
        # Calculate monthly total for this category
        today = datetime.now().date()
        month_start = today.replace(day=1)
        
        monthly_total = sum(
            e["amount"] for e in self.expenses 
            if e["category"] == category and 
            datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start
        )
        
        if monthly_total > budget_amount:
            print(f"\n⚠  BUDGET ALERT: You've exceeded your {category} budget!")
            print(f"   Budget: ${budget_amount:.2f}, Spent: ${monthly_total:.2f}")
            print(f"   Over budget by: ${monthly_total - budget_amount:.2f}")
        elif monthly_total >= budget_amount * 0.8:  # 80% of budget
            print(f"\n⚠  BUDGET WARNING: You've used {monthly_total/budget_amount*100:.0f}% of your {category} budget")
    
    def show_budget_vs_actual(self):
        """Show budget vs actual spending"""
        if not any(self.budget.values()):
            print("\nNo budget set yet.")
            return
        
        today = datetime.now().date()
        month_start = today.replace(day=1)
        
        print("\n" + "=" * 60)
        print("BUDGET VS ACTUAL (THIS MONTH)")
        print("=" * 60)
        
        print(f"\n{'Category':15} {'Budget':10} {'Spent':10} {'Remaining':12} {'% Used':8}")
        print("-" * 60)
        
        total_budget = 0
        total_spent = 0
        
        for category in sorted(self.budget.keys()):
            budget = self.budget.get(category, 0)
            if budget == 0:
                continue
            
            # Calculate monthly spending for this category
            monthly_spent = sum(
                e["amount"] for e in self.expenses 
                if e["category"] == category and 
                datetime.strptime(e["date"], "%Y-%m-%d").date() >= month_start
            )
            
            remaining = budget - monthly_spent
            percent_used = (monthly_spent / budget * 100) if budget > 0 else 0
            
            status = "āœ…" if percent_used <= 100 else "⚠"
            
            print(f"{category:15} ${budget:8.2f} ${monthly_spent:8.2f} ${remaining:10.2f} {percent_used:7.1f}% {status}")
            
            total_budget += budget
            total_spent += monthly_spent
        
        print("-" * 60)
        total_remaining = total_budget - total_spent
        total_percent = (total_spent / total_budget * 100) if total_budget > 0 else 0
        
        print(f"{'TOTAL':15} ${total_budget:8.2f} ${total_spent:8.2f} ${total_remaining:10.2f} {total_percent:7.1f}%")
        print("=" * 60)
        
        if total_percent > 100:
            print("\n⚠  You have exceeded your total monthly budget!")
        elif total_percent > 80:
            print("\n⚠  You have used more than 80% of your monthly budget")
    
    def generate_reports(self):
        """Generate various reports"""
        if not self.expenses:
            print("\nNo expenses to generate reports.")
            return
        
        print("\n" + "=" * 50)
        print("REPORTS")
        print("=" * 50)
        
        print("\nAvailable Reports:")
        print("1. Monthly Summary Report")
        print("2. Category Analysis Report")
        print("3. Spending Trends (Last 6 Months)")
        print("4. Top Expenses Report")
        print("5. Payment Method Analysis")
        
        try:
            choice = input("\nSelect report (1-5): ").strip()
            
            if choice == "1":
                self.monthly_summary_report()
            elif choice == "2":
                self.category_analysis_report()
            elif choice == "3":
                self.spending_trends_report()
            elif choice == "4":
                self.top_expenses_report()
            elif choice == "5":
                self.payment_method_report()
            else:
                print("Invalid choice.")
        except Exception as e:
            print(f"Error generating report: {e}")
    
    def monthly_summary_report(self):
        """Generate monthly summary report"""
        today = datetime.now().date()
        current_month = today.strftime("%Y-%m")
        
        # Get current month expenses
        month_expenses = [e for e in self.expenses 
                         if e["date"].startswith(current_month)]
        
        if not month_expenses:
            print(f"\nNo expenses for {current_month}")
            return
        
        total = sum(e["amount"] for e in month_expenses)
        avg_per_day = total / today.day if today.day > 0 else total
        
        print(f"\nšŸ“Š MONTHLY SUMMARY REPORT - {current_month}")
        print("=" * 60)
        print(f"Total Expenses: ${total:.2f}")
        print(f"Number of Expenses: {len(month_expenses)}")
        print(f"Average per Day: ${avg_per_day:.2f}")
        
        # Projected monthly total
        days_in_month = 30  # Simplified
        projected_total = (total / today.day * days_in_month) if today.day > 0 else total
        print(f"Projected Monthly Total: ${projected_total:.2f}")
        
        # Compare with previous month
        prev_month_date = today.replace(day=1) - timedelta(days=1)
        prev_month = prev_month_date.strftime("%Y-%m")
        prev_month_expenses = [e for e in self.expenses 
                              if e["date"].startswith(prev_month)]
        
        if prev_month_expenses:
            prev_total = sum(e["amount"] for e in prev_month_expenses)
            change = total - prev_total
            change_percent = (change / prev_total * 100) if prev_total > 0 else 0
            
            change_symbol = "ā–²" if change > 0 else "ā–¼"
            print(f"\nComparison with {prev_month}:")
            print(f"  Previous Month: ${prev_total:.2f}")
            print(f"  Change: {change_symbol}${abs(change):.2f} ({change_percent:+.1f}%)")
        
        print("=" * 60)
    
    def search_expenses(self):
        """Search expenses by keyword"""
        if not self.expenses:
            print("\nNo expenses to search.")
            return
        
        keyword = input("\nSearch keyword (description/category): ").lower().strip()
        if not keyword:
            print("Please enter a keyword.")
            return
        
        results = []
        for expense in self.expenses:
            if (keyword in expense["description"].lower() or 
                keyword in expense["category"].lower()):
                results.append(expense)
        
        if results:
            print(f"\nFound {len(results)} expense(s) matching '{keyword}':")
            self.view_all_expenses(results)
        else:
            print(f"\nNo expenses found matching '{keyword}'.")
    
    def export_data(self):
        """Export data to CSV"""
        if not self.expenses:
            print("\nNo expenses to export.")
            return
        
        filename = f"expenses_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        
        with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
            fieldnames = ['id', 'date', 'description', 'amount', 'category', 'payment_method', 'added_date']
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            
            writer.writeheader()
            for expense in self.expenses:
                writer.writerow(expense)
        
        print(f"\nāœ… Expenses exported to {filename}")
        print(f"Total records: {len(self.expenses)}")
    
    def delete_expense(self):
        """Delete an expense"""
        if not self.expenses:
            print("\nNo expenses to delete.")
            return
        
        self.view_all_expenses()
        
        try:
            expense_id = int(input("\nEnter expense ID to delete: "))
            for i, expense in enumerate(self.expenses):
                if expense["id"] == expense_id:
                    confirm = input(f"Delete '{expense['description']}' (${expense['amount']})? (y/n): ")
                    if confirm.lower() == 'y':
                        deleted = self.expenses.pop(i)
                        self.save_expenses()
                        print(f"\nāœ… Expense deleted: {deleted['description']}")
                        # Reassign IDs
                        for j, exp in enumerate(self.expenses, 1):
                            exp["id"] = j
                        self.save_expenses()
                    return
            print("\nāŒ Expense not found.")
        except ValueError:
            print("\nāŒ Please enter a valid number.")
    
    def run(self):
        """Run the expense tracker"""
        print("\nšŸ’° Welcome to Expense Tracker!")
        print("Track your expenses and manage your budget.")
        
        while True:
            self.display_menu()
            
            try:
                choice = input("\nEnter your choice (1-10): ").strip()
                
                if choice == "1":
                    self.add_expense()
                elif choice == "2":
                    self.view_all_expenses()
                elif choice == "3":
                    self.view_by_category()
                elif choice == "4":
                    self.view_by_month()
                elif choice == "5":
                    self.manage_budget()
                elif choice == "6":
                    self.generate_reports()
                elif choice == "7":
                    self.search_expenses()
                elif choice == "8":
                    self.export_data()
                elif choice == "9":
                    self.delete_expense()
                elif choice == "10":
                    print("\nšŸ‘‹ Thank you for using Expense Tracker!")
                    print("Your financial data has been saved.")
                    break
                else:
                    print("\nāŒ Invalid choice. Please enter 1-10.")
            except KeyboardInterrupt:
                print("\n\nšŸ‘‹ Goodbye!")
                break
            except Exception as e:
                print(f"\nāŒ An error occurred: {e}")

# Run the application
if __name__ == "__main__":
    tracker = ExpenseTracker()
    tracker.run()