initial commit

This commit is contained in:
2025-12-20 14:52:47 -06:00
commit 71397f0b8a
8 changed files with 1033 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
time_tracker.db
.DS_Store
reset_invoiced.py

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# time tracking
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://gitlab.speerfam.net/mattspeer/time-tracking.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://gitlab.speerfam.net/mattspeer/time-tracking/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

274
create_invoice.py Normal file
View File

@@ -0,0 +1,274 @@
import sqlite3
import os
from datetime import date, datetime
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
def connect_db():
"""
Connects to the SQLite database file and returns the connection object.
If the file does not exist, it prints an an error message.
"""
db_file = 'time_tracker.db'
if not os.path.exists(db_file):
print(f"Error: Database file '{db_file}' not found. Please run the database creation script first.")
return None
return sqlite3.connect(db_file)
def generate_invoice():
"""
Guides the user through selecting a client and month to generate a PDF invoice.
Calculates costs, creates the PDF, and updates the database.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
# Display active clients for selection
cursor.execute('SELECT client_id, client_name FROM clients WHERE active = 1 ORDER BY client_name')
active_clients = cursor.fetchall()
if not active_clients:
print("No active clients found to generate an invoice for.")
return
print("\n--- Select a Client to Invoice ---")
for client_id, client_name in active_clients:
print(f"{client_id}: {client_name}")
print("0: Exit")
print("----------------------------------\n")
while True:
try:
choice = input("Enter the ID of the client (or 0 to exit): ")
if choice == '0':
print("Exiting invoice generation.")
return
client_id = int(choice)
# Check if the entered ID is in the list of active clients
if any(c[0] == client_id for c in active_clients):
break
else:
print("Invalid client ID. Please enter a valid ID from the list.")
except ValueError:
print("Invalid input. Please enter a number.")
# Get the month from the user
month_year_str = input("Enter the month to invoice (YYYY-MM): ")
if len(month_year_str) != 7 or month_year_str[4] != '-':
print("Invalid date format. Please use YYYY-MM.")
return
# Fetch client details and billing rate
cursor.execute('SELECT * FROM clients WHERE client_id = ?', (client_id,))
client_data = cursor.fetchone()
client_name, billing_rate = client_data[1], client_data[7]
# Fetch time entries for the selected client and month
cursor.execute('''
SELECT date, hours, description, project, entry_id
FROM time_tracking
WHERE client_id = ? AND date LIKE ? AND invoiced = 0
ORDER BY date
''', (client_id, f"{month_year_str}%"))
time_entries = cursor.fetchall()
if not time_entries:
print("No new time entries found for this client and month.")
return
# Group entries by date and calculate daily totals
daily_summary = {}
for entry in time_entries:
entry_date = entry[0]
if entry_date not in daily_summary:
daily_summary[entry_date] = {'hours': 0.0, 'descriptions': [], 'projects': [], 'entry_ids': []}
daily_summary[entry_date]['hours'] += entry[1]
daily_summary[entry_date]['descriptions'].append(f"{entry[2]}")
daily_summary[entry_date]['projects'].append(f"{entry[3]}")
daily_summary[entry_date]['entry_ids'].append(entry[4])
# Prepare data for PDF and calculate invoice total
invoice_total = 0.0
# Reorder table headers as requested
data_for_pdf = [['Date', 'Description', 'Project', 'Hours', 'Rate', 'Amount']]
for daily_date, summary in daily_summary.items():
daily_hours = summary['hours']
daily_total = daily_hours * billing_rate
invoice_total += daily_total
# Join descriptions and projects with <br/> for ReportLab to create new lines
descriptions_str = "<br/>".join(summary['descriptions'])
projects_str = "<br/>".join(summary['projects'])
# Use Paragraph to handle multi-line descriptions and projects in the PDF
data_for_pdf.append([
daily_date,
Paragraph(descriptions_str, getSampleStyleSheet()['Normal']),
Paragraph(projects_str, getSampleStyleSheet()['Normal']),
f"{daily_hours:.2f}",
f"${billing_rate:.2f}",
f"${daily_total:.2f}"
])
# Display the invoice summary before creating the PDF
print("\n--- Invoice Summary ---")
for row in data_for_pdf[1:]: # Skip header row for print
print(f"Date: {row[0]}, Hours: {row[3]}, Total: {row[5]}")
print(f"\nTotal Invoice Amount: ${invoice_total:.2f}")
# Confirmation to create the PDF
confirm = input("Generate PDF and update database? (yes/no): ").lower()
if confirm not in ['yes', 'y']:
print("Invoice generation cancelled.")
return
# Get the desired save directory from the user with a default value
default_dir = os.path.expanduser("~/Documents/0 - Inbox")
save_dir = input(f"Enter directory to save PDF (default: {default_dir}): ")
if not save_dir:
save_dir = default_dir
# Ensure the directory exists
if not os.path.exists(save_dir):
os.makedirs(save_dir)
print(f"Created directory: {save_dir}")
# Generate unique invoice ID and full file path
invoice_id = datetime.now().strftime('%Y%m%d%H%M%S')
file_name = f"Invoice_{invoice_id}_{client_name.replace(' ', '_')}.pdf"
full_file_path = os.path.join(save_dir, file_name)
# Create PDF and update database
create_pdf(full_file_path, client_data, invoice_total, data_for_pdf, invoice_id)
# Update database
update_database_after_invoice(conn, cursor, time_entries, client_id, invoice_total)
print(f"\nInvoice successfully created at '{full_file_path}' and database has been updated.")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
if conn:
conn.close()
def create_pdf(file_name, client_data, invoice_total, data_for_pdf, invoice_id):
"""
Creates the PDF document with the invoice details.
"""
doc = SimpleDocTemplate(file_name, pagesize=letter)
elements = []
# Define styles for the document
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='InvoiceTitle', fontSize=24, fontName='Helvetica-Bold'))
styles.add(ParagraphStyle(name='ClientInfo', fontSize=12))
styles.add(ParagraphStyle(name='AmountDue', fontSize=12, alignment=2, fontName='Helvetica-Bold'))
today_date_str = date.today().strftime('%m/%d/%Y')
# Top header table (INVOICE and From)
header_top_table = Table([
[
Paragraph("INVOICE", styles['InvoiceTitle']),
Paragraph("<b>From:</b><br/>Matt Speer<br/>2313 Hunters Cove<br/>Vestavia Hills, AL 35216", styles['ClientInfo'])
]
], colWidths=[250, 250])
header_top_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
('ALIGN', (1, 0), (1, 0), 'RIGHT')
]))
elements.append(header_top_table)
elements.append(Spacer(1, 20))
# Construct the client address string conditionally
client_address_str = f"<b>Invoice For:</b><br/>{client_data[1]}<br/>{client_data[2]}"
if client_data[3]: # Check if street_address_2 is not empty
client_address_str += f"<br/>{client_data[3]}"
client_address_str += f"<br/>{client_data[4]}, {client_data[5]} {client_data[6]}"
# Bottom header table (Invoice ID/Date and Invoice For)
header_bottom_table = Table([
[
Paragraph(f"Invoice ID: {invoice_id}<br/><br/>Invoice Date: {today_date_str}", styles['ClientInfo']),
Paragraph(client_address_str, styles['ClientInfo'])
]
], colWidths=[250, 250])
header_bottom_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
('ALIGN', (1, 0), (1, 0), 'RIGHT')
]))
elements.append(header_bottom_table)
elements.append(Spacer(1, 20))
# Create the table for time entries
table = Table(data_for_pdf, colWidths=[80, 200, 80, 40, 50, 50])
table.setStyle(TableStyle([
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('ALIGN', (0, 0), (-1, -0), 'LEFT'),
('VALIGN', (0, 0), (-1, 0), 'MIDDLE'), # Vertical align the header to the middle
('VALIGN', (0, 1), (-1, -1), 'TOP'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('LINEABOVE', (0, 0), (-1, 0), 1, colors.black),
('LINEBELOW', (0, 0), (-1, 0), 1, colors.black),
]))
elements.append(table)
elements.append(Spacer(1, 12))
# Line before the total amount due
line_table = Table([['']], colWidths=[500]) # Set width to match the time entries table
line_table.setStyle(TableStyle([
('LINEBELOW', (0, 0), (-1, -1), 1, colors.black),
('BOTTOMPADDING', (0, 0), (-1, -1), 0)
]))
elements.append(line_table)
elements.append(Spacer(1, 12))
# Add the total amount due section after the table
elements.append(Paragraph(f"AMOUNT DUE: ${invoice_total:.2f}", styles['AmountDue']))
# Build the document
doc.build(elements)
def update_database_after_invoice(conn, cursor, time_entries, client_id, invoice_total):
"""
Updates the 'invoiced' field for time entries and the client's balance.
"""
entry_ids = [entry[4] for entry in time_entries]
# Update time entries to be invoiced
cursor.executemany('UPDATE time_tracking SET invoiced = 1 WHERE entry_id = ?', [(entry_id,) for entry_id in entry_ids])
# Update the client's balance
cursor.execute('SELECT balance FROM clients WHERE client_id = ?', (client_id,))
current_balance = cursor.fetchone()[0]
new_balance = current_balance - invoice_total
cursor.execute('UPDATE clients SET balance = ? WHERE client_id = ?', (new_balance, client_id))
conn.commit()
if __name__ == "__main__":
generate_invoice()

65
db_build.py Normal file
View File

@@ -0,0 +1,65 @@
import sqlite3
def create_database():
"""
Connects to the SQLite database and creates the 'clients' and 'time_tracking' tables
if they do not already exist.
"""
db_file = 'time_tracker.db' # Define the database file name
conn = None # Initialize the connection variable to None
try:
# Connect to the database. This will create the file if it doesn't exist.
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Create the 'clients' table. The 'address' field has been split into
# separate fields for better data organization.
cursor.execute('''
CREATE TABLE IF NOT EXISTS clients (
client_id INTEGER PRIMARY KEY,
client_name TEXT NOT NULL UNIQUE,
street_address_1 TEXT,
street_address_2 TEXT,
city TEXT,
state TEXT,
zip_code TEXT,
billing_rate REAL,
balance REAL,
active INTEGER NOT NULL DEFAULT 1
);
''')
# Create the 'time_tracking' table with new fields for date and hours.
# The 'invoiced' field is an INTEGER where 0 is false and 1 is true.
# The 'client_id' is a foreign key that references the 'clients' table.
cursor.execute('''
CREATE TABLE IF NOT EXISTS time_tracking (
entry_id INTEGER PRIMARY KEY,
project TEXT NOT NULL,
description TEXT,
invoiced INTEGER NOT NULL DEFAULT 0 CHECK(invoiced IN (0, 1)),
date TEXT NOT NULL,
hours REAL NOT NULL,
client_id INTEGER,
FOREIGN KEY (client_id) REFERENCES clients (client_id)
);
''')
# Commit the changes to the database.
conn.commit()
print(f"Successfully created tables 'clients' and 'time_tracking' in '{db_file}'")
except sqlite3.Error as e:
# Print an error message if something goes wrong.
print(f"An error occurred: {e}")
finally:
# Ensure the database connection is always closed, even if an error occurs.
if conn:
conn.close()
print("Database connection closed.")
if __name__ == "__main__":
# Call the function to create the database when the script is run directly.
create_database()

70
main.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
# It is assumed that the following scripts exist in the same directory:
# - manage_clients.py
# - track_time.py
# - create_invoice.py
# These modules will be run as separate processes.
def run_script_from_file(script_name):
"""
Runs a Python script as a subprocess.
Args:
script_name (str): The name of the Python file to run.
"""
# Check if the file exists
if not os.path.exists(script_name):
print(f"Error: The script '{script_name}' was not found.")
print("Please ensure all required script files are in the same directory.")
return
try:
# Run the script using subprocess.run
# This will execute the script's top-level code (including any code in
# its 'if __name__ == "__main__":' block).
print(f"\n--- Running {script_name} ---")
subprocess.run(['python3', script_name], check=True)
print(f"--- {script_name} completed ---")
except subprocess.CalledProcessError as e:
print(f"An error occurred while trying to run '{script_name}': {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
def main_menu():
"""
Displays the main menu and handles user input to run different scripts.
"""
while True:
print("\n" + "="*40)
print(" TIME TRACKER MAIN MENU")
print("="*40)
print("1. Manage Clients")
print("2. Track Time")
print("3. Create Invoice")
print("4. Reports")
print("0. Exit") # Changed Exit option to 0
print("="*40)
choice = input("Enter your choice (1-3, or 0): ") # Updated prompt
if choice == '1':
run_script_from_file('manage_clients.py')
elif choice == '2':
run_script_from_file('track_time.py')
elif choice == '3':
run_script_from_file('create_invoice.py')
elif choice == '4':
run_script_from_file('reports.py')
elif choice == '0': # Updated logic for the new exit choice
print("Exiting the Time Tracker. Goodbye!")
break
else:
print("Invalid choice. Please enter a number between 1 and 3, or 0 to exit.")
if __name__ == "__main__":
main_menu()

289
manage_clients.py Normal file
View File

@@ -0,0 +1,289 @@
import sqlite3
import os
def connect_db():
"""
Connects to the SQLite database file and returns the connection object.
If the file does not exist, it prints an error message.
"""
db_file = 'time_tracker.db'
if not os.path.exists(db_file):
print(f"Error: Database file '{db_file}' not found. Please run the database creation script first.")
return None
return sqlite3.connect(db_file)
def display_client_details(client_data):
"""
Displays the details of a single client in a readable format.
"""
print("\n--- Client Details ---")
print(f"ID: {client_data[0]}")
print(f"Name: {client_data[1]}")
print(f"Street Address 1: {client_data[2]}")
print(f"Street Address 2: {client_data[3]}")
print(f"City: {client_data[4]}")
print(f"State: {client_data[5]}")
print(f"Zip Code: {client_data[6]}")
print(f"Billing Rate: {client_data[7]:.2f}")
print(f"Balance: {client_data[8]:.2f}")
print(f"Active: {'Yes' if client_data[9] else 'No'}")
print("----------------------\n")
def add_new_client():
"""
Prompts the user for information to create a new client and adds it to the database.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
print("\n--- Add New Client ---")
# Prompt for each field
client_name = input("Enter client name: ")
street_address_1 = input("Enter street address 1: ")
street_address_2 = input("Enter street address 2 (optional): ")
city = input("Enter city: ")
state = input("Enter state: ")
zip_code = input("Enter zip code: ")
# Validate billing rate and balance
while True:
try:
billing_rate = float(input("Enter billing rate: "))
break
except ValueError:
print("Invalid input. Please enter a number for billing rate.")
while True:
try:
balance = float(input("Enter starting balance: "))
break
except ValueError:
print("Invalid input. Please enter a number for balance.")
# Active status is boolean, use 1 or 0
active_input = input("Is the client active? (yes/no): ").lower()
active = 1 if active_input in ['yes', 'y'] else 0
# Insert the new client into the database
cursor.execute('''
INSERT INTO clients (client_name, street_address_1, street_address_2, city, state, zip_code, billing_rate, balance, active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (client_name, street_address_1, street_address_2, city, state, zip_code, billing_rate, balance, active))
conn.commit()
print("\nClient added successfully!")
# Fetch the newly created client to display it
last_id = cursor.lastrowid
cursor.execute('SELECT * FROM clients WHERE client_id = ?', (last_id,))
new_client = cursor.fetchone()
display_client_details(new_client)
except sqlite3.IntegrityError:
print("\nError: A client with that name already exists. Please choose a unique name.")
except sqlite3.Error as e:
print(f"\nAn error occurred: {e}")
finally:
if conn:
conn.close()
def edit_existing_client():
"""
Displays a list of clients and allows the user to select and edit one.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
cursor.execute('SELECT client_id, client_name FROM clients')
clients = cursor.fetchall()
if not clients:
print("No clients found to edit.")
return
print("\n--- Existing Clients ---")
for client_id, client_name in clients:
print(f"{client_id}: {client_name}")
print("----------------------\n")
while True:
try:
choice = input("Enter the ID of the client you want to edit (or 'back' to return to menu): ").strip().lower()
if choice == 'back':
return
client_id = int(choice)
cursor.execute('SELECT * FROM clients WHERE client_id = ?', (client_id,))
client_data = cursor.fetchone()
if client_data:
break
else:
print("Invalid client ID. Please try again.")
except ValueError:
print("Invalid input. Please enter a number or 'back'.")
display_client_details(client_data)
# The fields that can be edited, mapped to their database column names
field_map = {
'1': 'client_name',
'2': 'street_address_1',
'3': 'street_address_2',
'4': 'city',
'5': 'state',
'6': 'zip_code',
'7': 'billing_rate',
'8': 'balance',
'9': 'active'
}
print("\n--- Select a Field to Edit ---")
print("1: Client Name")
print("2: Street Address 1")
print("3: Street Address 2")
print("4: City")
print("5: State")
print("6: Zip Code")
print("7: Billing Rate")
print("8: Balance")
print("9: Active Status")
print("0: Cancel and Return to Main Menu")
while True:
edit_choice = input("Enter the number of the field you want to edit: ")
if edit_choice == '0':
print("Edit cancelled.")
return
column_name = field_map.get(edit_choice)
if column_name:
break
else:
print("Invalid choice. Please enter a valid number.")
# Get the new value from the user, prepopulating with old value
current_value_index = list(field_map.keys()).index(edit_choice) + 1
# Handle special cases for data types
if column_name in ['billing_rate', 'balance']:
new_value_str = input(f"Enter new value for {column_name.replace('_', ' ')} (current: {client_data[current_value_index]}): ")
if not new_value_str:
new_value = client_data[current_value_index]
else:
try:
new_value = float(new_value_str)
except ValueError:
print("Invalid input. Please enter a number.")
return
elif column_name == 'active':
new_value_input = input(f"Is the client active? (yes/no, current: {'Yes' if client_data[current_value_index] else 'No'}): ").lower()
if not new_value_input:
new_value = client_data[current_value_index]
else:
new_value = 1 if new_value_input in ['yes', 'y'] else 0
else:
new_value_input = input(f"Enter new value for {column_name.replace('_', ' ')} (current: {client_data[current_value_index]}): ")
if not new_value_input:
new_value = client_data[current_value_index]
else:
new_value = new_value_input
# Update the database
cursor.execute(f"UPDATE clients SET {column_name} = ? WHERE client_id = ?", (new_value, client_id))
conn.commit()
print("\nClient information updated successfully!")
# Fetch and display the updated client details
cursor.execute('SELECT * FROM clients WHERE client_id = ?', (client_id,))
updated_client = cursor.fetchone()
display_client_details(updated_client)
except sqlite3.Error as e:
print(f"\nAn error occurred: {e}")
finally:
if conn:
conn.close()
def display_client_info():
"""
Displays a list of clients and allows the user to select one to view its details.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
cursor.execute('SELECT client_id, client_name FROM clients ORDER BY client_name')
clients = cursor.fetchall()
if not clients:
print("No clients found.")
return
print("\n--- Existing Clients ---")
for client_id, client_name in clients:
print(f"{client_id}: {client_name}")
print("----------------------\n")
while True:
try:
choice = input("Enter the ID of the client you want to view (or 'back' to return to menu): ").strip().lower()
if choice == 'back':
return
client_id = int(choice)
cursor.execute('SELECT * FROM clients WHERE client_id = ?', (client_id,))
client_data = cursor.fetchone()
if client_data:
display_client_details(client_data)
break
else:
print("Invalid client ID. Please try again.")
except ValueError:
print("Invalid input. Please enter a number or 'back'.")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
if conn:
conn.close()
def main_menu():
"""
Displays the main menu and handles user choices.
"""
while True:
print("\n--- Time Tracker Client Manager ---")
print("1. Add a new client")
print("2. Display client information")
print("3. Edit an existing client")
print("0. Exit")
choice = input("Enter your choice: ").strip()
if choice == '1':
add_new_client()
elif choice == '2':
display_client_info()
elif choice == '3':
edit_existing_client()
elif choice == '0':
print("Exiting. Goodbye!")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main_menu()

125
reports.py Normal file
View File

@@ -0,0 +1,125 @@
import sqlite3
import os
from datetime import datetime
def connect_db():
"""
Connects to the SQLite database file and returns the connection object.
If the file does not exist, it prints an an error message.
"""
db_file = 'time_tracker.db'
if not os.path.exists(db_file):
print(f"Error: Database file '{db_file}' not found. Please run the database creation script first.")
return None
return sqlite3.connect(db_file)
def unbilled_time_report():
"""
Generates a report of unbilled time entries for a selected client.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
# Display active clients for selection
cursor.execute('SELECT client_id, client_name FROM clients WHERE active = 1 ORDER BY client_name')
active_clients = cursor.fetchall()
if not active_clients:
print("No active clients found to generate a report for.")
return
print("\n--- Select a Client for Unbilled Time Report ---")
for client_id, client_name in active_clients:
print(f"{client_id}: {client_name}")
print("0: Exit")
print("--------------------------------------------------\n")
while True:
try:
choice = input("Enter the ID of the client (or 0 to exit): ")
if choice == '0':
print("Exiting report generation.")
return
client_id = int(choice)
if any(c[0] == client_id for c in active_clients):
break
else:
print("Invalid client ID. Please enter a valid ID from the list.")
except ValueError:
print("Invalid input. Please enter a number.")
# Fetch client details and billing rate
cursor.execute('SELECT client_name, billing_rate FROM clients WHERE client_id = ?', (client_id,))
client_name, billing_rate = cursor.fetchone()
# Fetch unbilled time entries for the selected client, including description
cursor.execute('''
SELECT date, hours, description, project
FROM time_tracking
WHERE client_id = ? AND invoiced = 0
ORDER BY date
''', (client_id,))
time_entries = cursor.fetchall()
if not time_entries:
print(f"\nNo unbilled time entries found for {client_name}.")
return
# Calculate daily summary, descriptions, and total cost
daily_summary = {}
grand_total_cost = 0.0
for entry_date, hours, description, project in time_entries:
if entry_date not in daily_summary:
daily_summary[entry_date] = {'hours': 0.0, 'cost': 0.0, 'entries': []}
daily_summary[entry_date]['hours'] += hours
daily_summary[entry_date]['cost'] += hours * billing_rate
daily_summary[entry_date]['entries'].append({'description': description, 'project': project})
grand_total_cost += hours * billing_rate
# Print the formatted report
print(f"\n--- Unbilled Time Report for {client_name} ---")
print(f"Total Unbilled Cost: ${grand_total_cost:.2f}\n")
for daily_date, summary in daily_summary.items():
print(f"Date: {daily_date} | Hours: {summary['hours']:.2f} | Cost: ${summary['cost']:.2f}")
for entry in summary['entries']:
print(f" - Project: {entry['project']}, Description: {entry['description']}")
print("-" * 32)
print(f"\nReport generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
if conn:
conn.close()
def main_menu():
"""
Displays the main menu and handles user choices.
"""
while True:
print("\n--- Reports Menu ---")
print("1: Unbilled Time")
print("0: Exit")
print("--------------------\n")
choice = input("Enter your choice: ")
if choice == '1':
unbilled_time_report()
elif choice == '0':
print("Exiting reports script. Goodbye!")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main_menu()

114
track_time.py Normal file
View File

@@ -0,0 +1,114 @@
import sqlite3
import os
from datetime import date
def connect_db():
"""
Connects to the SQLite database file and returns the connection object.
If the file does not exist, it prints an error message.
"""
db_file = 'time_tracker.db'
if not os.path.exists(db_file):
print(f"Error: Database file '{db_file}' not found. Please run the database creation script first.")
return None
return sqlite3.connect(db_file)
def add_time_entry():
"""
Prompts the user for details and adds a new time entry to the database.
"""
conn = connect_db()
if not conn:
return
try:
cursor = conn.cursor()
# Display all active clients for selection
cursor.execute('SELECT client_id, client_name FROM clients WHERE active = 1 ORDER BY client_name')
active_clients = cursor.fetchall()
if not active_clients:
print("No active clients found. Please add an active client first.")
return
print("\n--- Select a Client ---")
for client_id, client_name in active_clients:
print(f"{client_id}: {client_name}")
print("0: Exit") # Added exit option
print("----------------------")
while True:
try:
choice = int(input("Enter the ID of the client (or 0 to exit): "))
except ValueError:
print("Invalid input. Please enter a number.")
continue
if choice == 0:
print("Exiting time entry.")
return # Exit the function if the user chooses 0
# Check if the entered ID is in the list of active clients
if any(c[0] == choice for c in active_clients):
client_id = choice
break
else:
print("Invalid client ID. Please enter a valid ID from the list.")
print(f"\n--- Add New Time Entry for Client ID {client_id} ---")
project = input("Enter project name: ")
description = input("Enter a brief description: ")
# Get the hours and validate it's a number
while True:
try:
hours = float(input("Enter number of hours (e.g., 1.5): "))
if hours <= 0:
print("Hours must be a positive number.")
else:
break
except ValueError:
print("Invalid input. Please enter a number for hours.")
# Get the date, defaulting to today's date
entry_date = input(f"Enter the date (YYYY-MM-DD, default is today: {date.today()}): ")
if not entry_date:
entry_date = str(date.today())
# The 'invoiced' field defaults to 0 (false)
invoiced = 0
# Insert the new time entry into the database
cursor.execute('''
INSERT INTO time_tracking (project, description, invoiced, date, hours, client_id)
VALUES (?, ?, ?, ?, ?, ?)
''', (project, description, invoiced, entry_date, hours, client_id))
conn.commit()
print("\nTime entry added successfully!")
# Fetch and display the newly created entry
last_id = cursor.lastrowid
cursor.execute('SELECT * FROM time_tracking WHERE entry_id = ?', (last_id,))
new_entry = cursor.fetchone()
print("\n--- New Time Entry Details ---")
print(f"ID: {new_entry[0]}")
print(f"Project: {new_entry[1]}")
print(f"Description: {new_entry[2]}")
print(f"Invoiced: {'Yes' if new_entry[3] else 'No'}")
print(f"Date: {new_entry[4]}")
print(f"Hours: {new_entry[5]:.2f}")
print(f"Client ID: {new_entry[6]}")
print("------------------------------\n")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
if conn:
conn.close()
if __name__ == "__main__":
add_time_entry()