Skip to content

Commit cae4dfd

Browse files
Merge pull request #1769 from Facelessism/budget-tracker
Improve the transaction management and financial data accuracy
2 parents 0dbacd4 + efca430 commit cae4dfd

1 file changed

Lines changed: 155 additions & 31 deletions

File tree

utilities/Budget-Tracker/budget_tracker.py

Lines changed: 155 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
"""
32
💰 Budget Tracker CLI Tool
43
A command-line budget tracker to manage income, expenses, and view summaries by category.
@@ -9,9 +8,11 @@
98
import os
109
from datetime import datetime
1110
from collections import defaultdict
11+
from decimal import Decimal, InvalidOperation
1212

1313
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "budget_data.csv")
14-
FIELDNAMES = ["date", "type", "category", "description", "amount"]
14+
15+
FIELDNAMES = ["id", "date", "type", "category", "description", "amount"]
1516

1617
# ─────────────────────────────────────────────
1718
# FILE HELPERS
@@ -24,24 +25,59 @@ def initialize_file():
2425
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
2526
writer.writeheader()
2627

27-
2828
def load_transactions():
29-
"""Load all transactions from CSV."""
29+
"""Load all transactions from CSV and automatically migrating older files without IDs."""
3030
transactions = []
31+
3132
with open(DATA_FILE, mode="r", newline="") as f:
3233
reader = csv.DictReader(f)
33-
for row in reader:
34-
row["amount"] = float(row["amount"])
34+
35+
has_id = "id" in (reader.fieldnames or [])
36+
37+
for index, row in enumerate(reader, start=1):
38+
if has_id:
39+
row["id"] = int(row["id"])
40+
else:
41+
row["id"] = index
42+
row["amount"] = Decimal(row["amount"])
3543
transactions.append(row)
36-
return transactions
3744

45+
if transactions and not has_id:
46+
save_transactions(transactions)
47+
48+
return transactions
3849

3950
def save_transaction(entry):
4051
"""Append a single transaction to the CSV."""
4152
with open(DATA_FILE, mode="a", newline="") as f:
4253
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
43-
writer.writerow(entry)
54+
entry_copy = entry.copy()
55+
entry_copy["amount"] = str(entry_copy["amount"])
56+
writer.writerow(entry_copy)
4457

58+
def save_transactions(transactions):
59+
"""Rewrite entire CSV with the current transactions."""
60+
with open(DATA_FILE, "w", newline="") as f:
61+
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
62+
writer.writeheader()
63+
for t in transactions:
64+
row = t.copy()
65+
row["amount"] = str(row["amount"])
66+
writer.writerow(row)
67+
68+
def get_next_id(transactions):
69+
"""Get the next available ID."""
70+
if not transactions:
71+
return 1
72+
else:
73+
return max(t["id"] for t in transactions) + 1
74+
75+
def get_transaction_by_id(transactions, trans_id):
76+
"""Retrieve transaction by ID."""
77+
for t in transactions:
78+
if t["id"] == trans_id:
79+
return t
80+
return None
4581

4682
# ─────────────────────────────────────────────
4783
# CORE FEATURES
@@ -59,26 +95,27 @@ def add_transaction(trans_type):
5995
description = input("Description (optional): ").strip()
6096

6197
try:
62-
amount = float(input("Amount (₹): ").strip())
63-
if amount <= 0:
98+
amount_input = input("Amount (₹): ").strip()
99+
amount = Decimal(amount_input).quantize(Decimal("0.01"))
100+
if amount <= Decimal("0.00"):
64101
print("❌ Amount must be greater than 0.")
65102
return
66-
except ValueError:
103+
except (InvalidOperation, ValueError):
67104
print("❌ Invalid amount. Please enter a number.")
68105
return
69106

107+
transactions = load_transactions()
70108
entry = {
109+
"id": get_next_id(transactions),
71110
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
72111
"type": trans_type,
73112
"category": category.capitalize(),
74113
"description": description,
75-
"amount": round(amount, 2),
114+
"amount": amount
76115
}
77-
78116
save_transaction(entry)
79117
print(f"✅ {trans_type.capitalize()} of ₹{amount:.2f} added under '{category.capitalize()}'.")
80118

81-
82119
def view_summary():
83120
"""Show total income, expenses, and current balance."""
84121
transactions = load_transactions()
@@ -87,8 +124,16 @@ def view_summary():
87124
print("\n📭 No transactions found.")
88125
return
89126

90-
total_income = sum(t["amount"] for t in transactions if t["type"] == "income")
91-
total_expense = sum(t["amount"] for t in transactions if t["type"] == "expense")
127+
total_income = sum(
128+
(t["amount"] for t in transactions if t["type"] == "income"),
129+
Decimal("0.00")
130+
)
131+
132+
total_expense = sum(
133+
(t["amount"] for t in transactions if t["type"] == "expense"),
134+
Decimal("0.00")
135+
)
136+
92137
balance = total_income - total_expense
93138

94139
print("\n" + "═" * 35)
@@ -101,7 +146,6 @@ def view_summary():
101146
print(f" {balance_label} : ₹{abs(balance):>10.2f}")
102147
print("═" * 35)
103148

104-
105149
def view_category_summary():
106150
"""Show spending/income broken down by category."""
107151
transactions = load_transactions()
@@ -110,8 +154,8 @@ def view_category_summary():
110154
print("\n📭 No transactions found.")
111155
return
112156

113-
income_by_cat = defaultdict(float)
114-
expense_by_cat = defaultdict(float)
157+
income_by_cat = defaultdict(lambda: Decimal("0.00"))
158+
expense_by_cat = defaultdict(lambda: Decimal("0.00"))
115159

116160
for t in transactions:
117161
if t["type"] == "income":
@@ -135,7 +179,6 @@ def view_category_summary():
135179

136180
print("═" * 35)
137181

138-
139182
def view_all_transactions():
140183
"""Display all recorded transactions."""
141184
transactions = load_transactions()
@@ -145,19 +188,18 @@ def view_all_transactions():
145188
return
146189

147190
print("\n" + "═" * 70)
148-
print(f" {'DATE':<17} {'TYPE':<10} {'CATEGORY':<15} {'DESCRIPTION':<15} {'AMOUNT':>8}")
191+
print(f" {'ID':<4} {'DATE':<17} {'TYPE':<10} {'CATEGORY':<15} {'DESCRIPTION':<15} {'AMOUNT':>8}")
149192
print("─" * 70)
150193

151194
for t in transactions:
152195
symbol = "+" if t["type"] == "income" else "-"
153196
print(
154-
f" {t['date']:<17} {t['type']:<10} {t['category']:<15} "
197+
f" {t['id']:<4} {t['date']:<17} {t['type']:<10} {t['category']:<15} "
155198
f"{t['description'][:14]:<15} {symbol}{t['amount']:>7.2f}"
156199
)
157200

158201
print("═" * 70)
159202

160-
161203
def delete_all_transactions():
162204
"""Clear all transaction data after confirmation."""
163205
confirm = input("\n⚠️ Are you sure you want to delete ALL data? (yes/no): ").strip().lower()
@@ -169,6 +211,84 @@ def delete_all_transactions():
169211
else:
170212
print("❌ Cancelled.")
171213

214+
def edit_transaction():
215+
"""Edit a transaction, updating in-place."""
216+
transactions = load_transactions()
217+
if not transactions:
218+
print("\n📭 No transactions to edit.")
219+
return
220+
221+
print("\nSelect a transaction to edit by ID:")
222+
for t in transactions:
223+
print(f" {t['id']}: {t['date']} | {t['type']} | {t['category']} | {t['description']} | ₹{t['amount']:.2f}")
224+
225+
try:
226+
trans_id_input = input("Enter Transaction ID: ").strip()
227+
trans_id = int(trans_id_input)
228+
except ValueError:
229+
print("❌ Invalid ID.")
230+
return
231+
232+
transaction = get_transaction_by_id(transactions, trans_id)
233+
if not transaction:
234+
print("❌ Transaction ID not found.")
235+
return
236+
237+
new_type_input = input(f"Type [{transaction['type']}] (income/expense): ").strip().lower()
238+
if new_type_input:
239+
if new_type_input not in ("income", "expense"):
240+
print("❌ Invalid type.")
241+
return
242+
transaction["type"] = new_type_input
243+
244+
new_category = input(f"Category [{transaction['category']}]: ").strip()
245+
if new_category:
246+
transaction['category'] = new_category.capitalize()
247+
248+
new_description = input(f"Description [{transaction['description']}]: ").strip()
249+
if new_description:
250+
transaction['description'] = new_description
251+
252+
new_amount_str = input(f"Amount (₹) [{transaction['amount']}]: ").strip()
253+
if new_amount_str:
254+
try:
255+
new_amount = Decimal(new_amount_str).quantize(Decimal("0.01"))
256+
if new_amount <= Decimal("0.00"):
257+
print("❌ Amount must be greater than 0.")
258+
return
259+
transaction['amount'] = new_amount
260+
except (InvalidOperation, ValueError):
261+
print("❌ Invalid amount.")
262+
return
263+
264+
save_transactions(transactions)
265+
print("✅ Transaction updated successfully.")
266+
267+
def delete_transaction():
268+
"""Delete a specific transaction by ID."""
269+
transactions = load_transactions()
270+
if not transactions:
271+
print("\n📭 No transactions to delete.")
272+
return
273+
274+
print("\nSelect a transaction to delete by ID:")
275+
for t in transactions:
276+
print(f" {t['id']}: {t['date']} | {t['type']} | {t['category']} | {t['description']} | ₹{t['amount']:.2f}")
277+
278+
try:
279+
trans_id_input = input("Enter Transaction ID: ").strip()
280+
trans_id = int(trans_id_input)
281+
except ValueError:
282+
print("❌ Invalid ID.")
283+
return
284+
285+
new_transactions = [t for t in transactions if t["id"] != trans_id]
286+
if len(new_transactions) == len(transactions):
287+
print("❌ Transaction ID not found.")
288+
return
289+
290+
save_transactions(new_transactions)
291+
print("🗑️ Transaction deleted.")
172292

173293
# ─────────────────────────────────────────────
174294
# MAIN MENU
@@ -183,18 +303,19 @@ def print_menu():
183303
print(" 3. 📋 View All Transactions")
184304
print(" 4. 📊 View Summary")
185305
print(" 5. 🗂️ Category-wise Breakdown")
186-
print(" 6. 🗑️ Clear All Data")
187-
print(" 7. 🚪 Exit")
306+
print(" 6. ✏️ Edit a Transaction")
307+
print(" 7. 🗑️ Delete a Transaction")
308+
print(" 8. 🗑️ Clear All Data")
309+
print(" 9. 🚪 Exit")
188310
print("═" * 35)
189311

190-
191312
def main():
192313
initialize_file()
193314
print("\n👋 Welcome to Budget Tracker!")
194315

195316
while True:
196317
print_menu()
197-
choice = input(" Enter your choice (1-7): ").strip()
318+
choice = input(" Enter your choice (1-9): ").strip()
198319

199320
if choice == "1":
200321
add_transaction("income")
@@ -207,13 +328,16 @@ def main():
207328
elif choice == "5":
208329
view_category_summary()
209330
elif choice == "6":
210-
delete_all_transactions()
331+
edit_transaction()
211332
elif choice == "7":
333+
delete_transaction()
334+
elif choice == "8":
335+
delete_all_transactions()
336+
elif choice == "9":
212337
print("\n👋 Goodbye! Keep tracking your budget. 💸\n")
213338
break
214339
else:
215-
print("❌ Invalid choice. Please enter a number between 1 and 7.")
216-
340+
print("❌ Invalid choice. Please enter a number between 1 and 9.")
217341

218342
if __name__ == "__main__":
219-
main()
343+
main()

0 commit comments

Comments
 (0)