feat: Remember generated invoice number

- Added `invoice_file` attribute to store the filename where the invoice number is persisted.
- Implemented `load_invoice_number` to read the last invoice number from the file on app startup. If the file doesn't exist or contains an invalid number, it defaults to 1.
- Implemented `save_invoice_number` to persist the incremented invoice number to the file after generating a PDF.
- Modified `export_to_pdf` to increment and save the invoice number after successful PDF generation.
This commit is contained in:
Blake Ridgway 2025-04-04 12:18:41 -05:00
parent 4d8502c68d
commit bec228f9a3

View file

@ -11,6 +11,7 @@ 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:
@ -28,112 +29,201 @@ class TimeLogix:
# --- 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 = 1
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 = 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 = 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 = 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 = 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 = 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 = 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 = 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.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 = 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 = 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_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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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:
@ -150,6 +240,23 @@ class TimeLogix:
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:
@ -229,12 +336,40 @@ class TimeLogix:
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, 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("working_sessions.csv", "w", newline="") as csvfile:
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()
@ -266,11 +401,19 @@ class TimeLogix:
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, 6.5 * inch, "Bill To:")
c.drawString(inch, bill_to_y, "Bill To:") # "Bill To:" label
c.setFont("Helvetica", 10)
c.drawString(inch, 6.3 * inch, self.client_name)
c.drawString(inch, 6.1 * inch, self.client_address)
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"]]
@ -330,6 +473,7 @@ class TimeLogix:
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}")