refactor: Create main.py as entry point, implement on close
This commit creates a dedicated entry point `main.py`, instantiates the application components, and handles the WM_DELETE_WINDOW protocol. - Removed window creation and mainloop from previous file. - Created a dedicated entry point with basic error handling. - Implemented an on_closing protocol to properly close db and exit application - Implemented the loading of database objects.
This commit is contained in:
parent
2c6c55a176
commit
1231673385
2 changed files with 22 additions and 497 deletions
22
main.py
Normal file
22
main.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import sys
|
||||||
|
from database import Database
|
||||||
|
from pdf_exporter import PDFExporter
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
db = Database()
|
||||||
|
pdf_exporter = PDFExporter()
|
||||||
|
app = MainWindow(db, pdf_exporter)
|
||||||
|
|
||||||
|
def on_closing():
|
||||||
|
db.close()
|
||||||
|
app.destroy() # Properly destroy the Tkinter window
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
app.protocol("WM_DELETE_WINDOW", on_closing) # Handle window closing
|
||||||
|
app.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
497
time_logix.py
497
time_logix.py
|
|
@ -1,497 +0,0 @@
|
||||||
import customtkinter as ctk
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
import time
|
|
||||||
import datetime
|
|
||||||
import csv
|
|
||||||
from reportlab.lib.pagesizes import letter
|
|
||||||
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 os
|
|
||||||
|
|
||||||
|
|
||||||
class TimeLogix:
|
|
||||||
def __init__(self):
|
|
||||||
self.root = ctk.CTk()
|
|
||||||
self.root.title("TimeLogix")
|
|
||||||
|
|
||||||
# Theme Configuration
|
|
||||||
ctk.set_appearance_mode("Dark") # Or "Light", "System"
|
|
||||||
ctk.set_default_color_theme("blue")
|
|
||||||
|
|
||||||
# --- Styling ---
|
|
||||||
self.font_family = "Segoe UI"
|
|
||||||
self.font_size = 12
|
|
||||||
|
|
||||||
# --- App Data ---
|
|
||||||
self.project_file = "projects.txt"
|
|
||||||
self.invoice_file = "invoice_number.txt" # File to store invoice number
|
|
||||||
self.projects = self.load_projects()
|
|
||||||
self.is_running = False
|
|
||||||
self.start_time = None
|
|
||||||
self.elapsed_time = 0
|
|
||||||
self.timer_id = None
|
|
||||||
self.log_entries = []
|
|
||||||
self.invoice_number = self.load_invoice_number() # Load invoice number
|
|
||||||
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.csv_file = "working_sessions.csv"
|
|
||||||
|
|
||||||
# --- Scrollable Frame ---
|
|
||||||
self.scrollable_frame = ctk.CTkScrollableFrame(
|
|
||||||
self.root, width=450, height=600
|
|
||||||
) # Consider making height adaptable
|
|
||||||
self.scrollable_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# --- UI Elements ---
|
|
||||||
self.task_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Task Description:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.task_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.task_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.task_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.project_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Project:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.project_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.project_combo = ctk.CTkComboBox(
|
|
||||||
self.scrollable_frame, values=self.projects, width=250
|
|
||||||
)
|
|
||||||
self.project_combo.pack(pady=(2, 10))
|
|
||||||
if self.projects:
|
|
||||||
self.project_combo.set(self.projects[0])
|
|
||||||
|
|
||||||
self.time_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame, text="00:00:00", font=(self.font_family, 36)
|
|
||||||
)
|
|
||||||
self.time_label.pack(pady=(15, 20))
|
|
||||||
|
|
||||||
self.start_stop_button = ctk.CTkButton(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Start",
|
|
||||||
command=self.toggle_timer,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.start_stop_button.pack(pady=(5, 20))
|
|
||||||
|
|
||||||
self.log_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Log Entries:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.log_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.log_text = ctk.CTkTextbox(
|
|
||||||
self.scrollable_frame,
|
|
||||||
width=400,
|
|
||||||
height=100,
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.log_text.pack(pady=5, padx=10, fill="both", expand=True)
|
|
||||||
|
|
||||||
self.new_project_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="New Project:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.new_project_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.new_project_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.new_project_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.add_project_button = ctk.CTkButton(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Add Project",
|
|
||||||
command=self.add_project,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.add_project_button.pack(pady=(5, 15))
|
|
||||||
|
|
||||||
# --- Button Frame ---
|
|
||||||
button_frame = ctk.CTkFrame(self.scrollable_frame)
|
|
||||||
button_frame.pack(pady=(10, 15))
|
|
||||||
|
|
||||||
self.export_csv_button = ctk.CTkButton(
|
|
||||||
button_frame,
|
|
||||||
text="Export to CSV",
|
|
||||||
command=self.export_to_csv,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.export_csv_button.pack(
|
|
||||||
side="left", padx=5, pady=5
|
|
||||||
) # Using side="left" for horizontal layout
|
|
||||||
|
|
||||||
self.export_pdf_button = ctk.CTkButton(
|
|
||||||
button_frame,
|
|
||||||
text="Export to PDF",
|
|
||||||
command=self.export_to_pdf,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.export_pdf_button.pack(side="left", padx=5, pady=5)
|
|
||||||
|
|
||||||
self.exit_button = ctk.CTkButton(
|
|
||||||
button_frame, text="Exit", command=self.exit_app, corner_radius=8
|
|
||||||
)
|
|
||||||
self.exit_button.pack(side="left", padx=5, pady=5)
|
|
||||||
|
|
||||||
self.total_time_button = ctk.CTkButton(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Calculate Total Time",
|
|
||||||
command=self.calculate_total_time,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.total_time_button.pack(pady=(5, 15))
|
|
||||||
|
|
||||||
self.total_time_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Total Time: 00:00:00",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.total_time_label.pack(pady=(5, 15))
|
|
||||||
|
|
||||||
# --- Settings UI ---
|
|
||||||
self.company_name_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Company Name:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.company_name_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.company_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.company_name_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.company_address_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Company Address:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.company_address_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.company_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.company_address_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.client_name_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Client Name:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.client_name_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.client_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.client_name_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.client_address_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Client Address:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.client_address_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.client_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
|
|
||||||
self.client_address_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.hourly_rate_label = ctk.CTkLabel(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Hourly Rate:",
|
|
||||||
font=(self.font_family, self.font_size),
|
|
||||||
)
|
|
||||||
self.hourly_rate_label.pack(pady=(10, 2))
|
|
||||||
|
|
||||||
self.hourly_rate_entry = ctk.CTkEntry(self.scrollable_frame, width=100)
|
|
||||||
self.hourly_rate_entry.pack(pady=(2, 10))
|
|
||||||
|
|
||||||
self.update_settings_button = ctk.CTkButton(
|
|
||||||
self.scrollable_frame,
|
|
||||||
text="Update Settings",
|
|
||||||
command=self.update_settings,
|
|
||||||
corner_radius=8,
|
|
||||||
)
|
|
||||||
self.update_settings_button.pack(pady=(15, 20))
|
|
||||||
|
|
||||||
self.load_log_entries()
|
|
||||||
|
|
||||||
def load_projects(self):
|
|
||||||
try:
|
|
||||||
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:
|
|
||||||
print(f"Error saving projects: {e}")
|
|
||||||
|
|
||||||
def load_invoice_number(self):
|
|
||||||
try:
|
|
||||||
with open(self.invoice_file, "r") as f:
|
|
||||||
return int(f.read().strip())
|
|
||||||
except FileNotFoundError:
|
|
||||||
return 1
|
|
||||||
except ValueError:
|
|
||||||
print("Invalid invoice number in file, resetting to 1.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def save_invoice_number(self):
|
|
||||||
try:
|
|
||||||
with open(self.invoice_file, "w") as f:
|
|
||||||
f.write(str(self.invoice_number))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving invoice number: {e}")
|
|
||||||
|
|
||||||
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.configure(values=self.projects)
|
|
||||||
self.project_combo.set(new_project)
|
|
||||||
self.save_projects()
|
|
||||||
self.new_project_entry.delete(0, tk.END)
|
|
||||||
elif new_project in self.projects:
|
|
||||||
print("Project already exists.")
|
|
||||||
else:
|
|
||||||
print("Project name cannot be empty.")
|
|
||||||
|
|
||||||
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:
|
|
||||||
self.hourly_rate = float(self.hourly_rate_entry.get())
|
|
||||||
except ValueError:
|
|
||||||
print("Invalid hourly rate. Using default.")
|
|
||||||
self.hourly_rate = 50.00
|
|
||||||
|
|
||||||
def toggle_timer(self):
|
|
||||||
if self.is_running:
|
|
||||||
self.stop_timer()
|
|
||||||
else:
|
|
||||||
self.start_timer()
|
|
||||||
|
|
||||||
def start_timer(self):
|
|
||||||
self.is_running = True
|
|
||||||
self.start_stop_button.configure(text="Stop")
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.update_timer()
|
|
||||||
|
|
||||||
def stop_timer(self):
|
|
||||||
self.is_running = False
|
|
||||||
self.start_stop_button.configure(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.configure(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.configure(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 load_log_entries(self):
|
|
||||||
if os.path.exists(self.csv_file):
|
|
||||||
try:
|
|
||||||
with open(self.csv_file, "r") as csvfile:
|
|
||||||
reader = csv.DictReader(csvfile)
|
|
||||||
for row in reader:
|
|
||||||
try:
|
|
||||||
row["duration"] = float(row["duration"])
|
|
||||||
except ValueError:
|
|
||||||
# Try converting duration from hh:mm:ss format to seconds
|
|
||||||
try:
|
|
||||||
h, m, s = map(int, row["duration"].split(":"))
|
|
||||||
row["duration"] = h * 3600 + m * 60 + s
|
|
||||||
except ValueError:
|
|
||||||
print(
|
|
||||||
f"Invalid duration format: {row['duration']}. "
|
|
||||||
"Skipping this entry."
|
|
||||||
)
|
|
||||||
continue # Skip this entry if the format is invalid
|
|
||||||
|
|
||||||
self.log_entries.append(row)
|
|
||||||
self.update_log_display()
|
|
||||||
print("Loaded log entries from CSV successfully!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading log entries from CSV: {e}")
|
|
||||||
|
|
||||||
def export_to_csv(self):
|
|
||||||
try:
|
|
||||||
with open(self.csv_file, "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 ---
|
|
||||||
bill_to_y = 6.5 * inch # Starting y position for "Bill To:"
|
|
||||||
line_height = 0.2 * inch # Height for each line of text
|
|
||||||
|
|
||||||
c.setFont("Helvetica-Bold", 12)
|
|
||||||
c.drawString(inch, bill_to_y, "Bill To:") # "Bill To:" label
|
|
||||||
|
|
||||||
c.setFont("Helvetica", 10)
|
|
||||||
c.drawString(
|
|
||||||
inch, bill_to_y - line_height, self.client_name
|
|
||||||
) # Client Name
|
|
||||||
c.drawString(
|
|
||||||
inch, bill_to_y - 2 * line_height, self.client_address
|
|
||||||
) # 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)
|
|
||||||
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
|
|
||||||
self.save_invoice_number() # Save the updated invoice number
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error exporting to PDF: {e}")
|
|
||||||
|
|
||||||
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.configure(text=f"Total Time: {time_str}")
|
|
||||||
|
|
||||||
def exit_app(self):
|
|
||||||
self.root.destroy()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = TimeLogix()
|
|
||||||
app.root.mainloop()
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue