initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
time_tracker.db
|
||||
.DS_Store
|
||||
reset_invoiced.py
|
||||
93
README.md
Normal file
93
README.md
Normal 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
274
create_invoice.py
Normal 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
65
db_build.py
Normal 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
70
main.py
Normal 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
289
manage_clients.py
Normal 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
125
reports.py
Normal 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
114
track_time.py
Normal 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()
|
||||
Reference in New Issue
Block a user