turns-00049.parquet:14133
83b20aa483350801e2ff6f7f
turn 7/8gpt-4o-mini-2024-07-18EnglishSouth Africa5101 words
degenerate_repetitionAbsentFinal dense release
USER
please revise and do not omit anything provide the full code snip.
ASSISTANT
Certainly! Below is the complete and corrected version of your code for the invoicing application, including all necessary components and functionalities, merged and structured properly without omitting anything.
### Complete Code for the Invoicing Application
```python
import sys
import json
import os
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
import pandas as pd
import pdfplumber # Added for PDF parsing
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QTabWidget, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QMessageBox, QComboBox,
QTextEdit, QListWidget, QListWidgetItem, QFileDialog, QSpinBox, QDialog
)
# Define file paths
CLIENTS_FILE = 'clients.json'
LINE_ITEMS_FILE = 'line_items.json'
INVOICES_DIR = 'invoices'
EMAIL_CONFIG_FILE = 'email_config.json'
TEMPLATES_FILE = 'templates.json'
COMPANY_DETAILS_FILE = 'company_details.json' # New file for company details
INVOICES_METADATA_FILE = 'invoices.json' # New file for invoice metadata
# Ensure necessary directories exist
os.makedirs(INVOICES_DIR, exist_ok=True)
# Function to load data from JSON
def load_json(file_path):
if os.path.exists(file_path):
with open(file_path, 'r') as f:
return json.load(f)
return {}
# Function to save data to JSON
def save_json(data, file_path):
with open(file_path, 'w') as f:
json.dump(data, f, indent=4)
# GUI Application
class QuickBooksReplacement(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.clients = load_json(CLIENTS_FILE)
self.line_items_data = load_json(LINE_ITEMS_FILE)
self.email_config = load_json(EMAIL_CONFIG_FILE)
self.templates = load_json(TEMPLATES_FILE)
self.company_details = load_json(COMPANY_DETAILS_FILE) # Initialize company details
self.invoices_metadata = load_json(INVOICES_METADATA_FILE) # Initialize invoice metadata
self.invoice_counter = self.get_next_invoice_number() # Initialize invoice counter
self.init_ui()
def get_next_invoice_number(self):
"""Gets the next available invoice number."""
if os.path.exists("invoice_counter.txt"):
with open("invoice_counter.txt", "r") as f:
try:
counter = int(f.read())
return counter + 1
except ValueError:
return 1
else:
return 1
def update_invoice_counter(self):
"""Updates the invoice counter file."""
with open("invoice_counter.txt", "w") as f:
f.write(str(self.invoice_counter))
def init_ui(self):
self.setWindowTitle("QuickBooks Replacement Tool")
self.setGeometry(100, 100, 1400, 900) # Increased width and height for better layout
layout = QVBoxLayout()
# Apply Enhanced Dark Theme
self.apply_dark_theme()
# Tab Widget
self.tabs = QTabWidget()
layout.addWidget(self.tabs)
# Tabs
self.client_tab = ClientManagementTab(self)
self.line_item_tab = LineItemManagementTab(self)
self.invoice_tab = InvoiceCreationTab(self)
self.manage_invoices_tab = ManageInvoicesTab(self)
self.financial_statements_tab = FinancialStatementsTab(self)
self.email_config_tab = EmailConfigTab(self)
self.company_details_tab = CompanyDetailsTab(self)
self.templates_tab = TemplateManagementTab(self)
self.tabs.addTab(self.client_tab, QtGui.QIcon.fromTheme("user-group"), "Clients")
self.tabs.addTab(self.line_item_tab, QtGui.QIcon.fromTheme("list-add"), "Line Items")
self.tabs.addTab(self.invoice_tab, QtGui.QIcon.fromTheme("document-new"), "Create Invoice")
self.tabs.addTab(self.manage_invoices_tab, QtGui.QIcon.fromTheme("folder"), "Manage Invoices")
self.tabs.addTab(self.financial_statements_tab, QtGui.QIcon.fromTheme("view-summary"), "Financial Statements")
self.tabs.addTab(self.email_config_tab, QtGui.QIcon.fromTheme("email-send"), "Email Settings")
self.tabs.addTab(self.company_details_tab, QtGui.QIcon.fromTheme("office-address"), "Company Details")
self.tabs.addTab(self.templates_tab, QtGui.QIcon.fromTheme("format-paint"), "Templates")
# Log Message Area
self.log_area = QTextEdit(self)
self.log_area.setReadOnly(True)
self.log_area.setStyleSheet("""
background-color: #1E1E1E;
color: #ffffff;
font-family: Consolas;
font-size: 12px;
border: 1px solid #555555;
""")
log_layout = QVBoxLayout()
log_layout.addWidget(QLabel("Log:"))
log_layout.addWidget(self.log_area)
layout.addLayout(log_layout)
self.setLayout(layout)
def apply_dark_theme(self):
dark_stylesheet = """
/* General Widget Styles */
QWidget {
background-color: #121212;
color: #ffffff;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QLabel {
color: #ffffff;
}
QLineEdit, QTextEdit, QComboBox, QListWidget, QListWidgetItem, QSpinBox, QPushButton {
background-color: #1E1E1E;
border: 1px solid #555555;
color: #ffffff;
padding: 5px;
border-radius: 4px;
}
QLineEdit:focus, QTextEdit:focus, QComboBox:focus, QListWidget:focus, QSpinBox:focus, QPushButton:focus {
border: 2px solid #1E90FF;
}
QPushButton {
background-color: #2D2D30;
}
QPushButton:hover {
background-color: #3E3E42;
}
QPushButton:pressed {
background-color: #5A5A5E;
}
QTabWidget::pane {
border: 1px solid #555555;
}
QTabBar::tab {
background: #2D2D30;
border: 1px solid #555555;
padding: 10px;
margin: 2px;
color: #ffffff;
border-radius: 4px;
}
QTabBar::tab:selected {
background: #1E90FF;
color: #ffffff;
}
QMenuBar, QMenu {
background-color: #121212;
color: #ffffff;
font-size: 14px;
}
QMenu::item:selected {
background-color: #3E3E42;
}
QListWidget::item:selected {
background-color: #1E90FF;
color: #ffffff;
}
QHeaderView::section {
background-color: #2D2D30;
color: #ffffff;
padding: 4px;
border: 1px solid #555555;
}
QDialog {
background-color: #121212;
color: #ffffff;
}
QDialog QPushButton {
min-width: 80px;
}
"""
self.setStyleSheet(dark_stylesheet)
def log_message(self, message):
"""Logs message to the text area."""
self.log_area.append(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {message}")
# Client Management Tab
class ClientManagementTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui__()
def init_ui(self):
layout = QVBoxLayout()
# Form to add/edit client
form_group = QtWidgets.QGroupBox("Add New Client")
form_layout = QHBoxLayout()
self.client_name_input = QLineEdit(self)
self.client_name_input.setPlaceholderText("Client Name")
self.client_email_input = QLineEdit(self)
self.client_email_input.setPlaceholderText("Client Email")
self.client_address_input = QLineEdit(self)
self.client_address_input.setPlaceholderText("Client Address")
self.client_vat_input = QLineEdit(self) # VAT Number Field
self.client_vat_input.setPlaceholderText("Client VAT Number")
self.add_client_button = QPushButton("Add Client")
self.add_client_button.clicked.connect(self.add_client)
form_layout.addWidget(QLabel("Name:"))
form_layout.addWidget(self.client_name_input)
form_layout.addWidget(QLabel("Email:"))
form_layout.addWidget(self.client_email_input)
form_layout.addWidget(QLabel("Address:"))
form_layout.addWidget(self.client_address_input)
form_layout.addWidget(QLabel("VAT Number:")) # Label for VAT
form_layout.addWidget(self.client_vat_input) # VAT Input
form_layout.addWidget(self.add_client_button)
form_group.setLayout(form_layout)
layout.addWidget(form_group)
# List of clients
self.clients_list = QListWidget()
self.load_clients()
layout.addWidget(QLabel("Existing Clients:"))
layout.addWidget(self.clients_list)
# Buttons to edit and delete clients
btn_layout = QHBoxLayout()
self.edit_client_button = QPushButton("Edit Selected")
self.edit_client_button.clicked.connect(self.edit_client)
self.delete_client_button = QPushButton("Delete Selected")
self.delete_client_button.clicked.connect(self.delete_client)
btn_layout.addWidget(self.edit_client_button)
btn_layout.addWidget(self.delete_client_button)
layout.addLayout(btn_layout)
self.setLayout(layout)
def load_clients(self):
self.clients_list.clear()
for client in self.parent.clients.keys():
item = QListWidgetItem(client)
self.clients_list.addItem(item)
def add_client(self):
name = self.client_name_input.text().strip()
email = self.client_email_input.text().strip()
address = self.client_address_input.text().strip()
vat = self.client_vat_input.text().strip() # Retrieve VAT number
if not name or not email or not address:
QMessageBox.warning(self, "Input Error", "Please fill in all required fields.")
return
if name in self.parent.clients:
QMessageBox.warning(self, "Duplicate Client", "Client already exists.")
return
self.parent.clients[name] = {
"email": email,
"address": address,
"vat_number": vat # Store VAT number
}
save_json(self.parent.clients, CLIENTS_FILE)
self.parent.log_message(f"Client '{name}' added.")
self.load_clients()
self.client_name_input.clear()
self.client_email_input.clear()
self.client_address_input.clear()
self.client_vat_input.clear()
def edit_client(self):
selected = self.clients_list.currentItem()
if not selected:
QMessageBox.warning(self, "Selection Error", "Please select a client to edit.")
return
name = selected.text()
client_data = self.parent.clients[name]
# Dialog to edit client
dialog = EditClientDialog(name, client_data, self.parent)
if dialog.exec():
new_name, new_email, new_address, new_vat = dialog.get_data()
if new_name != name and new_name in self.parent.clients:
QMessageBox.warning(self, "Duplicate Client", "Client name already exists.")
return
# Update client
del self.parent.clients[name]
self.parent.clients[new_name] = {
"email": new_email,
"address": new_address,
"vat_number": new_vat # Update VAT number
}
save_json(self.parent.clients, CLIENTS_FILE)
self.parent.log_message(f"Client '{name}' updated to '{new_name}'.")
self.load_clients()
def delete_client(self):
selected = self.clients_list.currentItem()
if not selected:
QMessageBox.warning(self, "Selection Error", "Please select a client to delete.")
return
name = selected.text()
reply = QMessageBox.question(
self, 'Delete Confirmation',
f"Are you sure you want to delete client '{name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
del self.parent.clients[name]
save_json(self.parent.clients, CLIENTS_FILE)
self.parent.log_message(f"Client '{name}' deleted.")
self.load_clients()
class EditClientDialog(QDialog):
def __init__(self, name, data, parent):
super().__init__()
self.setWindowTitle("Edit Client")
self.parent = parent
self.name = name
self.data = data
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout()
self.name_input = QLineEdit(self)
self.name_input.setText(self.name)
self.email_input = QLineEdit(self)
self.email_input.setText(self.data.get("email", ""))
self.address_input = QLineEdit(self)
self.address_input.setText(self.data.get("address", ""))
self.vat_input = QLineEdit(self) # VAT Number Field
self.vat_input.setText(self.data.get("vat_number", "")) # Retrieve VAT number
self.layout.addWidget(QLabel("Name:"))
self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Email:"))
self.layout.addWidget(self.email_input)
self.layout.addWidget(QLabel("Address:"))
self.layout.addWidget(self.address_input)
self.layout.addWidget(QLabel("VAT Number:")) # Label for VAT
self.layout.addWidget(self.vat_input) # VAT Input
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.setLayout(self.layout)
def get_data(self):
return (
self.name_input.text().strip(),
self.email_input.text().strip(),
self.address_input.text().strip(),
self.vat_input.text().strip() # Return VAT number
)
# Line Item Management Tab
class LineItemManagementTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Form to add/edit line item
form_group = QtWidgets.QGroupBox("Add New Line Item")
form_layout = QHBoxLayout()
self.item_name_input = QLineEdit(self)
self.item_name_input.setPlaceholderText("Item Name")
self.item_price_input = QLineEdit(self)
self.item_price_input.setPlaceholderText("Item Price")
self.add_item_button = QPushButton("Add Item")
self.add_item_button.clicked.connect(self.add_line_item)
form_layout.addWidget(QLabel("Name:"))
form_layout.addWidget(self.item_name_input)
form_layout.addWidget(QLabel("Price:"))
form_layout.addWidget(self.item_price_input)
form_layout.addWidget(self.add_item_button)
form_group.setLayout(form_layout)
layout.addWidget(form_group)
# Supplier Section (Retained for internal management but not displayed in invoices)
supplier_group = QtWidgets.QGroupBox("Supplier Information (Internal Use)")
supplier_layout = QHBoxLayout()
self.supplier_input = QLineEdit(self)
self.supplier_input.setPlaceholderText("Supplier Name")
supplier_layout.addWidget(QLabel("Supplier:"))
supplier_layout.addWidget(self.supplier_input)
supplier_group.setLayout(supplier_layout)
layout.addWidget(supplier_group)
# List of line items
self.items_list = QListWidget()
self.load_line_items()
layout.addWidget(QLabel("Existing Line Items:"))
layout.addWidget(self.items_list)
# Buttons to edit, delete, and import line items
btn_layout = QHBoxLayout()
self.edit_item_button = QPushButton("Edit Selected")
self.edit_item_button.clicked.connect(self.edit_line_item)
self.delete_item_button = QPushButton("Delete Selected")
self.delete_item_button.clicked.connect(self.delete_line_item)
self.import_pdf_button = QPushButton("Import from PDF") # New Import Button
self.import_pdf_button.clicked.connect(self.import_from_pdf)
btn_layout.addWidget(self.edit_item_button)
btn_layout.addWidget(self.delete_item_button)
btn_layout.addWidget(self.import_pdf_button) # Add Import Button to Layout
layout.addLayout(btn_layout)
self.setLayout(layout)
def load_line_items(self):
self.items_list.clear()
for item, details in self.parent.line_items_data.items():
price = details.get('price', 0.0)
self.items_list.addItem(f"{item} - Price: R{price:.2f}")
def add_line_item(self):
name = self.item_name_input.text().strip()
price = self.item_price_input.text().strip()
supplier = self.supplier_input.text().strip() # Optional, for internal use
if not name or not price:
QMessageBox.warning(self, "Input Error", "Please fill in the Item Name and Price.")
return
try:
price = float(price)
if price < 0:
raise ValueError
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid positive price.")
return
if name in self.parent.line_items_data:
QMessageBox.warning(self, "Duplicate Item", "Line item already exists.")
return
self.parent.line_items_data[name] = {"price": price, "supplier": supplier}
save_json(self.parent.line_items_data, LINE_ITEMS_FILE)
self.parent.log_message(f"Line item '{name}' added.")
self.load_line_items()
self.item_name_input.clear()
self.item_price_input.clear()
self.supplier_input.clear()
def edit_line_item(self):
selected = self.items_list.currentItem()
if not selected:
QMessageBox.warning(self, "Selection Error", "Please select a line item to edit.")
return
item_text = selected.text()
name = item_text.split(" - Price: ")[0]
item_data = self.parent.line_items_data[name]
# Dialog to edit line item
dialog = EditLineItemDialog(name, item_data, self.parent)
if dialog.exec():
new_name, new_price, new_supplier = dialog.get_data()
if new_name != name and new_name in self.parent.line_items_data:
QMessageBox.warning(self, "Duplicate Item", "Line item name already exists.")
return
try:
new_price = float(new_price)
if new_price < 0:
raise ValueError
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid positive price.")
return
# Update line item
del self.parent.line_items_data[name]
self.parent.line_items_data[new_name] = {"price": new_price, "supplier": new_supplier}
save_json(self.parent.line_items_data, LINE_ITEMS_FILE)
self.parent.log_message(f"Line item '{name}' updated to '{new_name}'.")
self.load_line_items()
def delete_line_item(self):
selected = self.items_list.currentItem()
if not selected:
QMessageBox.warning(self, "Selection Error", "Please select a line item to delete.")
return
item_text = selected.text()
name = item_text.split(" - Price: ")[0]
reply = QMessageBox.question(
self, 'Delete Confirmation',
f"Are you sure you want to delete line item '{name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
del self.parent.line_items_data[name]
save_json(self.parent.line_items_data, LINE_ITEMS_FILE)
self.parent.log_message(f"Line item '{name}' deleted.")
self.load_line_items()
def import_from_pdf(self):
"""Imports line items from a selected PDF file."""
file_path, _ = QFileDialog.getOpenFileName(self, "Select PDF", "", "PDF Files (*.pdf)")
if not file_path:
return
try:
with pdfplumber.open(file_path) as pdf:
imported_items = 0
for page in pdf.pages:
tables = page.extract_tables()
for table in tables:
if not table:
continue
headers = table[0]
if all(header in headers for header in ["Item", "Quantity", "Price", "Total"]):
item_idx = headers.index("Item")
quantity_idx = headers.index("Quantity")
price_idx = headers.index("Price")
total_idx = headers.index("Total")
for row in table[1:]:
if len(row) < 4:
continue
item_name = row[item_idx].strip()
quantity = row[quantity_idx].strip()
price = row[price_idx].strip().replace('R', '').replace(',', '')
total = row[total_idx].strip().replace('R', '').replace(',', '')
if not item_name:
continue
try:
quantity = int(quantity)
price = float(price)
total = float(total)
except ValueError:
self.parent.log_message(f"Invalid data in PDF for item '{item_name}'. Skipping.")
continue
# Add to line items
if item_name not in self.parent.line_items_data:
self.parent.line_items_data[item_name] = {
"price": price,
"supplier": "" # Supplier not included as per requirement
}
imported_items += 1
else:
self.parent.log_message(f"Line item '{item_name}' already exists. Skipping import.")
if imported_items > 0:
save_json(self.parent.line_items_data, LINE_ITEMS_FILE)
self.parent.log_message(f"Imported {imported_items} line items from PDF.")
QMessageBox.information(self, "Import Successful", f"Imported {imported_items} line items from the PDF.")
self.load_line_items()
else:
QMessageBox.information(self, "Import Result", "No new line items were imported from the PDF.")
except Exception as e:
QMessageBox.warning(self, "Import Error", f"Failed to import from PDF: {e}")
self.parent.log_message(f"Failed to import from PDF '{file_path}': {e}")
# Dialog for editing a line item
class EditLineItemDialog(QDialog):
def __init__(self, name, data, parent):
super().__init__()
self.setWindowTitle("Edit Line Item")
self.parent = parent
self.name = name
self.data = data
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout()
self.name_input = QLineEdit(self)
self.name_input.setText(self.name)
self.price_input = QLineEdit(self)
self.price_input.setText(str(self.data["price"]))
self.layout.addWidget(QLabel("Name:"))
self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Price:"))
self.layout.addWidget(self.price_input)
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.setLayout(self.layout)
def get_data(self):
return (
self.name_input.text().strip(),
self.price_input.text().strip()
)
# Invoice Creation Tab
class InvoiceCreationTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.line_items = []
self.selected_template = None # For template usage
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Template Selection
template_layout = QHBoxLayout()
self.template_combo = QComboBox(self)
self.load_templates()
self.template_combo.currentIndexChanged.connect(self.change_template)
template_layout.addWidget(QLabel("Select Template:"))
template_layout.addWidget(self.template_combo)
layout.addLayout(template_layout)
# Client selection
client_layout = QHBoxLayout()
self.client_combo = QComboBox(self)
self.load_clients()
client_layout.addWidget(QLabel("Select Client:"))
client_layout.addWidget(self.client_combo)
layout.addLayout(client_layout)
# VAT Number Display (Read-only)
vat_display_layout = QHBoxLayout()
self.vat_display = QLineEdit(self)
self.vat_display.setReadOnly(True)
vat_display_layout.addWidget(QLabel("Client VAT Number:"))
vat_display_layout.addWidget(self.vat_display)
layout.addLayout(vat_display_layout)
# Update VAT display when client changes
self.client_combo.currentIndexChanged.connect(self.update_vat_display)
self.update_vat_display() # Initialize VAT display
# Line Items selection
line_item_layout = QHBoxLayout()
self.item_combo = QComboBox(self)
self.load_line_items()
self.quantity_input = QSpinBox(self)
self.quantity_input.setMinimum(1)
self.quantity_input.setValue(1)
self.add_item_button = QPushButton("Add Item")
self.add_item_button.clicked.connect(self.add_line_item)
line_item_layout.addWidget(QLabel("Item:"))
line_item_layout.addWidget(self.item_combo)
line_item_layout.addWidget(QLabel("Quantity:"))
line_item_layout.addWidget(self.quantity_input)
line_item_layout.addWidget(self.add_item_button)
layout.addLayout(line_item_layout)
# List to display added line items without supplier info
self.added_items_list = QListWidget()
self.added_items_list.setStyleSheet("""
QListWidget {
background-color: #1E1E1E;
color: #ffffff;
border: 1px solid #555555;
}
QListWidget::item {
padding: 5px;
}
""")
layout.addWidget(QLabel("Added Line Items:"))
layout.addWidget(self.added_items_list)
# Discounts and Taxes
discounts_taxes_layout = QHBoxLayout()
self.discount_input = QLineEdit(self)
self.discount_input.setPlaceholderText("Discount (%)")
self.tax_rate_input = QLineEdit(self)
self.tax_rate_input.setPlaceholderText("Tax Rate (%)")
discounts_taxes_layout.addWidget(QLabel("Discount (%):"))
discounts_taxes_layout.addWidget(self.discount_input)
discounts_taxes_layout.addWidget(QLabel("Tax Rate (%):"))
discounts_taxes_layout.addWidget(self.tax_rate_input)
layout.addLayout(discounts_taxes_layout)
# Output Format Selection
output_layout = QHBoxLayout()
self.output_format_combo = QComboBox(self)
self.output_format_combo.addItems(["PDF", "Excel"])
output_layout.addWidget(QLabel("Output Format:"))
output_layout.addWidget(self.output_format_combo)
layout.addLayout(output_layout)
# Button to create invoice
self.create_invoice_button = QPushButton("Create Invoice")
self.create_invoice_button.clicked.connect(self.create_invoice)
layout.addWidget(self.create_invoice_button)
self.setLayout(layout)
def load_templates(self):
self.template_combo.clear()
templates_list = list(self.parent.templates.keys())
self.template_combo.addItems(["Default"] + templates_list)
self.selected_template = "Default"
def change_template(self, index):
selected = self.template_combo.currentText()
if selected == "Default":
self.selected_template = None
else:
self.selected_template = selected
def load_clients(self):
self.client_combo.clear()
self.client_combo.addItems(list(self.parent.clients.keys()))
def load_line_items(self):
self.item_combo.clear()
self.item_combo.addItems(list(self.parent.line_items_data.keys()))
def add_line_item(self):
item_name = self.item_combo.currentText()
quantity = self.quantity_input.value()
if not item_name:
QMessageBox.warning(self, "Selection Error", "Please select a line item.")
return
price = self.parent.line_items_data[item_name]['price']
total = price * quantity
self.line_items.append({
"item": item_name,
"quantity": quantity,
"price": price,
"total": total
})
self.update_added_items_list()
def update_added_items_list(self):
self.added_items_list.clear()
for idx, item in enumerate(self.line_items, 1):
self.added_items_list.addItem(
f"{idx}. {item['item']} - Qty: {item['quantity']} - Price: R{item['price']:.2f} - Total: R{item['total']:.2f}"
)
def update_vat_display(self):
client_name = self.client_combo.currentText()
vat_number = self.parent.clients.get(client_name, {}).get("vat_number", "")
self.vat_display.setText(vat_number)
def create_invoice(self):
client_name = self.client_combo.currentText()
vat_number = self.parent.clients.get(client_name, {}).get("vat_number", "")
discount = self.discount_input.text().strip()
tax_rate = self.tax_rate_input.text().strip()
if not client_name:
QMessageBox.warning(self, "Input Error", "Please select a client.")
return
if not self.line_items:
QMessageBox.warning(self, "Input Error", "Please add at least one line item.")
return
# Calculate totals
subtotal = sum(item['total'] for item in self.line_items)
discount_amount = 0
if discount:
try:
discount_percentage = float(discount)
discount_amount = subtotal * (discount_percentage / 100)
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid discount percentage.")
return
subtotal_after_discount = subtotal - discount_amount
tax_amount = 0
if tax_rate:
try:
tax_percentage = float(tax_rate)
tax_amount = subtotal_after_discount * (tax_percentage / 100)
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid tax rate percentage.")
return
total = subtotal_after_discount + tax_amount
invoice_date = datetime.now().strftime("%Y-%m-%d") # Automatic date filling
client_address = self.parent.clients[client_name]["address"]
invoice_filename = f"{client_name.replace(' ', '_')}_invoice_{self.parent.invoice_counter}"
# Prepare DataFrame without supplier info
df = pd.DataFrame([
{"Item": item['item'], "Quantity": item['quantity'], "Price (ZAR)": item['price'], "Total (ZAR)": item['total']}
for item in self.line_items
])
output_format = self.output_format_combo.currentText()
success = False
if output_format == "PDF":
success = self.create_pdf(client_name, vat_number, invoice_date, client_address, df, invoice_filename, subtotal, discount_amount, tax_amount, total)
elif output_format == "Excel":
success = self.create_excel(client_name, vat_number, invoice_date, client_address, df, invoice_filename, subtotal, discount_amount, tax_amount, total)
else:
QMessageBox.warning(self, "Format Error", "Please select a valid output format.")
return
if success:
# Log invoice metadata
invoice_metadata = {
"invoice_number": self.parent.invoice_counter - 1,
"client_name": client_name,
"date": invoice_date,
"total": total,
"status": "Unpaid",
"filename": f"{invoice_filename}.{output_format.lower()}"
}
self.parent.invoices_metadata[str(self.parent.invoice_counter - 1)] = invoice_metadata
save_json(self.parent.invoices_metadata, INVOICES_METADATA_FILE)
# Reset form
self.line_items = []
self.update_added_items_list()
self.parent.invoice_counter += 1 # Increment invoice counter after successful creation
self.parent.update_invoice_counter()
def apply_template_attributes(self, c, template, invoice_details):
"""Applies template attributes like header, footer, and logo to the PDF."""
if 'header_text' in template:
c.setFont("Helvetica-Bold", 16)
c.drawString(50, invoice_details['height'] - 50, template['header_text'])
if 'footer_text' in template:
c.setFont("Helvetica", 10)
c.drawString(50, 30, template['footer_text'])
if 'logo_path' in template and template['logo_path']:
if os.path.exists(template['logo_path']):
c.drawImage(template['logo_path'], invoice_details['width'] - 150, invoice_details['height'] - 100, width=100, height=50)
def create_pdf(self, client, vat, invoice_date, client_address, df, invoice_filename, subtotal, discount, tax, total):
"""Generate PDF invoice."""
pdf_filename = os.path.join(INVOICES_DIR, f"{invoice_filename}.pdf")
template = self.get_selected_template()
c = canvas.Canvas(pdf_filename, pagesize=letter)
width, height = letter
# Apply Template Attributes
invoice_details = {'width': width, 'height': height}
self.apply_template_attributes(c, template, invoice_details)
# Company Details
company_name = self.parent.company_details.get("company_name", "Your Company Name")
company_address = self.parent.company_details.get("company_address", "Your Company Address")
company_email = self.parent.company_details.get("company_email", "your.email@example.com")
company_phone = self.parent.company_details.get("company_phone", "123-456-7890")
# Optional: Company logo
if self.parent.company_details.get("company_logo_path", "") and os.path.exists(self.parent.company_details["company_logo_path"]):
c.drawImage(self.parent.company_details["company_logo_path"], 50, height - 100, width=100, height=50)
c.setFont("Helvetica-Bold", 14)
c.drawString(50, height - 120, company_name)
c.setFont("Helvetica", 12)
c.drawString(50, height - 140, company_address)
c.drawString(50, height - 160, f"Email: {company_email}")
c.drawString(50, height - 180, f"Phone: {company_phone}")
# Invoice Header
c.setFont("Helvetica-Bold", 20)
c.drawString(400, height - 120, "INVOICE") # Adjusted position
# Invoice Information
c.setFont("Helvetica", 12)
c.drawString(400, height - 150, f"Invoice Number: {self.parent.invoice_counter - 1}") # Displaying counter
c.drawString(400, height - 170, f"Date: {invoice_date}")
# Client Information
c.setFont("Helvetica-Bold", 12)
c.drawString(50, height - 220, "Bill To:")
c.setFont("Helvetica", 12)
c.drawString(50, height - 240, f"{client}")
c.drawString(50, height - 260, f"Address: {client_address}")
if vat:
c.drawString(50, height - 280, f"VAT Number: {vat}")
# Draw a box around client information
c.rect(40, height - 290, 500, 60, stroke=1, fill=0)
# Table Headers
c.setFont("Helvetica-Bold", 12)
y = height - 350
c.drawString(50, y, "Item")
c.drawString(250, y, "Quantity")
c.drawString(350, y, "Price (ZAR)")
c.drawString(450, y, "Total (ZAR)")
y -= 20
c.line(50, y, 550, y)
# Table Content
c.setFont("Helvetica", 12)
for index, row in df.iterrows():
y -= 20
if y < 150:
c.showPage()
y = height - 50
# Re-apply template on new page
self.apply_template_attributes(c, template, invoice_details)
# Re-draw company details
c.setFont("Helvetica-Bold", 14)
c.drawString(50, height - 120, company_name)
c.setFont("Helvetica", 12)
c.drawString(50, height - 140, company_address)
c.drawString(50, height - 160, f"Email: {company_email}")
c.drawString(50, height - 180, f"Phone: {company_phone}")
# Re-draw invoice header
c.setFont("Helvetica-Bold", 20)
c.drawString(400, height - 120, "INVOICE")
c.setFont("Helvetica", 12)
c.drawString(400, height - 150, f"Invoice Number: {self.parent.invoice_counter - 1}")
c.drawString(400, height - 170, f"Date: {invoice_date}")
# Re-draw client information
c.setFont("Helvetica-Bold", 12)
c.drawString(50, height - 220, "Bill To:")
c.setFont("Helvetica", 12)
c.drawString(50, height - 240, f"{client}")
c.drawString(50, height - 260, f"Address: {client_address}")
if vat:
c.drawString(50, height - 280, f"VAT Number: {vat}")
# Re-draw table headers
c.setFont("Helvetica-Bold", 12)
y = height - 300
c.drawString(50, y, "Item")
c.drawString(250, y, "Quantity")
c.drawString(350, y, "Price (ZAR)")
c.drawString(450, y, "Total (ZAR)")
y -= 20
c.line(50, y, 550, y)
c.drawString(50, y, str(row['Item']))
c.drawString(250, y, str(row['Quantity']))
c.drawString(350, y, f"R{row['Price (ZAR)']:.2f}")
c.drawString(450, y, f"R{row['Total (ZAR)']:.2f}")
# Draw lines around each row
c.line(45, y - 5, 555, y - 5)
# Totals
y -= 40
c.setFont("Helvetica-Bold", 12)
c.drawString(350, y, "Subtotal:")
c.drawString(450, y, f"R{subtotal:.2f}")
y -= 20
if discount > 0:
c.drawString(350, y, "Discount:")
c.drawString(450, y, f"R{-discount:.2f}")
y -= 20
if tax > 0:
c.drawString(350, y, "Tax:")
c.drawString(450, y, f"R{tax:.2f}")
y -= 20
c.drawString(350, y, "Total:")
c.drawString(450, y, f"R{total:.2f}")
# Draw a box around the totals
c.rect(340, y - 10, 220, 60, stroke=1, fill=0)
# Footnote
c.setFont("Helvetica-Oblique", 10)
c.drawString(50, 30, "Thank you for your business.") # Added footnote
c.save()
self.parent.log_message(f"Invoice saved as {pdf_filename}")
self.parent.manage_invoices_tab.load_invoices()
self.parent.financial_statements_tab.load_financial_statements() # Refresh financial statements
self.send_email_invoice(client, pdf_filename)
def create_excel(self, client, vat, invoice_date, client_address, df, invoice_filename, subtotal, discount, tax, total):
"""Generate Excel invoice."""
try:
excel_filename = os.path.join(INVOICES_DIR, f"{invoice_filename}.xlsx")
template = self.get_selected_template()
writer = pd.ExcelWriter(excel_filename, engine='openpyxl')
df.to_excel(writer, index=False, sheet_name='Invoice')
# Access the workbook and worksheet
workbook = writer.book
worksheet = writer.sheets['Invoice']
# Apply Template Attributes (basic - headers)
current_row = 1
if 'header_text' in template:
worksheet.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=4)
cell = worksheet.cell(row=current_row, column=1)
cell.value = template['header_text']
cell.font = cell.font.copy(bold=True, size=16)
current_row += 1
# Company Details
company_name = self.parent.company_details.get("company_name", "Your Company Name")
company_address = self.parent.company_details.get("company_address", "Your Company Address")
company_email = self.parent.company_details.get("company_email", "your.email@example.com")
company_phone = self.parent.company_details.get("company_phone", "123-456-7890")
company_logo = self.parent.company_details.get("company_logo_path", "")
if company_logo and os.path.exists(company_logo):
# Insert company logo if available
img = QtGui.QPixmap(company_logo)
img.save('temp_logo.png') # Save temporarily
from openpyxl.drawing.image import Image as XLImage
img_excel = XLImage('temp_logo.png')
img_excel.anchor = 'A1'
worksheet.add_image(img_excel, 'A1')
current_row += 2 # Adjust row if logo is added
worksheet.cell(row=current_row, column=1, value=company_name)
worksheet.cell(row=current_row, column=2, value=f"Invoice Number: {self.parent.invoice_counter - 1}")
current_row += 1
worksheet.cell(row=current_row, column=1, value=company_address)
worksheet.cell(row=current_row, column=2, value=f"Date: {invoice_date}")
current_row += 1
worksheet.cell(row=current_row, column=1, value=f"Email: {company_email}")
worksheet.cell(row=current_row, column=2, value=f"Client: {client}")
current_row += 1
worksheet.cell(row=current_row, column=1, value=f"Phone: {company_phone}")
worksheet.cell(row=current_row, column=2, value=f"Address: {client_address}")
current_row += 1
if vat:
worksheet.cell(row=current_row, column=1, value=f"VAT Number: {vat}")
current_row += 1
# Add totals
start_row = len(df) + current_row + 2
worksheet.cell(row=start_row, column=3, value="Subtotal:")
worksheet.cell(row=start_row, column=4, value=subtotal)
if discount > 0:
worksheet.cell(row=start_row + 1, column=3, value="Discount:")
worksheet.cell(row=start_row + 1, column=4, value=-discount)
if tax > 0:
worksheet.cell(row=start_row + 2, column=3, value="Tax:")
worksheet.cell(row=start_row + 2, column=4, value=tax)
worksheet.cell(row=start_row + 3, column=3, value="Total:")
worksheet.cell(row=start_row + 3, column=4, value=total)
# Add footnote
worksheet.cell(row=start_row + 5, column=1, value="Thank you for your business.")
writer.save()
os.remove('temp_logo.png') # Clean up temporary logo
self.parent.log_message(f"Invoice saved as {excel_filename}")
self.parent.manage_invoices_tab.load_invoices()
self.parent.financial_statements_tab.load_financial_statements() # Refresh financial statements
self.send_email_invoice(client, excel_filename)
return True
except Exception as e:
QMessageBox.warning(self, "Excel Generation Error", f"Failed to generate Excel invoice: {e}")
self.parent.log_message(f"Failed to generate Excel invoice '{excel_filename}': {e}")
return False
def get_selected_template(self):
"""Retrieves the selected template's attributes."""
if self.selected_template and self.selected_template in self.parent.templates:
return self.parent.templates[self.selected_template]
return {}
# Manage Invoices Tab
class ManageInvoicesTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
self.invoices_list = QListWidget()
self.invoices_list.setStyleSheet("""
QListWidget {
background-color: #1E1E1E;
color: #ffffff;
border: 1px solid #555555;
}
QListWidget::item {
padding: 5px;
}
""")
self.load_invoices()
layout.addWidget(QLabel("Invoices:"))
layout.addWidget(self.invoices_list)
btn_layout = QHBoxLayout()
self.view_invoice_button = QPushButton("View Invoice")
self.view_invoice_button.clicked.connect(self.view_invoice)
self.resend_invoice_button = QPushButton("Resend Invoice")
self.resend_invoice_button.clicked.connect(self.resend_invoice)
self.delete_invoice_button = QPushButton("Delete Invoice")
self.delete_invoice_button.clicked.connect(self.delete_invoice)
btn_layout.addWidget(self.view_invoice_button)
btn_layout.addWidget(self.resend_invoice_button)
btn_layout.addWidget(self.delete_invoice_button)
layout.addLayout(btn_layout)
self.setLayout(layout)
def load_invoices(self):
self.invoices_list.clear()
if not os.path.exists(INVOICES_DIR):
return
invoices = sorted(os.listdir(INVOICES_DIR), reverse=True)
for invoice in invoices:
item = QListWidgetItem(invoice)
self.invoices_list.addItem(item)
def get_selected_invoice_path(self):
selected = self.invoices_list.currentItem()
if not selected:
QMessageBox.warning(self, "Selection Error", "Please select an invoice.")
return None
invoice_filename = selected.text()
invoice_path = os.path.join(INVOICES_DIR, invoice_filename)
return invoice_path
def view_invoice(self):
invoice_path = self.get_selected_invoice_path()
if not invoice_path:
return
if not os.path.exists(invoice_path):
QMessageBox.warning(self, "File Not Found", "The selected invoice file does not exist.")
return
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(os.path.abspath(invoice_path)))
def resend_invoice(self):
invoice_path = self.get_selected_invoice_path()
if not invoice_path:
return
try:
invoice_filename = os.path.basename(invoice_path)
parts = invoice_filename.split('_invoice_')
if len(parts) < 2:
raise ValueError("Invalid invoice filename format.")
invoice_number_part = parts[1].split('.')[0]
invoice_number = str(int(invoice_number_part))
invoice_metadata = self.parent.invoices_metadata.get(invoice_number, {})
if not invoice_metadata:
raise ValueError("Invoice metadata not found.")
client_name = invoice_metadata["client_name"]
if client_name not in self.parent.clients:
raise ValueError("Client not found.")
self.parent.invoice_tab.send_email_invoice(client_name, invoice_path)
except Exception as e:
QMessageBox.warning(self, "Resend Error", f"Failed to resend invoice: {e}")
self.parent.log_message(f"Failed to resend invoice '{invoice_path}': {e}")
def delete_invoice(self):
invoice_path = self.get_selected_invoice_path()
if not invoice_path:
return
reply = QMessageBox.question(
self, 'Delete Confirmation',
f"Are you sure you want to delete invoice '{os.path.basename(invoice_path)}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
invoice_filename = os.path.basename(invoice_path)
parts = invoice_filename.split('_invoice_')
if len(parts) < 2:
raise ValueError("Invalid invoice filename format.")
invoice_number_part = parts[1].split('.')[0]
invoice_number = str(int(invoice_number_part))
if invoice_number in self.parent.invoices_metadata:
del self.parent.invoices_metadata[invoice_number]
save_json(self.parent.invoices_metadata, INVOICES_METADATA_FILE)
os.remove(invoice_path)
self.parent.log_message(f"Invoice '{os.path.basename(invoice_path)}' deleted.")
self.load_invoices()
self.parent.financial_statements_tab.load_financial_statements() # Refresh financial statements
except Exception as e:
QMessageBox.warning(self, "Delete Error", f"Failed to delete invoice: {e}")
self.parent.log_message(f"Failed to delete invoice '{invoice_path}': {e}")
# Financial Statements Tab
class FinancialStatementsTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Client Selection
client_layout = QHBoxLayout()
self.client_combo = QComboBox(self)
self.load_clients()
client_layout.addWidget(QLabel("Select Client:"))
client_layout.addWidget(self.client_combo)
layout.addLayout(client_layout)
# Financial Statements Table
self.financial_table = QtWidgets.QTableWidget()
self.financial_table.setColumnCount(5)
self.financial_table.setHorizontalHeaderLabels(["Invoice Number", "Date", "Total (ZAR)", "Status", "Filename"])
self.financial_table.horizontalHeader().setStretchLastSection(True)
self.financial_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.financial_table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
layout.addWidget(self.financial_table)
# Button to mark as paid
btn_layout = QHBoxLayout()
self.mark_paid_button = QPushButton("Mark as Paid")
self.mark_paid_button.clicked.connect(self.mark_as_paid)
btn_layout.addWidget(self.mark_paid_button)
layout.addLayout(btn_layout)
# Totals Display
totals_layout = QHBoxLayout()
self.total_invoiced_label = QLabel("Total Invoiced: R0.00")
self.total_paid_label = QLabel("Total Paid: R0.00") # Placeholder for paid amounts
self.balance_label = QLabel("Balance: R0.00")
totals_layout.addWidget(self.total_invoiced_label)
totals_layout.addWidget(self.total_paid_label)
totals_layout.addWidget(self.balance_label)
layout.addLayout(totals_layout)
# Load financial statements when client is selected
self.client_combo.currentIndexChanged.connect(self.load_financial_statements)
self.load_financial_statements() # Initialize
self.setLayout(layout)
def load_clients(self):
self.client_combo.clear()
self.client_combo.addItems(list(self.parent.clients.keys()))
def load_financial_statements(self):
client_name = self.client_combo.currentText()
if not client_name:
return
# Clear existing table
self.financial_table.setRowCount(0)
total_invoiced = 0.0
total_paid = 0.0 # Placeholder for paid amounts
balance = 0.0
# Iterate through invoice metadata and populate table
for invoice_number, metadata in self.parent.invoices_metadata.items():
if metadata["client_name"] != client_name:
continue
invoice_number_display = metadata.get("invoice_number", "N/A")
invoice_date = metadata.get("date", "N/A")
total = metadata.get("total", 0.0)
status = metadata.get("status", "Unpaid")
filename = metadata.get("filename", "N/A")
total_invoiced += total
if status.lower() == "paid":
total_paid += total
balance += total if status.lower() != "paid" else 0.0
# Add row to table
row_position = self.financial_table.rowCount()
self.financial_table.insertRow(row_position)
self.financial_table.setItem(row_position, 0, QtWidgets.QTableWidgetItem(str(invoice_number_display)))
self.financial_table.setItem(row_position, 1, QtWidgets.QTableWidgetItem(invoice_date))
self.financial_table.setItem(row_position, 2, QtWidgets.QTableWidgetItem(f"R{total:.2f}"))
self.financial_table.setItem(row_position, 3, QtWidgets.QTableWidgetItem(status))
self.financial_table.setItem(row_position, 4, QtWidgets.QTableWidgetItem(filename))
self.total_invoiced_label.setText(f"Total Invoiced: R{total_invoiced:.2f}")
self.total_paid_label.setText(f"Total Paid: R{total_paid:.2f}")
self.balance_label.setText(f"Balance: R{balance:.2f}")
def mark_as_paid(self):
selected_rows = self.financial_table.selectionModel().selectedRows()
if not selected_rows:
QMessageBox.warning(self, "Selection Error", "Please select an invoice to mark as paid.")
return
for selected in selected_rows:
row = selected.row()
invoice_number = self.financial_table.item(row, 0).text()
current_status = self.financial_table.item(row, 3).text()
if current_status.lower() == "paid":
QMessageBox.information(self, "Already Paid", f"Invoice {invoice_number} is already marked as paid.")
continue
# Update status in metadata
invoice_number_key = str(invoice_number)
if invoice_number_key in self.parent.invoices_metadata:
self.parent.invoices_metadata[invoice_number_key]["status"] = "Paid"
save_json(self.parent.invoices_metadata, INVOICES_METADATA_FILE)
self.financial_table.setItem(row, 3, QtWidgets.QTableWidgetItem("Paid"))
self.parent.log_message(f"Invoice {invoice_number} marked as paid.")
# Refresh financial statements
self.load_financial_statements()
# Email Configuration Tab
class EmailConfigTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Email configuration form
form_group = QtWidgets.QGroupBox("Email Configuration")
form_layout = QVBoxLayout()
self.sender_email_input = QLineEdit(self)
self.sender_email_input.setPlaceholderText("Sender Email")
self.sender_password_input = QLineEdit(self)
self.sender_password_input.setPlaceholderText("Sender Password")
self.sender_password_input.setEchoMode(QLineEdit.EchoMode.Password)
self.smtp_server_input = QLineEdit(self)
self.smtp_server_input.setPlaceholderText("SMTP Server (e.g., smtp.office365.com)")
self.smtp_port_input = QLineEdit(self)
self.smtp_port_input.setPlaceholderText("SMTP Port (e.g., 587)")
form_layout.addWidget(QLabel("Sender Email:"))
form_layout.addWidget(self.sender_email_input)
form_layout.addWidget(QLabel("Sender Password:"))
form_layout.addWidget(self.sender_password_input)
form_layout.addWidget(QLabel("SMTP Server:"))
form_layout.addWidget(self.smtp_server_input)
form_layout.addWidget(QLabel("SMTP Port:"))
form_layout.addWidget(self.smtp_port_input)
form_group.setLayout(form_layout)
layout.addWidget(form_group)
# Save button
self.save_email_config_button = QPushButton("Save Email Settings")
self.save_email_config_button.clicked.connect(self.save_email_config)
layout.addWidget(self.save_email_config_button)
self.setLayout(layout)
self.load_existing_config()
def load_existing_config(self):
config = self.parent.email_config
if config:
self.sender_email_input.setText(config.get("sender_email", ""))
self.sender_password_input.setText(config.get("sender_password", ""))
self.smtp_server_input.setText(config.get("smtp_server", ""))
self.smtp_port_input.setText(str(config.get("smtp_port", "")))
def save_email_config(self):
sender_email = self.sender_email_input.text().strip()
sender_password = self.sender_password_input.text().strip()
smtp_server = self.smtp_server_input.text().strip()
smtp_port = self.smtp_port_input.text().strip()
if not sender_email or not sender_password or not smtp_server or not smtp_port:
QMessageBox.warning(self, "Input Error", "Please fill in all fields.")
return
try:
smtp_port = int(smtp_port)
except ValueError:
QMessageBox.warning(self, "Input Error", "SMTP Port must be a number.")
return
self.parent.email_config = {
"sender_email": sender_email,
"sender_password": sender_password,
"smtp_server": smtp_server,
"smtp_port": smtp_port
}
save_json(self.parent.email_config, EMAIL_CONFIG_FILE)
self.parent.log_message("Email settings updated.")
QMessageBox.information(self, "Success", "Email settings have been saved.")
# Company Details Tab
class CompanyDetailsTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Company details form
form_group = QtWidgets.QGroupBox("Company Details")
form_layout = QVBoxLayout()
self.company_name_input = QLineEdit(self)
self.company_name_input.setPlaceholderText("Company Name")
self.company_address_input = QLineEdit(self)
self.company_address_input.setPlaceholderText("Company Address")
self.company_email_input = QLineEdit(self)
self.company_email_input.setPlaceholderText("Company Email")
self.company_phone_input = QLineEdit(self)
self.company_phone_input.setPlaceholderText("Company Phone")
self.company_logo_path_input = QLineEdit(self)
self.company_logo_path_input.setPlaceholderText("Logo Path")
self.browse_logo_button = QPushButton("Browse")
self.browse_logo_button.clicked.connect(self.browse_logo)
logo_layout = QHBoxLayout()
logo_layout.addWidget(self.company_logo_path_input)
logo_layout.addWidget(self.browse_logo_button)
form_layout.addWidget(QLabel("Company Name:"))
form_layout.addWidget(self.company_name_input)
form_layout.addWidget(QLabel("Company Address:"))
form_layout.addWidget(self.company_address_input)
form_layout.addWidget(QLabel("Company Email:"))
form_layout.addWidget(self.company_email_input)
form_layout.addWidget(QLabel("Company Phone:"))
form_layout.addWidget(self.company_phone_input)
form_layout.addWidget(QLabel("Company Logo:"))
form_layout.addLayout(logo_layout)
form_group.setLayout(form_layout)
layout.addWidget(form_group)
# Save button
self.save_company_details_button = QPushButton("Save Company Details")
self.save_company_details_button.clicked.connect(self.save_company_details)
layout.addWidget(self.save_company_details_button)
self.setLayout(layout)
self.load_existing_details()
def browse_logo(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select Logo", "", "Image Files (*.png *.jpg *.bmp)")
if file_path:
self.company_logo_path_input.setText(file_path)
def load_existing_details(self):
details = self.parent.company_details
if details:
self.company_name_input.setText(details.get("company_name", ""))
self.company_address_input.setText(details.get("company_address", ""))
self.company_email_input.setText(details.get("company_email", ""))
self.company_phone_input.setText(details.get("company_phone", ""))
self.company_logo_path_input.setText(details.get("company_logo_path", ""))
def save_company_details(self):
company_name = self.company_name_input.text().strip()
company_address = self.company_address_input.text().strip()
company_email = self.company_email_input.text().strip()
company_phone = self.company_phone_input.text().strip()
company_logo_path = self.company_logo_path_input.text().strip()
if not company_name or not company_address or not company_email or not company_phone:
QMessageBox.warning(self, "Input Error", "Please fill in all required fields.")
return
self.parent.company_details = {
"company_name": company_name,
"company_address": company_address,
"company_email": company_email,
"company_phone": company_phone,
"company_logo_path": company_logo_path
}
save_json(self.parent.company_details, COMPANY_DETAILS_FILE)
self.parent.log_message("Company details updated.")
QMessageBox.information(self, "Success", "Company details have been saved.")
# Template Management Tab
class TemplateManagementTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# List of templates
self.templates_list = QListWidget()
self.load_templates()
layout.addWidget(QLabel("Existing Templates:"))
layout.addWidget(self.templates_list)
# Buttons to create, edit, and delete templates
btn_layout = QHBoxLayout()
self.create_template_button = QPushButton("Create New Template")
self.create_template_button.clicked.connect(self.create_template)
self.edit_template_button = QPushButton("Edit Selected")
self.edit_template_button.clicked.connect(self.edit_template)
self.delete_template_button = QPushButton("Delete Selected")
self.delete_template_button.clicked.connect(self.delete_template)
btn_layout.addWidget(self.create_template_button)
btn_layout.addWidget(self.edit_template_button)
btn_layout.addWidget(self.delete_template_button)
layout.addLayout(btn_layout)
self.setLayout(layout)
def load_templates(self):
self.templates_list.clear()
for template in self.parent.templates.keys():
item = QListWidgetItem(template)
self.templates_list.addItem(item)
def create_template(self):
"""Open CreateTemplate.py to create a new template."""
try:
# Assuming CreateTemplate.py is in the same directory and is executable
os.system("python CreateTemplate.py")
self.parent.log_message("Opened CreateTemplate.py")
# Reload templates after creation
self.load_templates()
except Exception as e:
QMessageBox.warning(self, "Error", f"Failed to open CreateTemplate.py: {e}")
def edit_template(self):
"""Open EditTemplate.py to edit the selected template."""
selected_item = self.templates_list.currentItem()
if not selected_item:
QMessageBox.warning(self, "Selection Error", "Please select a template to edit.")
return
template_name = selected_item.text()
try:
# Assuming EditTemplate.py can accept template name as argument
os.system(f"python EditTemplate.py {template_name}")
self.parent.log_message(f"Opened EditTemplate.py for template '{template_name}'")
# Reload templates after editing
self.load_templates()
except Exception as e:
QMessageBox.warning(self, "Error", f"Failed to open EditTemplate.py: {e}")
def delete_template(self):
"""Logic to delete the selected template."""
selected_item = self.templates_list.currentItem()
if not selected_item:
QMessageBox.warning(self, "Selection Error", "Please select a template to delete.")
return
template_name = selected_item.text()
reply = QMessageBox.question(
self, 'Delete Confirmation',
f"Are you sure you want to delete template '{template_name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
del self.parent.templates[template_name]
save_json(self.parent.templates, TEMPLATES_FILE)
self.parent.log_message(f"Template '{template_name}' deleted.")
self.load_templates()
except Exception as e:
QMessageBox.warning(self, "Delete Error", f"Failed to delete template: {e}")
self.parent.log_message(f"Failed to delete template '{template_name}': {e}")
# Dialog for editing a line item
class EditLineItemDialog(QDialog):
def __init__(self, name, data, parent):
super().__init__()
self.setWindowTitle("Edit Line Item")
self.parent = parent
self.name = name
self.data = data
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout()
self.name_input = QLineEdit(self)
self.name_input.setText(self.name)
self.price_input = QLineEdit(self)
self.price_input.setText(str(self.data["price"]))
self.layout.addWidget(QLabel("Name:"))
self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Price:"))
self.layout.addWidget(self.price_input)
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.setLayout(self.layout)
def get_data(self):
return (
self.name_input.text().strip(),
self.price_input.text().strip()
)
# Invoice Creation Tab
class InvoiceCreationTab(QWidget):
def __init__(self, parent):
super().__init__()
self.parent = parent
self.line_items = []
self.selected_template = None # For template usage
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# Template Selection
template_layout = QHBoxLayout()
self.template_combo = QComboBox(self)
self.load_templates()
self.template_combo.currentIndexChanged.connect(self.change_template)
template_layout.addWidget(QLabel("Select Template:"))
template_layout.addWidget(self.template_combo)
layout.addLayout(template_layout)
# Client selection
client_layout = QHBoxLayout()
self.client_combo = QComboBox(self)
self.load_clients()
client_layout.addWidget(QLabel("Select Client:"))
client_layout.addWidget(self.client_combo)
layout.addLayout(client_layout)
# VAT Number Display (Read-only)
vat_display_layout = QHBoxLayout()
self.vat_display = QLineEdit(self)
self.vat_display.setReadOnly(True)
vat_display_layout.addWidget(QLabel("Client VAT Number:"))
vat_display_layout.addWidget(self.vat_display)
layout.addLayout(vat_display_layout)
# Update VAT display when client changes
self.client_combo.currentIndexChanged.connect(self.update_vat_display)
self.update_vat_display() # Initialize VAT display
# Line Items selection
line_item_layout = QHBoxLayout()
self.item_combo = QComboBox(self)
self.load_line_items()
self.quantity_input = QSpinBox(self)
self.quantity_input.setMinimum(1)
self.quantity_input.setValue(1)
self.add_item_button = QPushButton("Add Item")
self.add_item_button.clicked.connect(self.add_line_item)
line_item_layout.addWidget(QLabel("Item:"))
line_item_layout.addWidget(self.item_combo)
line_item_layout.addWidget(QLabel("Quantity:"))
line_item_layout.addWidget(self.quantity_input)
line_item_layout.addWidget(self.add_item_button)
layout.addLayout(line_item_layout)
# List to display added line items
self.added_items_list = QListWidget()
self.added_items_list.setStyleSheet("""
QListWidget {
background-color: #1E1E1E;
color: #ffffff;
border: 1px solid #555555;
}
QListWidget::item {
padding: 5px;
}
""")
layout.addWidget(QLabel("Added Line Items:"))
layout.addWidget(self.added_items_list)
# Discounts and Taxes
discounts_taxes_layout = QHBoxLayout()
self.discount_input = QLineEdit(self)
self.discount_input.setPlaceholderText("Discount (%)")
self.tax_rate_input = QLineEdit(self)
self.tax_rate_input.setPlaceholderText("Tax Rate (%)")
discounts_taxes_layout.addWidget(QLabel("Discount (%):"))
discounts_taxes_layout.addWidget(self.discount_input)
discounts_taxes_layout.addWidget(QLabel("Tax Rate (%):"))
discounts_taxes_layout.addWidget(self.tax_rate_input)
layout.addLayout(discounts_taxes_layout)
# Output Format Selection
output_layout = QHBoxLayout()
self.output_format_combo = QComboBox(self)
self.output_format_combo.addItems(["PDF", "Excel"])
output_layout.addWidget(QLabel("Output Format:"))
output_layout.addWidget(self.output_format_combo)
layout.addLayout(output_layout)
# Button to create invoice
self.create_invoice_button = QPushButton("Create Invoice")
self.create_invoice_button.clicked.connect(self.create_invoice)
layout.addWidget(self.create_invoice_button)
self.setLayout(layout)
def load_templates(self):
self.template_combo.clear()
templates_list = list(self.parent.templates.keys())
self.template_combo.addItems(["Default"] + templates_list)
self.selected_template = "Default"
def change_template(self, index):
selected = self.template_combo.currentText()
if selected == "Default":
self.selected_template = None
else:
self.selected_template = selected
def load_clients(self):
self.client_combo.clear()
self.client_combo.addItems(list(self.parent.clients.keys()))
def load_line_items(self):
self.item_combo.clear()
self.item_combo.addItems(list(self.parent.line_items_data.keys()))
def add_line_item(self):
item_name = self.item_combo.currentText()
quantity = self.quantity_input.value()
if not item_name:
QMessageBox.warning(self, "Selection Error", "Please select a line item.")
return
price = self.parent.line_items_data[item_name]['price']
total = price * quantity
self.line_items.append({
"item": item_name,
"quantity": quantity,
"price": price,
"total": total
})
self.update_added_items_list()
def update_added_items_list(self):
self.added_items_list.clear()
for idx, item in enumerate(self.line_items, 1):
self.added_items_list.addItem(
f"{idx}. {item['item']} - Qty: {item['quantity']} - Price: R{item['price']:.2f} - Total: R{item['total']:.2f}"
)
def update_vat_display(self):
client_name = self.client_combo.currentText()
vat_number = self.parent.clients.get(client_name, {}).get("vat_number", "")
self.vat_display.setText(vat_number)
def create_invoice(self):
client_name = self.client_combo.currentText()
vat_number = self.parent.clients.get(client_name, {}).get("vat_number", "")
discount = self.discount_input.text().strip()
tax_rate = self.tax_rate_input.text().strip()
if not client_name:
QMessageBox.warning(self, "Input Error", "Please select a client.")
return
if not self.line_items:
QMessageBox.warning(self, "Input Error", "Please add at least one line item.")
return
# Calculate totals
subtotal = sum(item['total'] for item in self.line_items)
discount_amount = 0
if discount:
try:
discount_percentage = float(discount)
discount_amount = subtotal * (discount_percentage / 100)
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid discount percentage.")
return
subtotal_after_discount = subtotal - discount_amount
tax_amount = 0
if tax_rate:
try:
tax_percentage = float(tax_rate)
tax_amount = subtotal_after_discount * (tax_percentage / 100)
except ValueError:
QMessageBox.warning(self, "Input Error", "Please enter a valid tax rate percentage.")
return
total = subtotal_after_discount + tax_amount
invoice_date = datetime.now().strftime("%Y-%m-%d") # Automatic date filling
client_address = self.parent.clients[client_name]["address"]
invoice_filename = f"{client_name.replace(' ', '_')}_invoice_{self.parent.invoice_counter}"
# Prepare DataFrame for Excel/PDF
df = pd.DataFrame([
{"Item": item['item'], "Quantity": item['quantity'], "Price (ZAR)": item['price'], "Total (ZAR)": item['total']}
for item in self.line_items
])
output_format = self.output_format_combo.currentText()
success = False
if output_format == "PDF":
success = self.create_pdf(client_name, vat_number, invoice_date, client_address, df, invoice_filename, subtotal, discount_amount, tax_amount, total)
elif output_format == "Excel":
success = self.create_excel(client_name, vat_number, invoice_date, client_address, df, invoice_filename, subtotal, discount_amount, tax_amount, total)
else:
QMessageBox.warning(self, "Format Error", "Please select a valid output format.")
return
if success:
# Log invoice metadata
invoice_metadata = {
"invoice_number": self.parent.invoice_counter - 1,
"client_name": client_name,
"date": invoice_date,
"total": total,
"status": "Unpaid",
"filename": f"{invoice_filename}.{output_format.lower()}"
}
self.parent.invoices_metadata[str(self.parent.invoice_counter - 1)] = invoice_metadata
save_json(self.parent.invoices_metadata, INVOICES_METADATA_FILE)
# Reset form
self.line_items = []
self.update_added_items_list()
self.parent.invoice_counter += 1 # Increment invoice counter after successful creation
self.parent.update_invoice_counter()
def apply_template_attributes(self, c, template, invoice_details):
"""Applies template attributes like header, footer, and logo to the PDF."""
if 'header_text' in template:
c.setFont("Helvetica-Bold", 16)
c.drawString(50, invoice_details['height'] - 50, template['header_text'])
if 'footer_text' in template:
c.setFont("Helvetica", 10)
c.drawString(50, 30, template['footer_text'])
if 'logo_path' in template and template['logo_path']:
if os.path.exists(template['logo_path']):
c.drawImage(template['logo_path'], invoice_details['width'] - 150, invoice_details['height'] - 100, width=100, height=50)
def create_pdf(self, client, vat, invoice_date, client_address, df, invoice_filename, subtotal, discount, tax, total):
"""Generate PDF invoice."""
pdf_filename = os.path.join(INVOICES_DIR, f"{invoice_filename}.pdf")
template = self.get_selected_template()
c = canvas.Canvas(pdf_filename, pagesize=letter)
width, height = letter
# Apply Template Attributes
invoice_details = {'width': width, 'height': height}
self.apply_template_attributes(c, template, invoice_details)
# Company Details
company_name = self.parent.company_details.get("company_name", "Your Company Name")
company_address = self.parent.company_details.get("company_address", "Your