1-
21"""
32💰 Budget Tracker CLI Tool
43A command-line budget tracker to manage income, expenses, and view summaries by category.
98import os
109from datetime import datetime
1110from collections import defaultdict
11+ from decimal import Decimal , InvalidOperation
1212
1313DATA_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-
2828def 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
3950def 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-
82119def 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-
105149def 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-
139182def 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-
161203def 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 ("\n Select 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 ("\n Select 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-
191312def 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
218342if __name__ == "__main__" :
219- main ()
343+ main ()
0 commit comments