overhaul of timelogix app
This commit is contained in:
parent
266d21b540
commit
34d7c4c9dd
1 changed files with 355 additions and 225 deletions
580
time_logix.py
580
time_logix.py
|
|
@ -1,271 +1,401 @@
|
|||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from datetime import datetime, timedelta
|
||||
from tkinter import ttk
|
||||
import time
|
||||
import datetime
|
||||
import csv
|
||||
import os
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import Table, TableStyle, Paragraph
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
import sys # Import the sys module
|
||||
|
||||
class TimeLogix(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("TimeLogix")
|
||||
self.geometry("500x500")
|
||||
|
||||
class TimeLogix:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("TimeLogix Application")
|
||||
|
||||
# Styling
|
||||
self.bg_color = "#f0f0f0" # Light gray background
|
||||
self.font_family = "Segoe UI" # Modern font
|
||||
self.font_size = 10
|
||||
self.button_bg = "#4CAF50" # Green button background
|
||||
self.button_fg = "white" # White button text
|
||||
self.button_width = 15 # Consistent button width
|
||||
|
||||
self.root.configure(bg=self.bg_color)
|
||||
|
||||
style = ttk.Style()
|
||||
style.configure(
|
||||
"TLabel", background=self.bg_color, font=(self.font_family, self.font_size)
|
||||
)
|
||||
style.configure(
|
||||
"TButton",
|
||||
background=self.button_bg,
|
||||
foreground=self.button_fg,
|
||||
font=(self.font_family, self.font_size),
|
||||
padding=5,
|
||||
width=self.button_width, # Set button width here
|
||||
)
|
||||
style.configure(
|
||||
"TCombobox", background="white", font=(self.font_family, self.font_size)
|
||||
)
|
||||
style.configure(
|
||||
"TEntry", background="white", font=(self.font_family, self.font_size)
|
||||
)
|
||||
style.configure("My.TFrame", background=self.bg_color) # Frame style
|
||||
|
||||
self.is_running = False
|
||||
self.start_time = None
|
||||
self.tracking = False
|
||||
self.sessions = []
|
||||
self.total_hours = 0
|
||||
self.elapsed_time = 0
|
||||
self.timer_id = None
|
||||
self.log_entries = []
|
||||
self.project_file = "projects.txt"
|
||||
self.projects = self.load_projects()
|
||||
self.invoice_number = 1
|
||||
self.company_name = "Your Company Name"
|
||||
self.company_address = "123 Main St, Anytown, USA"
|
||||
self.client_name = "Client Name"
|
||||
self.client_address = "Client Address"
|
||||
self.hourly_rate = 60.00
|
||||
|
||||
self.create_widgets()
|
||||
self.load_sessions_from_csv()
|
||||
self.update_total_hours()
|
||||
# UI elements
|
||||
self.task_label = ttk.Label(root, text="Task Description:")
|
||||
self.task_label.grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.task_entry = ttk.Entry(root, width=40)
|
||||
self.task_entry.grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
|
||||
def create_widgets(self):
|
||||
self.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12))
|
||||
self.status_label.pack(pady=5)
|
||||
self.project_label = ttk.Label(root, text="Project:")
|
||||
self.project_label.grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.project_combo = ttk.Combobox(root, values=self.projects)
|
||||
self.project_combo.grid(row=1, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
if self.projects:
|
||||
self.project_combo.set(self.projects[0])
|
||||
|
||||
self.start_button = tk.Button(
|
||||
self, text="Start Tracking", width=20, command=self.start_tracking
|
||||
self.time_label = ttk.Label(
|
||||
root, text="00:00:00", font=(self.font_family, 36)
|
||||
) # Larger time
|
||||
self.time_label.grid(row=2, column=0, columnspan=2, pady=15)
|
||||
|
||||
self.start_stop_button = ttk.Button(
|
||||
root, text="Start", command=self.toggle_timer
|
||||
)
|
||||
self.start_button.pack(pady=2)
|
||||
self.start_stop_button.grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
self.stop_button = tk.Button(
|
||||
self, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED
|
||||
self.log_label = ttk.Label(root, text="Log Entries:")
|
||||
self.log_label.grid(row=4, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
|
||||
self.log_text = tk.Text(
|
||||
root,
|
||||
height=10,
|
||||
width=50,
|
||||
bg="white",
|
||||
font=(self.font_family, self.font_size),
|
||||
)
|
||||
self.stop_button.pack(pady=2)
|
||||
self.log_text.grid(row=5, column=0, columnspan=2, padx=10, pady=5)
|
||||
|
||||
self.description_label = tk.Label(self, text="Work Description:")
|
||||
self.description_label.pack(pady=1)
|
||||
self.description_entry = tk.Text(self, height=3, width=40)
|
||||
self.description_entry.pack(pady=1)
|
||||
# Add Project UI
|
||||
self.new_project_label = ttk.Label(root, text="New Project:")
|
||||
self.new_project_label.grid(row=6, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.new_project_entry = ttk.Entry(root, width=30)
|
||||
self.new_project_entry.grid(row=6, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
|
||||
self.export_csv_button = tk.Button(
|
||||
self, text="Export to CSV", width=20, command=self.export_sessions_csv
|
||||
self.add_project_button = ttk.Button(
|
||||
root, text="Add Project", command=self.add_project
|
||||
)
|
||||
self.export_csv_button.pack(pady=2)
|
||||
self.add_project_button.grid(row=7, column=0, columnspan=2, pady=5)
|
||||
|
||||
self.export_pdf_button = tk.Button(
|
||||
self, text="Export to PDF", width=20, command=self.export_sessions_pdf
|
||||
# Button Grouping
|
||||
button_frame = ttk.Frame(root, padding=10, style="My.TFrame")
|
||||
button_frame.grid(row=8, column=0, columnspan=2, pady=10)
|
||||
|
||||
self.export_csv_button = ttk.Button(
|
||||
button_frame, text="Export to CSV", command=self.export_to_csv
|
||||
)
|
||||
self.export_pdf_button.pack(pady=2)
|
||||
self.export_csv_button.grid(row=0, column=0, padx=5, pady=5)
|
||||
|
||||
self.total_hours_label = tk.Label(self, text="Total Hours Worked: 0.00", font=("Helvetica", 12))
|
||||
self.total_hours_label.pack(pady=5)
|
||||
|
||||
self.log_text = tk.Text(self, height=10, state=tk.DISABLED)
|
||||
self.log_text.pack(pady=5, padx=10, fill='both', expand=True)
|
||||
|
||||
self.exit_button = tk.Button(
|
||||
self, text="Exit", width=10, command=self.exit_app
|
||||
self.export_pdf_button = ttk.Button(
|
||||
button_frame, text="Export to PDF", command=self.export_to_pdf
|
||||
)
|
||||
self.exit_button.pack(pady=5)
|
||||
self.export_pdf_button.grid(row=0, column=1, padx=5, pady=5)
|
||||
|
||||
def log_message(self, message):
|
||||
if hasattr(self, 'log_text'):
|
||||
self.log_text.configure(state=tk.NORMAL)
|
||||
self.log_text.insert(tk.END, f"{message}\n")
|
||||
self.log_text.configure(state=tk.DISABLED)
|
||||
self.log_text.see(tk.END)
|
||||
|
||||
def start_tracking(self):
|
||||
if self.tracking:
|
||||
messagebox.showwarning("Warning", "Already tracking!")
|
||||
return
|
||||
|
||||
self.start_time = datetime.now()
|
||||
self.tracking = True
|
||||
self.status_label.config(
|
||||
text=f"Status: Tracking started at {self.start_time.strftime('%H:%M:%S')}"
|
||||
self.exit_button = ttk.Button( # Create Exit button
|
||||
button_frame, text="Exit", command=self.exit_app
|
||||
)
|
||||
self.log_message(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
self.exit_button.grid(row=0, column=2, padx=5, pady=5) # Place in button_frame
|
||||
|
||||
def stop_tracking(self):
|
||||
if not self.tracking:
|
||||
messagebox.showwarning("Warning", "Not currently tracking!")
|
||||
return
|
||||
|
||||
end_time = datetime.now()
|
||||
duration = end_time - self.start_time
|
||||
description = self.description_entry.get("1.0", tk.END).strip()
|
||||
self.sessions.append((self.start_time, end_time, duration, description))
|
||||
self.tracking = False
|
||||
self.status_label.config(text="Status: Not Tracking")
|
||||
decimal_hours = self.get_decimal_hours(duration)
|
||||
self.log_message(
|
||||
f"Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, Duration: {self.format_duration(duration)} "
|
||||
f"({decimal_hours:.2f} hours), Description: {description}"
|
||||
self.total_time_button = ttk.Button(
|
||||
root,
|
||||
text="Calculate Total Time",
|
||||
command=self.calculate_total_time,
|
||||
width=20, # Set specific width for this button
|
||||
)
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
self.description_entry.delete("1.0", tk.END)
|
||||
self.total_time_button.grid(row=9, column=0, columnspan=2, pady=5)
|
||||
|
||||
self.update_total_hours()
|
||||
self.total_time_label = ttk.Label(root, text="Total Time: 00:00:00")
|
||||
self.total_time_label.grid(row=10, column=0, columnspan=2, pady=5)
|
||||
|
||||
def format_duration(self, duration):
|
||||
total_seconds = int(duration.total_seconds())
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{hours}:{minutes:02d}:{seconds:02d}"
|
||||
# Settings UI
|
||||
self.company_name_label = ttk.Label(root, text="Company Name:")
|
||||
self.company_name_label.grid(row=11, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.company_name_entry = ttk.Entry(root, width=30)
|
||||
self.company_name_entry.grid(row=11, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
self.company_name_entry.insert(0, self.company_name)
|
||||
|
||||
def get_decimal_hours(self, duration):
|
||||
total_hours = duration.total_seconds() / 3600
|
||||
return total_hours
|
||||
self.company_address_label = ttk.Label(root, text="Company Address:")
|
||||
self.company_address_label.grid(row=12, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.company_address_entry = ttk.Entry(root, width=30)
|
||||
self.company_address_entry.grid(row=12, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
self.company_address_entry.insert(0, self.company_address)
|
||||
|
||||
def export_sessions_csv(self):
|
||||
if not self.sessions:
|
||||
messagebox.showinfo("Info", "No sessions to export.")
|
||||
return
|
||||
self.client_name_label = ttk.Label(root, text="Client Name:")
|
||||
self.client_name_label.grid(row=13, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.client_name_entry = ttk.Entry(root, width=30)
|
||||
self.client_name_entry.grid(row=13, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
self.client_name_entry.insert(0, self.client_name)
|
||||
|
||||
filename = "working_sessions.csv"
|
||||
self.client_address_label = ttk.Label(root, text="Client Address:")
|
||||
self.client_address_label.grid(row=14, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.client_address_entry = ttk.Entry(root, width=30)
|
||||
self.client_address_entry.grid(row=14, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
self.client_address_entry.insert(0, self.client_address)
|
||||
|
||||
self.hourly_rate_label = ttk.Label(root, text="Hourly Rate:")
|
||||
self.hourly_rate_label.grid(row=15, column=0, sticky=tk.W, padx=10, pady=5)
|
||||
self.hourly_rate_entry = ttk.Entry(root, width=10)
|
||||
self.hourly_rate_entry.grid(row=15, column=1, sticky=tk.W, padx=10, pady=5)
|
||||
self.hourly_rate_entry.insert(0, str(self.hourly_rate))
|
||||
|
||||
self.update_settings_button = ttk.Button(
|
||||
root, text="Update Settings", command=self.update_settings
|
||||
)
|
||||
self.update_settings_button.grid(row=16, column=0, columnspan=2, pady=10)
|
||||
|
||||
# Configure grid weights to make the layout expand
|
||||
for i in range(17):
|
||||
root.grid_rowconfigure(i, weight=1)
|
||||
root.grid_columnconfigure(0, weight=1)
|
||||
root.grid_columnconfigure(1, weight=1)
|
||||
|
||||
def load_projects(self):
|
||||
try:
|
||||
with open(filename, mode="w", newline="") as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow([
|
||||
"Start Time", "End Time",
|
||||
"Duration (H:MM:SS)", "Decimal Hours", "Description"
|
||||
])
|
||||
for start, end, duration, description in self.sessions:
|
||||
writer.writerow([
|
||||
start.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
end.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
self.format_duration(duration),
|
||||
f"{self.get_decimal_hours(duration):.2f}",
|
||||
description
|
||||
])
|
||||
writer.writerow(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
|
||||
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
|
||||
with open(self.project_file, "r") as f:
|
||||
projects = [line.strip() for line in f]
|
||||
return projects
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
def save_projects(self):
|
||||
try:
|
||||
with open(self.project_file, "w") as f:
|
||||
for project in self.projects:
|
||||
f.write(project + "\n")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to export: {e}")
|
||||
print(f"Error saving projects: {e}")
|
||||
|
||||
def export_sessions_pdf(self):
|
||||
if not self.sessions:
|
||||
messagebox.showinfo("Info", "No sessions to export.")
|
||||
return
|
||||
def add_project(self):
|
||||
new_project = self.new_project_entry.get().strip()
|
||||
if new_project and new_project not in self.projects:
|
||||
self.projects.append(new_project)
|
||||
self.project_combo["values"] = self.projects # Update Combobox
|
||||
self.project_combo.set(new_project) # Set to the new project
|
||||
self.save_projects()
|
||||
self.new_project_entry.delete(0, tk.END) # Clear the entry
|
||||
elif new_project in self.projects:
|
||||
print("Project already exists.") # Replace with a GUI message box
|
||||
else:
|
||||
print("Project name cannot be empty.") # Replace with a GUI message box
|
||||
|
||||
filename = "working_sessions.pdf"
|
||||
def update_settings(self):
|
||||
self.company_name = self.company_name_entry.get()
|
||||
self.company_address = self.company_address_entry.get()
|
||||
self.client_name = self.client_name_entry.get()
|
||||
self.client_address = self.client_address_entry.get()
|
||||
try:
|
||||
doc = SimpleDocTemplate(filename, pagesize=letter)
|
||||
elements = []
|
||||
self.hourly_rate = float(self.hourly_rate_entry.get())
|
||||
except ValueError:
|
||||
print("Invalid hourly rate. Using default.") # Replace with GUI message
|
||||
self.hourly_rate = 50.00 # Revert to default, or handle the error
|
||||
|
||||
data = [
|
||||
["Start Time", "End Time", "Duration (H:MM:SS)", "Decimal Hours", "Description"]
|
||||
]
|
||||
for start, end, duration, description in self.sessions:
|
||||
data.append([
|
||||
start.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
end.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
self.format_duration(duration),
|
||||
f"{self.get_decimal_hours(duration):.2f}",
|
||||
description
|
||||
])
|
||||
# OPTIONAL: Save settings to a file here (e.g., JSON)
|
||||
|
||||
data.append(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
|
||||
def toggle_timer(self):
|
||||
if self.is_running:
|
||||
self.stop_timer()
|
||||
else:
|
||||
self.start_timer()
|
||||
|
||||
table = Table(data)
|
||||
style = TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
||||
])
|
||||
def start_timer(self):
|
||||
self.is_running = True
|
||||
self.start_stop_button.config(text="Stop")
|
||||
self.start_time = time.time()
|
||||
self.update_timer()
|
||||
|
||||
def stop_timer(self):
|
||||
self.is_running = False
|
||||
self.start_stop_button.config(text="Start")
|
||||
self.root.after_cancel(self.timer_id)
|
||||
self.log_time_entry()
|
||||
|
||||
def update_timer(self):
|
||||
if self.is_running:
|
||||
self.elapsed_time = time.time() - self.start_time
|
||||
minutes, seconds = divmod(self.elapsed_time, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
time_str = "{:02d}:{:02d}:{:02d}".format(
|
||||
int(hours), int(minutes), int(seconds)
|
||||
)
|
||||
self.time_label.config(text=time_str)
|
||||
self.timer_id = self.root.after(100, self.update_timer)
|
||||
|
||||
def log_time_entry(self):
|
||||
end_time = datetime.datetime.now()
|
||||
task_description = self.task_entry.get()
|
||||
project = self.project_combo.get()
|
||||
duration = self.elapsed_time
|
||||
start_time_str = end_time - datetime.timedelta(seconds=duration)
|
||||
|
||||
entry = {
|
||||
"task": task_description,
|
||||
"project": project,
|
||||
"start_time": start_time_str.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"duration": round(duration, 2),
|
||||
}
|
||||
self.log_entries.append(entry)
|
||||
self.update_log_display()
|
||||
self.elapsed_time = 0
|
||||
self.time_label.config(text="00:00:00")
|
||||
|
||||
def update_log_display(self):
|
||||
self.log_text.delete("1.0", tk.END)
|
||||
for entry in self.log_entries:
|
||||
self.log_text.insert(tk.END, f"Task: {entry['task']}\n")
|
||||
self.log_text.insert(tk.END, f"Project: {entry['project']}\n")
|
||||
self.log_text.insert(tk.END, f"Start: {entry['start_time']}\n")
|
||||
self.log_text.insert(tk.END, f"End: {entry['end_time']}\n")
|
||||
self.log_text.insert(tk.END, f"Duration: {entry['duration']} seconds\n")
|
||||
self.log_text.insert(tk.END, "-" * 20 + "\n")
|
||||
|
||||
def export_to_csv(self):
|
||||
try:
|
||||
with open("time_entries.csv", "w", newline="") as csvfile:
|
||||
fieldnames = ["task", "project", "start_time", "end_time", "duration"]
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for entry in self.log_entries:
|
||||
writer.writerow(entry)
|
||||
print("Exported to CSV successfully!")
|
||||
except Exception as e:
|
||||
print(f"Error exporting to CSV: {e}")
|
||||
|
||||
def export_to_pdf(self):
|
||||
try:
|
||||
filename = f"invoice_{self.invoice_number}.pdf"
|
||||
c = canvas.Canvas(filename, pagesize=letter)
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# --- Header ---
|
||||
c.setFont("Helvetica-Bold", 16)
|
||||
c.drawString(inch, 7.5 * inch, self.company_name)
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(inch, 7.3 * inch, self.company_address)
|
||||
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(4.5 * inch, 7.5 * inch, "Invoice")
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(
|
||||
4.5 * inch, 7.3 * inch, f"Invoice Number: {self.invoice_number}"
|
||||
)
|
||||
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
c.drawString(4.5 * inch, 7.1 * inch, f"Date: {current_date}")
|
||||
|
||||
# --- Client Info ---
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(inch, 6.5 * inch, "Bill To:")
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(inch, 6.3 * inch, self.client_name)
|
||||
c.drawString(inch, 6.1 * inch, self.client_address)
|
||||
|
||||
# --- Table ---
|
||||
data = [["Task", "Project", "Hours", "Rate", "Total"]]
|
||||
total_amount = 0
|
||||
|
||||
for entry in self.log_entries:
|
||||
hours = entry["duration"] / 3600
|
||||
line_total = hours * self.hourly_rate
|
||||
total_amount += line_total
|
||||
data.append(
|
||||
[
|
||||
entry["task"],
|
||||
entry["project"],
|
||||
f"{hours:.2f}",
|
||||
f"${self.hourly_rate:.2f}",
|
||||
f"${line_total:.2f}",
|
||||
]
|
||||
)
|
||||
|
||||
table = Table(data, colWidths=[1.5 * inch, 1.5 * inch, inch, inch, inch])
|
||||
style = TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
|
||||
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
||||
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
||||
]
|
||||
)
|
||||
table.setStyle(style)
|
||||
elements.append(table)
|
||||
table.wrapOn(c, letter[0] - 2 * inch, letter[1] - 2 * inch)
|
||||
table.drawOn(c, inch, 4 * inch)
|
||||
|
||||
# --- Totals ---
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(4 * inch, 3.5 * inch, "Subtotal:")
|
||||
c.setFont("Helvetica", 12)
|
||||
c.drawRightString(5.5 * inch, 3.5 * inch, f"${total_amount:.2f}")
|
||||
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(4 * inch, 3.3 * inch, "Tax (0%):")
|
||||
c.setFont("Helvetica", 12)
|
||||
c.drawRightString(5.5 * inch, 3.3 * inch, "$0.00")
|
||||
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(4 * inch, 3.1 * inch, "Total:")
|
||||
c.setFont("Helvetica", 12)
|
||||
c.drawRightString(5.5 * inch, 3.1 * inch, f"${total_amount:.2f}")
|
||||
|
||||
# --- Notes ---
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(inch, 2 * inch, "Notes:")
|
||||
c.drawString(inch, 1.8 * inch, "Thank you for your business!")
|
||||
|
||||
c.save()
|
||||
print(f"Exported to PDF successfully as {filename}!")
|
||||
self.invoice_number += 1
|
||||
|
||||
doc.build(elements)
|
||||
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to export: {e}")
|
||||
print(f"Error exporting to PDF: {e}")
|
||||
|
||||
def load_sessions_from_csv(self):
|
||||
filename = "working_sessions.csv"
|
||||
if os.path.exists(filename):
|
||||
try:
|
||||
with open(filename, mode="r") as csvfile:
|
||||
reader = csv.reader(csvfile)
|
||||
header = next(reader, None)
|
||||
row_number = 1
|
||||
total_hours_loaded = False
|
||||
def calculate_total_time(self):
|
||||
total_seconds = sum(entry["duration"] for entry in self.log_entries)
|
||||
minutes, seconds = divmod(total_seconds, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
time_str = "{:02d}:{:02d}:{:02d}".format(
|
||||
int(hours), int(minutes), int(seconds)
|
||||
)
|
||||
self.total_time_label.config(text=f"Total Time: {time_str}")
|
||||
|
||||
for row in reader:
|
||||
if not row:
|
||||
print(f"Skipping empty row: {row_number}")
|
||||
row_number += 1
|
||||
continue
|
||||
def exit_app(self): # Define the exit_app method
|
||||
self.root.destroy() # Close the Tkinter window
|
||||
sys.exit()
|
||||
|
||||
if len(row) == 5:
|
||||
start_time_str, end_time_str, duration_str, decimal_hours_str, description = row
|
||||
elif len(row) == 4:
|
||||
if row[0] == '' and row[1] == '' and row[2] == '' and row[3] == 'Total Hours':
|
||||
try:
|
||||
self.total_hours = float(row[4])
|
||||
self.update_total_hours()
|
||||
total_hours_loaded = True
|
||||
print(f"Total hours loaded from row {row_number}: {self.total_hours}")
|
||||
row_number += 1
|
||||
continue
|
||||
|
||||
except ValueError:
|
||||
print(f"Skipping total hours row {row_number} due to parsing error")
|
||||
row_number += 1
|
||||
continue
|
||||
|
||||
start_time_str, end_time_str, duration_str, decimal_hours_str = row
|
||||
description = ""
|
||||
else:
|
||||
print(f"Skipping row with unexpected number of columns ({len(row)}): {row_number}")
|
||||
row_number += 1
|
||||
continue
|
||||
|
||||
if not start_time_str or not end_time_str:
|
||||
print(f"Skipping row with missing time data: {row_number}")
|
||||
row_number += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
start_time = datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S")
|
||||
duration = self.parse_duration(duration_str)
|
||||
self.sessions.append((start_time, end_time, duration, description))
|
||||
self.log_message(
|
||||
f"Loaded: {start_time.strftime('%Y-%m-%d %H:%M:%S')} - {end_time.strftime('%Y-%m-%d %H:%M:%S')}, "
|
||||
f"Duration: {duration_str}, Description: {description}"
|
||||
)
|
||||
except ValueError as ve:
|
||||
print(f"Skipping row {row_number} due to parsing error: {ve}")
|
||||
row_number += 1
|
||||
|
||||
if not total_hours_loaded:
|
||||
self.update_total_hours()
|
||||
print("Total hours row was not found so calculating from current rows!")
|
||||
except FileNotFoundError:
|
||||
messagebox.showinfo("Info", "No CSV file found. Starting with a new session.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to load sessions from CSV: {e}")
|
||||
|
||||
def parse_duration(self, duration_str):
|
||||
hours, minutes, seconds = map(int, duration_str.split(':'))
|
||||
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||
|
||||
def update_total_hours(self):
|
||||
total_duration = timedelta()
|
||||
for start, end, duration, description in self.sessions:
|
||||
total_duration += duration
|
||||
|
||||
self.total_hours = total_duration.total_seconds() / 3600
|
||||
self.total_hours_label.config(text=f"Total Hours Worked: {self.total_hours:.2f}")
|
||||
|
||||
def exit_app(self):
|
||||
self.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TimeLogix()
|
||||
app.mainloop()
|
||||
root = tk.Tk()
|
||||
app = TimeLogix(root)
|
||||
root.mainloop()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue