Personal Budget Tracker

import json

import os

from datetime import datetime

from pathlib import Path

from collections import defaultdict


try:

    import matplotlib.pyplot as plt

    import matplotlib.patches as mpatches

    MATPLOTLIB_OK = True

except ImportError:

    MATPLOTLIB_OK = False


# ============================================================

# CONFIGURATION

# ============================================================


DATA_FILE    = "budget_data.json"

CURRENCY     = "INR"

CURRENCY_SYM = "₹"


# Default categories

EXPENSE_CATEGORIES = [

    "Food", "Transport", "Rent", "Utilities",

    "Entertainment", "Healthcare", "Shopping",

    "Education", "EMI / Loan", "Other"

]

INCOME_CATEGORIES = [

    "Salary", "Freelance", "Business",

    "Investment", "Gift", "Other"

]


# ============================================================

# LOAD & SAVE DATA

# ============================================================


def load_data():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r") as f:

                return json.load(f)

        except:

            pass

    return {"transactions": [], "budgets": {}}



def save_data(data):

    with open(DATA_FILE, "w") as f:

        json.dump(data, f, indent=2)



# ============================================================

# FORMAT AMOUNT

# ============================================================


def fmt(amount):

    return f"{CURRENCY_SYM}{amount:,.2f}"



# ============================================================

# ADD TRANSACTION

# ============================================================


def add_transaction(data, t_type):

    """Add an income or expense entry."""

    print(f"\n  Add {t_type.upper()}")

    print("  " + "-"*40)


    categories = INCOME_CATEGORIES if t_type == "income" else EXPENSE_CATEGORIES


    print("  Categories:")

    for i, cat in enumerate(categories, 1):

        print(f"  {i:>2}. {cat}")


    try:

        cat_idx = int(input("\n  Choose category (number): ").strip()) - 1

        if cat_idx < 0 or cat_idx >= len(categories):

            print("  Invalid category.")

            return

        category = categories[cat_idx]

    except ValueError:

        print("  Invalid input.")

        return


    try:

        amount = float(input("  Amount: ").strip())

        if amount <= 0:

            print("  Amount must be positive.")

            return

    except ValueError:

        print("  Invalid amount.")

        return


    note = input("  Note (optional): ").strip()


    date_input = input("  Date (DD-MM-YYYY) or Enter for today: ").strip()

    if date_input:

        try:

            date = datetime.strptime(date_input, "%d-%m-%Y").strftime("%d-%m-%Y")

        except ValueError:

            print("  Invalid date format. Using today.")

            date = datetime.now().strftime("%d-%m-%Y")

    else:

        date = datetime.now().strftime("%d-%m-%Y")


    entry = {

        "id":       len(data["transactions"]) + 1,

        "type":     t_type,

        "category": category,

        "amount":   amount,

        "note":     note,

        "date":     date,

        "added_at": datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    }


    data["transactions"].append(entry)

    save_data(data)


    icon = "+" if t_type == "income" else "-"

    print(f"\n  {icon} Saved: {category} — {fmt(amount)} on {date}")

    if note:

        print(f"  Note: {note}")



# ============================================================

# VIEW TRANSACTIONS

# ============================================================


def view_transactions(data, month=None, t_type=None):

    txns = data["transactions"]


    # Filter by month (MM-YYYY)

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    # Filter by type

    if t_type:

        txns = [t for t in txns if t["type"] == t_type]


    if not txns:

        print("\n  No transactions found.")

        return


    # Sort by date descending

    txns_sorted = sorted(txns,

                         key=lambda x: datetime.strptime(x["date"], "%d-%m-%Y"),

                         reverse=True)


    print("\n" + "="*68)

    label = f"{t_type.upper() if t_type else 'ALL'}"

    label += f"  [{month}]" if month else ""

    print(f"  TRANSACTIONS — {label}  ({len(txns)} entries)")

    print("="*68)

    print(f"  {'#':<5} {'DATE':<13} {'TYPE':<10} {'CATEGORY':<16} "

          f"{'AMOUNT':>12}  NOTE")

    print("  " + "-"*63)


    for t in txns_sorted[:50]:    # show last 50

        icon = "+" if t["type"] == "income" else "-"

        note = t.get("note", "")[:20]

        print(f"  {t['id']:<5} {t['date']:<13} {t['type']:<10} "

              f"{t['category']:<16} {icon}{fmt(t['amount']):>12}  {note}")


    if len(txns_sorted) > 50:

        print(f"  ... and {len(txns_sorted) - 50} more. Filter by month to see all.")


    total_in  = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance   = total_in - total_out


    print("  " + "-"*63)

    print(f"  Total Income  : {fmt(total_in)}")

    print(f"  Total Expense : {fmt(total_out)}")

    b_sign = "+" if balance >= 0 else ""

    print(f"  Balance       : {b_sign}{fmt(balance)}")

    print("="*68)



# ============================================================

# MONTHLY SUMMARY

# ============================================================


def monthly_summary(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    # Group by month

    monthly = defaultdict(lambda: {"income": 0, "expense": 0,

                                    "by_cat": defaultdict(float)})

    for t in txns:

        month = t["date"][3:]     # MM-YYYY

        monthly[month][t["type"]] += t["amount"]

        if t["type"] == "expense":

            monthly[month]["by_cat"][t["category"]] += t["amount"]


    # Sort months chronologically

    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    sorted_months = sorted(monthly.keys(), key=month_key)


    print("\n" + "="*60)

    print("  MONTHLY SUMMARY")

    print("="*60)

    print(f"  {'MONTH':<12} {'INCOME':>12} {'EXPENSE':>12} "

          f"{'BALANCE':>12}  STATUS")

    print("  " + "-"*56)


    for month in sorted_months:

        m     = monthly[month]

        bal   = m["income"] - m["expense"]

        sign  = "+" if bal >= 0 else ""

        status = "SURPLUS" if bal >= 0 else "DEFICIT"

        bar    = _mini_bar(m["income"], m["expense"])

        print(f"  {month:<12} {fmt(m['income']):>12} "

              f"{fmt(m['expense']):>12} "

              f"{sign}{fmt(bal):>12}  {status}  {bar}")


    # Overall totals

    total_in  = sum(m["income"]  for m in monthly.values())

    total_out = sum(m["expense"] for m in monthly.values())

    total_bal = total_in - total_out


    print("  " + "-"*56)

    print(f"  {'TOTAL':<12} {fmt(total_in):>12} "

          f"{fmt(total_out):>12} "

          f"{('+' if total_bal>=0 else '')}{fmt(total_bal):>12}")

    print("="*60)



def _mini_bar(income, expense):

    if max(income, expense) == 0:

        return ""

    scale = 12

    i_bar = int((income  / max(income, expense)) * scale)

    e_bar = int((expense / max(income, expense)) * scale)

    return f"I:{'█'*i_bar}  E:{'█'*e_bar}"



# ============================================================

# CATEGORY BREAKDOWN

# ============================================================


def category_breakdown(data, month=None):

    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]


    expenses = [t for t in txns if t["type"] == "expense"]

    incomes  = [t for t in txns if t["type"] == "income"]


    if not expenses and not incomes:

        print("\n  No data for this period.")

        return


    def show_breakdown(items, label):

        cat_totals = defaultdict(float)

        for t in items:

            cat_totals[t["category"]] += t["amount"]

        total = sum(cat_totals.values())

        if not cat_totals:

            return

        sorted_cats = sorted(cat_totals.items(), key=lambda x: x[1], reverse=True)

        max_val = sorted_cats[0][1]


        print(f"\n  {label}  (Total: {fmt(total)})")

        print("  " + "-"*55)

        print(f"  {'CATEGORY':<18} {'AMOUNT':>12}  {'%':>5}  BAR")

        print("  " + "-"*55)

        for cat, amt in sorted_cats:

            pct = (amt / total * 100) if total else 0

            bar = "█" * int((amt / max_val) * 20)

            print(f"  {cat:<18} {fmt(amt):>12}  {pct:>4.1f}%  {bar}")


    title = f"CATEGORY BREAKDOWN"

    if month:

        title += f"  [{month}]"

    print("\n" + "="*58)

    print(f"  {title}")

    print("="*58)


    show_breakdown(expenses, "EXPENSES")

    show_breakdown(incomes,  "INCOME SOURCES")



# ============================================================

# BUDGET GOALS

# ============================================================


def set_budget(data):

    print("\n  SET MONTHLY BUDGET LIMITS")

    print("  Categories:")

    for i, cat in enumerate(EXPENSE_CATEGORIES, 1):

        current = data["budgets"].get(cat, 0)

        current_str = f"  (current: {fmt(current)})" if current else ""

        print(f"  {i:>2}. {cat}{current_str}")


    try:

        idx = int(input("\n  Choose category: ").strip()) - 1

        if idx < 0 or idx >= len(EXPENSE_CATEGORIES):

            print("  Invalid.")

            return

        cat = EXPENSE_CATEGORIES[idx]

        amt = float(input(f"  Budget limit for {cat}: ").strip())

        data["budgets"][cat] = amt

        save_data(data)

        print(f"  Budget set: {cat} = {fmt(amt)}/month")

    except ValueError:

        print("  Invalid input.")



def check_budgets(data):

    budgets = data.get("budgets", {})

    if not budgets:

        print("\n  No budget limits set. Use option to set budgets.")

        return


    # Current month

    this_month = datetime.now().strftime("%m-%Y")

    txns = [t for t in data["transactions"]

            if t["type"] == "expense" and t["date"][3:] == this_month]


    cat_spent = defaultdict(float)

    for t in txns:

        cat_spent[t["category"]] += t["amount"]


    print("\n" + "="*58)

    print(f"  BUDGET STATUS — {this_month}")

    print("="*58)

    print(f"  {'CATEGORY':<18} {'BUDGET':>12} {'SPENT':>12} "

          f"{'LEFT':>12}  STATUS")

    print("  " + "-"*54)


    for cat, budget in sorted(budgets.items()):

        spent = cat_spent.get(cat, 0)

        left  = budget - spent

        pct   = (spent / budget * 100) if budget else 0

        bar   = "█" * min(int(pct / 5), 20)


        if pct >= 100:

            status = "OVER BUDGET!"

        elif pct >= 80:

            status = "WARNING"

        elif pct >= 50:

            status = "ON TRACK"

        else:

            status = "OK"


        print(f"  {cat:<18} {fmt(budget):>12} {fmt(spent):>12} "

              f"{fmt(left):>12}  {status}")

        print(f"  {'':18} {pct:>5.1f}% used  {bar}")


    print("="*58)



# ============================================================

# MATPLOTLIB CHARTS (Optional)

# ============================================================


def plot_monthly_chart(data):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if not txns:

        print("\n  No data to plot.")

        return


    monthly = defaultdict(lambda: {"income": 0, "expense": 0})

    for t in txns:

        month = t["date"][3:]

        monthly[month][t["type"]] += t["amount"]


    def month_key(m):

        try:

            return datetime.strptime(m, "%m-%Y")

        except:

            return datetime.min


    months  = sorted(monthly.keys(), key=month_key)[-12:]

    incomes  = [monthly[m]["income"]  for m in months]

    expenses = [monthly[m]["expense"] for m in months]

    balances = [i - e for i, e in zip(incomes, expenses)]


    x = range(len(months))

    width = 0.35


    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

    fig.suptitle("Personal Budget Overview", fontsize=14, fontweight="bold")


    # Bar chart — income vs expense

    bars1 = ax1.bar([i - width/2 for i in x], incomes,

                    width, label="Income", color="#2ecc71", alpha=0.85)

    bars2 = ax1.bar([i + width/2 for i in x], expenses,

                    width, label="Expense", color="#e74c3c", alpha=0.85)

    ax1.set_title("Monthly Income vs Expense")

    ax1.set_xticks(list(x))

    ax1.set_xticklabels(months, rotation=45, ha="right")

    ax1.set_ylabel(f"Amount ({CURRENCY})")

    ax1.legend()

    ax1.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax1.grid(axis="y", alpha=0.3)


    # Line chart — balance

    colors = ["#2ecc71" if b >= 0 else "#e74c3c" for b in balances]

    ax2.bar(list(x), balances, color=colors, alpha=0.8)

    ax2.axhline(0, color="black", linewidth=0.8, linestyle="--")

    ax2.set_title("Monthly Balance (Surplus / Deficit)")

    ax2.set_xticks(list(x))

    ax2.set_xticklabels(months, rotation=45, ha="right")

    ax2.set_ylabel(f"Balance ({CURRENCY})")

    ax2.yaxis.set_major_formatter(

        plt.FuncFormatter(lambda val, _: f"{CURRENCY_SYM}{val:,.0f}")

    )

    ax2.grid(axis="y", alpha=0.3)


    plt.tight_layout()

    plt.savefig("budget_chart.png", dpi=120, bbox_inches="tight")

    print("\n  Chart saved: budget_chart.png")

    plt.show()



def plot_expense_pie(data, month=None):

    if not MATPLOTLIB_OK:

        print("\n  matplotlib not installed.")

        print("  Install: pip install matplotlib")

        return


    txns = data["transactions"]

    if month:

        txns = [t for t in txns if t["date"][3:] == month]

    expenses = [t for t in txns if t["type"] == "expense"]


    if not expenses:

        print("\n  No expense data to plot.")

        return


    cat_totals = defaultdict(float)

    for t in expenses:

        cat_totals[t["category"]] += t["amount"]


    labels = list(cat_totals.keys())

    values = list(cat_totals.values())

    colors = plt.cm.Set3.colors[:len(labels)]


    fig, ax = plt.subplots(figsize=(9, 7))

    wedges, texts, autotexts = ax.pie(

        values, labels=labels, autopct="%1.1f%%",

        colors=colors, startangle=140,

        wedgeprops={"edgecolor": "white", "linewidth": 1.5}

    )

    for at in autotexts:

        at.set_fontsize(9)


    title = f"Expense Breakdown"

    if month:

        title += f" — {month}"

    ax.set_title(title, fontsize=13, fontweight="bold")


    total = sum(values)

    ax.text(0, -1.3, f"Total: {fmt(total)}",

            ha="center", fontsize=11, color="#333")


    plt.tight_layout()

    plt.savefig("expense_pie.png", dpi=120, bbox_inches="tight")

    print("\n  Pie chart saved: expense_pie.png")

    plt.show()



# ============================================================

# DELETE TRANSACTION

# ============================================================


def delete_transaction(data):

    try:

        tid = int(input("\n  Enter transaction ID to delete: ").strip())

    except ValueError:

        print("  Invalid ID.")

        return


    for i, t in enumerate(data["transactions"]):

        if t["id"] == tid:

            confirm = input(f"  Delete: {t['type']} {fmt(t['amount'])} "

                            f"— {t['category']} on {t['date']}? (y/n): ").strip().lower()

            if confirm == "y":

                data["transactions"].pop(i)

                save_data(data)

                print("  Deleted.")

            else:

                print("  Cancelled.")

            return


    print(f"  Transaction ID {tid} not found.")



# ============================================================

# QUICK STATS

# ============================================================


def quick_stats(data):

    txns = data["transactions"]

    if not txns:

        print("\n  No data yet.")

        return


    this_month = datetime.now().strftime("%m-%Y")

    this_txns  = [t for t in txns if t["date"][3:] == this_month]


    total_in   = sum(t["amount"] for t in txns if t["type"] == "income")

    total_out  = sum(t["amount"] for t in txns if t["type"] == "expense")

    balance    = total_in - total_out


    month_in   = sum(t["amount"] for t in this_txns if t["type"] == "income")

    month_out  = sum(t["amount"] for t in this_txns if t["type"] == "expense")

    month_bal  = month_in - month_out


    print("\n" + "="*50)

    print("  QUICK STATS")

    print("="*50)

    print(f"  This Month ({this_month}):")

    print(f"    Income   : {fmt(month_in)}")

    print(f"    Expense  : {fmt(month_out)}")

    b = "+" if month_bal >= 0 else ""

    print(f"    Balance  : {b}{fmt(month_bal)}")

    print(f"\n  All Time:")

    print(f"    Income   : {fmt(total_in)}")

    print(f"    Expense  : {fmt(total_out)}")

    b = "+" if balance >= 0 else ""

    print(f"    Balance  : {b}{fmt(balance)}")

    print(f"    Entries  : {len(txns)}")

    print("="*50)



# ============================================================

# MAIN MENU

# ============================================================


def print_menu():

    print("\n" + "-"*48)

    print("  PERSONAL BUDGET TRACKER")

    print("-"*48)

    print("  1.  Add income")

    print("  2.  Add expense")

    print("  3.  View transactions")

    print("  4.  Monthly summary")

    print("  5.  Category breakdown")

    print("  6.  Budget limits & status")

    print("  7.  Set budget limit for a category")

    print("  8.  Plot monthly income vs expense chart")

    print("  9.  Plot expense pie chart")

    print("  10. Quick stats")

    print("  11. Delete a transaction")

    print("  0.  Exit")

    print("-"*48)



def main():

    print("\n" + "="*55)

    print("     PERSONAL BUDGET TRACKER")

    print("="*55)


    data = load_data()

    count = len(data["transactions"])

    if count:

        print(f"\n  Loaded {count} transaction(s) from {DATA_FILE}")

    else:

        print(f"\n  No data yet. Start by adding income or expenses.")


    while True:

        print_menu()

        choice = input("  > ").strip()


        if choice == "1":

            add_transaction(data, "income")


        elif choice == "2":

            add_transaction(data, "expense")


        elif choice == "3":

            print("\n  Filter options:")

            print("  1. All transactions")

            print("  2. By month")

            print("  3. Income only")

            print("  4. Expenses only")

            sub = input("  Choice: ").strip()

            if sub == "1":

                view_transactions(data)

            elif sub == "2":

                month = input("  Month (MM-YYYY e.g. 04-2026): ").strip()

                view_transactions(data, month=month)

            elif sub == "3":

                view_transactions(data, t_type="income")

            elif sub == "4":

                view_transactions(data, t_type="expense")


        elif choice == "4":

            monthly_summary(data)


        elif choice == "5":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            category_breakdown(data, month=month or None)


        elif choice == "6":

            check_budgets(data)


        elif choice == "7":

            set_budget(data)


        elif choice == "8":

            plot_monthly_chart(data)


        elif choice == "9":

            month = input("\n  Month (MM-YYYY) or Enter for all time: ").strip()

            plot_expense_pie(data, month=month or None)


        elif choice == "10":

            quick_stats(data)


        elif choice == "11":

            view_transactions(data)

            delete_transaction(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

No comments: