Welcome to my personal blog

Python Bill Manager

Published on
8 min read
← Back to the blog
Authors

Bill Manager Tutorial

Introduction


The Bill Manager is a simple command-line program that allows users to manage their bills. This tutorial will walk you through each line of code, explaining what it does and how it contributes to the overall functionality of the program.

This is the full code of the app. Underneath you'll find an explantion of what every function does.

import json
import os
from datetime import datetime
from time import sleep
from collections import defaultdict
import curses

file_name = "data.json"
os.system("cls" if os.name == "nt" else "clear")

category_name = "categories.json"

def load_event_themes():
    with open(category_name, "r") as file:
        data = json.load(file)
        return data["categories"]

event_theme_options = load_event_themes()

def format_event_date(event_date):
    return datetime.strptime(event_date, "%a %b %d %Y").strftime("%m/%d/%Y")

def validate_date(date_str):
    try:
        dt = datetime.strptime(date_str, "%m/%d/%Y")
        return dt.strftime("%a %b %d %Y")
    except ValueError:
        return None


def load_bills():
    try:
        with open(file_name, "r") as file:
            return json.load(file)
    except:
        return {"bills":[]}

def save_bills(bills):
    try:
        with open(file_name, "w") as file:
            json.dump(bills, file, indent=4)
    except:
        print("Failed to save.")

def group_paid_bills_by_theme(bills):
    grouped_totals = defaultdict(float)
    longest_name = max(len(bill['event_theme']) for bill in bills['bills'])

    for bill in bills['bills']:
        if bill["paid"] == 1:
            grouped_totals[bill["event_theme"]] += bill["amount"]

    print("\033[1;32mTotal Amount for Paid Bills:\033[0m\n")
    for theme, total in grouped_totals.items():
        print(f"{theme.ljust(longest_name)}: ${total:.2f}")

def view_bills(bills):
    os.system("cls" if os.name == "nt" else "clear")
    bills_list = sorted(bills["bills"], key=lambda bill: datetime.strptime(bill["event_date"], "%a %b %d %Y"), reverse=True)

    if not bills_list:
        print('\033[1;32mNo Bills to display!\033[0m')
        input("\nPress Enter to return to the menu...")
    else:
        print("\033[1;32mYour bills:\033[0m\n")

        longest_name = max(len(bill["event_theme"]) for bill in bills_list)

        for bill in bills_list:
            status = "[Paid]" if bill["paid"] == 1 else "      "
            print(f"- {format_event_date(bill['event_date'])} - {bill['event_theme'].ljust(longest_name)} {status} ${bill['amount']}")

        input("\nPress Enter to return to the menu...")
        os.system("cls" if os.name == "nt" else "clear")

def view_unpaid_bills(bills):
    os.system("cls" if os.name == "nt" else "clear")
    bills_list = sorted(bills["bills"], key=lambda bill: datetime.strptime(bill["event_date"], "%a %b %d %Y"), reverse=True)

    if len(bills_list) == 0:
        print('\033[1;32mNo Bills to display!\033[0m')
        input("\nPress Enter to return to the menu...")
    else:
        print("\033[1;32mYour UNPAID bills:\033[0m\n")
        
        longest_name = max(len(bill['event_theme']) for bill in bills_list)

        for idx, bill in enumerate(bills_list):
            if bill['paid'] != 1:
                status = "βœ…" if bill['paid'] == 1 else ""
                print(f"{idx + 1}. {format_event_date(bill['event_date'])} - {bill['event_theme'].ljust(longest_name)} {status} ${bill['amount']}")

def add_bills(bills):
    os.system("cls" if os.name == "nt" else "clear")
    print("\033[1;32mπŸ’° Add a New Bill πŸ’³\033[0m\n")

    while True:
        event_date_input = input("Enter the event date (MM/DD/YYYY): ").strip()
        event_date = validate_date(event_date_input)
        if event_date:
            break
        print("Invalid date format. Please enter a valid date in MM/DD/YYYY format.")

    print("\nSelect an event theme:")
    for idx, option in enumerate(event_theme_options, start=1):
        print(f"{idx}. {option}")
    
    while True:
        try:
            theme_choice = int(input("\nEnter the number corresponding to the event theme: "))
            if 1 <= theme_choice <= len(event_theme_options):
                event_theme = event_theme_options[theme_choice - 1]
                break
            print("Invalid choice. Please select a valid number.")
        except ValueError:
            print("Enter a valid number.")

    paid = int(input("Is the bill paid? (1 for Yes, 0 for No): ").strip())
    amount = float(input("Enter the amount: ").strip())

    icons = {
        "Rent": "fas fa-home",
        "Car Insurance": "fas fa-bolt",
        "Internet": "fas fa-network-wired",
        "Groceries": "fas fa-shopping-cart"
    }
    icon = icons.get(event_theme, "fas fa-file-invoice")

    new_bill = {
        "event_date": event_date,
        "event_theme": event_theme,
        "paid": paid,
        "amount": amount,
        "icon": icon
    }

    bills["bills"].append(new_bill)

    try:
        with open(file_name, "w") as file:
            json.dump(bills, file, indent=4)
        print("\nill added successfully!")
    except:
        print("Failed to save the bill.")

    input("\nPress Enter to return to the menu...")
    os.system("cls" if os.name == "nt" else "clear")

def delete_bills(bills):
    os.system("cls" if os.name == "nt" else "clear")
    print("\033[1;32m Delete a Bill\033[0m")

    if not bills["bills"]:
        print("No bills to delete.")
        input("\nPress Enter to return to the menu...")
        os.system("cls" if os.name == "nt" else "clear")
        return

    indexed_bills = sorted(
        [(idx, bill) for idx, bill in enumerate(bills["bills"])],
        key=lambda item: datetime.strptime(item[1]["event_date"], "%a %b %d %Y"),
        reverse=True
    )

    longest_name = max(len(bill["event_theme"]) for _, bill in indexed_bills)

    print("\nSelect a bill to delete:\n")
    for display_idx, (original_idx, bill) in enumerate(indexed_bills, start=1):
        status = "[Paid]" if bill["paid"] == 1 else "      "
        print(f"{display_idx}. {format_event_date(bill['event_date'])} - {bill['event_theme'].ljust(longest_name)} {status} ${bill['amount']}")

    while True:
        try:
            choice = input("\nEnter the number of the bill to delete (or 'b' to go back): ").strip()
            if choice.lower() == "b":
                print("Returning to main menu...")
                os.system("cls" if os.name == "nt" else "clear")
                return

            bill_index = int(choice) - 1
            if 0 <= bill_index < len(indexed_bills):
                original_idx = indexed_bills[bill_index][0]  # Get original index before sorting
                del bills["bills"][original_idx]  # Remove the correct bill from the original list

                with open(file_name, "w") as file:
                    json.dump(bills, file, indent=4)

                print("\nBill deleted successfully!")
                break
            else:
                print("Invalid selection. Please choose a valid bill number.")
        except ValueError:
            print("Enter a valid number.")

    input("\nPress Enter to return to the menu...")
    os.system("cls" if os.name == "nt" else "clear")

def pay_bills(bills):
    unpaid_bills = sorted(
        [(idx, bill) for idx, bill in enumerate(bills["bills"]) if bill["paid"] != 1],
        key=lambda item: datetime.strptime(item[1]["event_date"], "%a %b %d %Y"), reverse=True
    )

    if not unpaid_bills:
        print("\033[1;32mNo unpaid bills to pay!\033[0m")
        input("\nPress Enter to return to the menu...")
        return

    print("\033[1;32mSelect a bill to mark as paid:\033[0m\n")
    for display_idx, (original_idx, bill) in enumerate(unpaid_bills, start=1):
        print(f"{display_idx}. {format_event_date(bill['event_date'])} - {bill['event_theme']}")

    while True:
        user_input = input("\nEnter the task number to mark as paid (or 'b' to go back): ").strip()

        if user_input.lower() == "b":
            os.system("cls" if os.name == "nt" else "clear")
            return

        try:
            task_number = int(user_input)
            if 1 <= task_number <= len(unpaid_bills):
                original_idx = unpaid_bills[task_number - 1][0]
                bills["bills"][original_idx]["paid"] = 1
                save_bills(bills)
                print("Bills marked as paid.")
                return
            else:
                print("Invalid bill number. Please choose a valid number from the list.")

        except ValueError:
            print("Enter a valid number.")

def welcome_screen(stdscr):
    curses.curs_set(0)
    stdscr.clear()
    stdscr.refresh()
    height, width = stdscr.getmaxyx()
    message = "πŸ’°πŸ‡±πŸ‡ΊπŸ’°  Welcome to Bill Manager! πŸš€πŸ”₯πŸ’³"
    prompt = "Press Enter to continue..."
    bottom = "made by Robin te Hofstee"
    stdscr.addstr(height // 2 - 1, (width - len(message)) // 2, message)
    stdscr.addstr(height // 2 + 1, (width - len(prompt)) // 2, prompt)
    stdscr.addstr(height // 2 + 10, (width - len(bottom)) // 2, bottom)
    stdscr.refresh()
    stdscr.getch()

def exit_screen(stdscr):
    curses.curs_set(0)
    stdscr.clear()
    stdscr.refresh()
    height, width = stdscr.getmaxyx()
    message = "πŸ‘‹ Thanks for using the program! πŸš€"
    prompt = "Press any key to exit..."
    stdscr.addstr(height // 2 - 1, (width - len(message)) // 2, message)
    stdscr.addstr(height // 2 + 1, (width - len(prompt)) // 2, prompt)
    stdscr.refresh()
    stdscr.getch()

def main():
    bills = load_bills()
    
    while True:
        print("\033[1;32mBill Manager\033[0m\n")
        print("1. View Bills\n2. View Unpaid Bills\n3. Add Bills\n4. Pay Bills\n5. Delete Bills\n6. Grouped Bills\n7. Exit\n")
        choice = input("Enter Your Choice: ").strip()

        if choice == "1":
            view_bills(bills)
        elif choice == "2":
            view_unpaid_bills(bills)
            input("\nPress Enter to return to the menu...")
            os.system("cls" if os.name == "nt" else "clear")
        elif choice == "3":
            os.system("cls" if os.name == "nt" else "clear")
            add_bills(bills)
        elif choice == "4":
            os.system("cls" if os.name == "nt" else "clear")
            pay_bills(bills)
        elif choice == "5":
            delete_bills(bills)
        elif choice == "6":
            os.system("cls" if os.name == "nt" else "clear")
            group_paid_bills_by_theme(bills)
            input("\nPress Enter to return to the menu...")
            os.system("cls" if os.name == "nt" else "clear")
        elif choice == "7":
            os.system("cls" if os.name == "nt" else "clear")
           # curses.wrapper(exit_screen)
            break
        else:
            print("Invalid choice. Please try again")
            sleep(1)
            os.system("cls" if os.name == "nt" else "clear")


#curses.wrapper(welcome_screen)
main()

and this is the data.json file:

{
    "bills": [
        {
            "event_date": "Wed Apr 23 2025",
            "event_theme": "Internet",
            "paid": 1,
            "amount": 114.95,
            "icon": "fas fa-network-wired"
        },
        {
            "event_date": "Sat Apr 05 2025",
            "event_theme": "Car Insurance",
            "paid": 1,
            "amount": 0,
            "icon": "fas fa-car-burst"
        },
        {
            "event_date": "Fri May 23 2025",
            "event_theme": "Internet",
            "paid": 0,
            "amount": 114.95,
            "icon": "fas fa-network-wired"
        },
        {
            "event_date": "Mon May 05 2025",
            "event_theme": "Car Insurance",
            "paid": 1,
            "amount": 92.27,
            "icon": "fas fa-car-burst"
        }
    ]
}

Line-by-Line Explanation


import json


This line imports the json module, which is used to load and save JSON files containing bill data.

import os


This line imports the os module, which provides a way to interact with the operating system (e.g., clearing the screen).

from datetime import datetime


This line imports the datetime class from the datetime module, which is used to work with dates and times.

from time import sleep


This line imports the sleep function from the time module, which is used to pause the program for a short period of time (e.g., when displaying a message).

from collections import defaultdict


This line imports the defaultdict class from the collections module, which is used to create a dictionary with default values.

Functions


welcome_screen(stdscr)


This function is called by the curses.wrapper() function to display the welcome screen. It uses the stdscr object to clear the screen and print a message.

exit_screen(stdscr)


This function is called by the curses.wrapper() function to display the exit screen. It uses the stdscr object to clear the screen and print a message.

Main Function


main()


This is the main entry point of the program. It loads the bill data from a JSON file, then enters an infinite loop where it repeatedly displays the menu and waits for user input.


The menu options are:

  1. View Bills: Displays all bills.
  2. View Unpaid Bills: Displays only unpaid bills.
  3. Add Bills: Allows users to add new bills.
  4. Pay Bills: Marks a bill as paid.
  5. Delete Bills: Deletes a bill.
  6. Grouped Bills: Groups paid bills by theme.
  7. Exit: Exits the program.

Code Blocks


The code blocks are:

  • add_bills(bills): Adds a new bill to the list of bills.
  • view_bills(bills): Displays all bills.
  • view_unpaid_bills(bills): Displays only unpaid bills.
  • pay_bills(bills): Marks a bill as paid.
  • delete_bills(bills): Deletes a bill.
  • group_paid_bills_by_theme(bills): Groups paid bills by theme.

Comments