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:
		
							parent
							
								
									4d8502c68d
								
							
						
					
					
						commit
						bec228f9a3
					
				
					 1 changed files with 173 additions and 29 deletions
				
			
		
							
								
								
									
										202
									
								
								time_logix.py
									
										
									
									
									
								
							
							
						
						
									
										202
									
								
								time_logix.py
									
										
									
									
									
								
							|  | @ -11,6 +11,7 @@ from reportlab.platypus import Table, TableStyle, Paragraph | ||||||
| from reportlab.lib.styles import getSampleStyleSheet | from reportlab.lib.styles import getSampleStyleSheet | ||||||
| from reportlab.lib.units import inch | from reportlab.lib.units import inch | ||||||
| import sys | import sys | ||||||
|  | import os | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TimeLogix: | class TimeLogix: | ||||||
|  | @ -28,112 +29,201 @@ class TimeLogix: | ||||||
| 
 | 
 | ||||||
|         # --- App Data --- |         # --- App Data --- | ||||||
|         self.project_file = "projects.txt" |         self.project_file = "projects.txt" | ||||||
|  |         self.invoice_file = "invoice_number.txt"  # File to store invoice number | ||||||
|         self.projects = self.load_projects() |         self.projects = self.load_projects() | ||||||
|         self.is_running = False |         self.is_running = False | ||||||
|         self.start_time = None |         self.start_time = None | ||||||
|         self.elapsed_time = 0 |         self.elapsed_time = 0 | ||||||
|         self.timer_id = None |         self.timer_id = None | ||||||
|         self.log_entries = [] |         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_name = "Your Company Name" | ||||||
|         self.company_address = "123 Main St, Anytown, USA" |         self.company_address = "123 Main St, Anytown, USA" | ||||||
|         self.client_name = "Client Name" |         self.client_name = "Client Name" | ||||||
|         self.client_address = "Client Address" |         self.client_address = "Client Address" | ||||||
|         self.hourly_rate = 60.00 |         self.hourly_rate = 60.00 | ||||||
|  |         self.csv_file = "working_sessions.csv" | ||||||
| 
 | 
 | ||||||
|         # --- Scrollable Frame --- |         # --- 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) |         self.scrollable_frame.pack(fill="both", expand=True, padx=10, pady=10) | ||||||
| 
 | 
 | ||||||
|         # --- UI Elements --- |         # --- 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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.task_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.task_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.task_entry.pack(pady=(2, 10)) |         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_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)) |         self.project_combo.pack(pady=(2, 10)) | ||||||
|         if self.projects: |         if self.projects: | ||||||
|             self.project_combo.set(self.projects[0]) |             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.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.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_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 = ctk.CTkTextbox( | ||||||
|         self.log_text.pack(pady=5, padx=10, fill='both', expand=True) |             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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.new_project_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.new_project_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.new_project_entry.pack(pady=(2, 10)) |         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)) |         self.add_project_button.pack(pady=(5, 15)) | ||||||
| 
 | 
 | ||||||
|         # --- Button Frame --- |         # --- Button Frame --- | ||||||
|         button_frame = ctk.CTkFrame(self.scrollable_frame) |         button_frame = ctk.CTkFrame(self.scrollable_frame) | ||||||
|         button_frame.pack(pady=(10, 15)) |         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 = ctk.CTkButton( | ||||||
|         self.export_csv_button.pack(side="left", padx=5, pady=5)  # Using side="left" for horizontal layout |             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.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.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_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)) |         self.total_time_label.pack(pady=(5, 15)) | ||||||
| 
 | 
 | ||||||
|         # --- Settings UI --- |         # --- 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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.company_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.company_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.company_name_entry.pack(pady=(2, 10)) |         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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.company_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.company_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.company_address_entry.pack(pady=(2, 10)) |         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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.client_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.client_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.client_name_entry.pack(pady=(2, 10)) |         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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.client_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250) |         self.client_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250) | ||||||
|         self.client_address_entry.pack(pady=(2, 10)) |         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_label.pack(pady=(10, 2)) | ||||||
| 
 | 
 | ||||||
|         self.hourly_rate_entry = ctk.CTkEntry(self.scrollable_frame, width=100) |         self.hourly_rate_entry = ctk.CTkEntry(self.scrollable_frame, width=100) | ||||||
|         self.hourly_rate_entry.pack(pady=(2, 10)) |         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.update_settings_button.pack(pady=(15, 20)) | ||||||
| 
 | 
 | ||||||
|  |         self.load_log_entries() | ||||||
|  | 
 | ||||||
|     def load_projects(self): |     def load_projects(self): | ||||||
|         try: |         try: | ||||||
|             with open(self.project_file, "r") as f: |             with open(self.project_file, "r") as f: | ||||||
|  | @ -150,6 +240,23 @@ class TimeLogix: | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             print(f"Error saving projects: {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): |     def add_project(self): | ||||||
|         new_project = self.new_project_entry.get().strip() |         new_project = self.new_project_entry.get().strip() | ||||||
|         if new_project and new_project not in self.projects: |         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"Project: {entry['project']}\n") | ||||||
|             self.log_text.insert(tk.END, f"Start: {entry['start_time']}\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"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") |             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): |     def export_to_csv(self): | ||||||
|         try: |         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"] |                 fieldnames = ["task", "project", "start_time", "end_time", "duration"] | ||||||
|                 writer = csv.DictWriter(csvfile, fieldnames=fieldnames) |                 writer = csv.DictWriter(csvfile, fieldnames=fieldnames) | ||||||
|                 writer.writeheader() |                 writer.writeheader() | ||||||
|  | @ -266,11 +401,19 @@ class TimeLogix: | ||||||
|             c.drawString(4.5 * inch, 7.1 * inch, f"Date: {current_date}") |             c.drawString(4.5 * inch, 7.1 * inch, f"Date: {current_date}") | ||||||
| 
 | 
 | ||||||
|             # --- Client Info --- |             # --- 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.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.setFont("Helvetica", 10) | ||||||
|             c.drawString(inch, 6.3 * inch, self.client_name) |             c.drawString( | ||||||
|             c.drawString(inch, 6.1 * inch, self.client_address) |                 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 --- |             # --- Table --- | ||||||
|             data = [["Task", "Project", "Hours", "Rate", "Total"]] |             data = [["Task", "Project", "Hours", "Rate", "Total"]] | ||||||
|  | @ -330,6 +473,7 @@ class TimeLogix: | ||||||
|             c.save() |             c.save() | ||||||
|             print(f"Exported to PDF successfully as {filename}!") |             print(f"Exported to PDF successfully as {filename}!") | ||||||
|             self.invoice_number += 1 |             self.invoice_number += 1 | ||||||
|  |             self.save_invoice_number()  # Save the updated invoice number | ||||||
| 
 | 
 | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             print(f"Error exporting to PDF: {e}") |             print(f"Error exporting to PDF: {e}") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Blake Ridgway
						Blake Ridgway