Compare commits
7 Commits
v1.0
...
6c1b55158d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c1b55158d | |||
| ab046271cf | |||
| 975b54b749 | |||
| 52239d00af | |||
| ffef627a4a | |||
| 910a3bc600 | |||
| 3cd30645fd |
@@ -1,274 +0,0 @@
|
||||
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
@@ -1,65 +0,0 @@
|
||||
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()
|
||||
@@ -1,90 +0,0 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def connect_db():
|
||||
db_file = 'time_tracker.db'
|
||||
if not os.path.exists(db_file):
|
||||
print(f"Error: Database file '{db_file}' not found.")
|
||||
return None
|
||||
return sqlite3.connect(db_file)
|
||||
|
||||
def display_entry_details(entry):
|
||||
print("\n--- Current Entry Details ---")
|
||||
print(f"ID: {entry[0]} | Project: {entry[1]} | Date: {entry[4]}")
|
||||
print(f"Description: {entry[2]}")
|
||||
print(f"Hours: {entry[5]}")
|
||||
print("-" * 30)
|
||||
|
||||
def edit_time_entry():
|
||||
conn = connect_db()
|
||||
if not conn: return
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 1. Select Client
|
||||
cursor.execute('SELECT client_id, client_name FROM clients ORDER BY client_name')
|
||||
clients = cursor.fetchall()
|
||||
|
||||
print("\n--- Select Client to Edit Time ---")
|
||||
for cid, name in clients:
|
||||
print(f"{cid}: {name}")
|
||||
|
||||
client_id = input("\nEnter Client ID (or 0 to go back): ")
|
||||
if client_id == '0' or not client_id: return
|
||||
|
||||
while True:
|
||||
# 2. List Uninvoiced Entries for Client
|
||||
cursor.execute('''SELECT entry_id, project, description, invoiced, date, hours
|
||||
FROM time_tracking WHERE client_id = ? AND invoiced = 0
|
||||
ORDER BY date DESC''', (client_id,))
|
||||
entries = cursor.fetchall()
|
||||
|
||||
if not entries:
|
||||
print("No uninvoiced entries found for this client.")
|
||||
break
|
||||
|
||||
print(f"\n--- Time Entries for Client {client_id} ---")
|
||||
for e in entries:
|
||||
print(f"ID {e[0]}: [{e[4]}] {e[1]} - {e[5]} hrs")
|
||||
print("0: Back to Main Menu")
|
||||
|
||||
entry_choice = input("\nSelect Entry ID to edit: ")
|
||||
if entry_choice == '0': break
|
||||
|
||||
# Find the specific entry
|
||||
selected_entry = next((e for e in entries if str(e[0]) == entry_choice), None)
|
||||
if not selected_entry:
|
||||
print("Invalid Entry ID.")
|
||||
continue
|
||||
|
||||
# 3. Edit Fields one by one
|
||||
display_entry_details(selected_entry)
|
||||
|
||||
# Project
|
||||
new_project = input(f"Project [{selected_entry[1]}]: ") or selected_entry[1]
|
||||
# Description
|
||||
new_desc = input(f"Description [{selected_entry[2]}]: ") or selected_entry[2]
|
||||
# Date
|
||||
new_date = input(f"Date [{selected_entry[4]}]: ") or selected_entry[4]
|
||||
# Hours
|
||||
new_hours_raw = input(f"Hours [{selected_entry[5]}]: ")
|
||||
new_hours = float(new_hours_raw) if new_hours_raw else selected_entry[5]
|
||||
|
||||
# 4. Update Database
|
||||
cursor.execute('''UPDATE time_tracking
|
||||
SET project = ?, description = ?, date = ?, hours = ?
|
||||
WHERE entry_id = ?''',
|
||||
(new_project, new_desc, new_date, new_hours, entry_choice))
|
||||
conn.commit()
|
||||
|
||||
print("\nUpdate Successful! New Values:")
|
||||
print(f"Project: {new_project}\nDescription: {new_desc}\nDate: {new_date}\nHours: {new_hours}")
|
||||
print("="*30)
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
edit_time_entry()
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/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. Edit Time") # New Option
|
||||
print("4. Create Invoice")
|
||||
print("5. Reports")
|
||||
print("0. Exit")
|
||||
print("="*40)
|
||||
|
||||
choice = input("Enter your choice (1-5, or 0): ")
|
||||
|
||||
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('edit_time.py') # Call new script
|
||||
elif choice == '4':
|
||||
run_script_from_file('create_invoice.py')
|
||||
elif choice == '5':
|
||||
run_script_from_file('reports.py')
|
||||
elif choice == '0':
|
||||
print("Exiting the Time Tracker. Goodbye!")
|
||||
break
|
||||
else:
|
||||
print("Invalid choice. Please enter a number between 1 and 5, or 0 to exit.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_menu()
|
||||
@@ -1,289 +0,0 @@
|
||||
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
@@ -1,125 +0,0 @@
|
||||
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
@@ -1,114 +0,0 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user