Desktop app update

This commit is contained in:
2026-02-18 12:42:36 -05:00
parent 0f913ae3c9
commit a29135304d
2 changed files with 480 additions and 187 deletions
+476 -187
View File
@@ -206,9 +206,32 @@ class QBOExcelSyncApp:
def __init__(self):
self.root = tk.Tk()
self.root.title(APP_NAME)
self.root.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
# Detect screen size and set appropriate window size
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# Calculate window size based on screen (80% of screen, but within limits)
window_width = min(max(int(screen_width * 0.8), MIN_WIDTH), WINDOW_WIDTH)
window_height = min(max(int(screen_height * 0.8), MIN_HEIGHT), WINDOW_HEIGHT)
# For smaller screens (like 1920x1080), use more of the screen
if screen_width <= 1920:
window_width = min(int(screen_width * 0.9), screen_width - 50)
window_height = min(int(screen_height * 0.85), screen_height - 100)
# Center the window
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
self.root.minsize(MIN_WIDTH, MIN_HEIGHT)
# Store screen info for responsive layouts
self.screen_width = screen_width
self.screen_height = screen_height
self.is_small_screen = screen_width <= 1920 or screen_height <= 1080
# Maximize window on startup (works on Windows)
try:
self.root.state('zoomed') # Windows
@@ -247,7 +270,7 @@ class QBOExcelSyncApp:
# Bind window close event
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
logger.info("Application started")
logger.info(f"Application started - Screen: {screen_width}x{screen_height}, Window: {window_width}x{window_height}")
def _setup_styles(self):
"""Configure ttk styles."""
@@ -344,89 +367,68 @@ class QBOExcelSyncApp:
def _create_dashboard_tab(self):
"""Create the dashboard tab."""
tab = ttk.Frame(self.notebook, padding=20)
tab = ttk.Frame(self.notebook, padding=15)
self.notebook.add(tab, text=" Dashboard ")
# Welcome header
# Header with title and connection status
header_frame = ttk.Frame(tab)
header_frame.pack(fill=tk.X, pady=(0, 20))
header_frame.pack(fill=tk.X, pady=(0, 10))
# Left side - title
title_frame = ttk.Frame(header_frame)
title_frame.pack(side=tk.LEFT)
ttk.Label(
header_frame,
title_frame,
text="Welcome to QBO Excel Sync",
style='Title.TLabel'
).pack(anchor=tk.W)
ttk.Label(
header_frame,
title_frame,
text="Import your Excel data to QuickBooks with ease.",
style='Subtitle.TLabel',
foreground='gray'
).pack(anchor=tk.W, pady=(5, 0))
).pack(anchor=tk.W)
# Stats cards frame
stats_frame = ttk.Frame(tab)
stats_frame.pack(fill=tk.X, pady=10)
# Right side - connection status (like Lookup tab)
self.dashboard_conn_label = ttk.Label(header_frame, text="Not connected", foreground='gray')
self.dashboard_conn_label.pack(side=tk.RIGHT, padx=10)
# Connection status card
self.conn_card = self._create_stat_card(
stats_frame,
"Connection Status",
"Not Connected",
"Connect to QuickBooks to start"
)
self.conn_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
# Supported types card
types_card = self._create_stat_card(
stats_frame,
"Supported Types",
"6 Types",
"Checks, Invoices, Bills, & more"
)
types_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(10, 0))
# Quick actions
actions_frame = ttk.LabelFrame(tab, text="Quick Actions", padding=15)
actions_frame.pack(fill=tk.X, pady=20)
# Quick actions - horizontal buttons
actions_frame = ttk.LabelFrame(tab, text="Quick Actions", padding=10)
actions_frame.pack(fill=tk.X, pady=(0, 10))
actions_inner = ttk.Frame(actions_frame)
actions_inner.pack(fill=tk.X)
ttk.Button(
actions_inner,
text="🔗 Manage Connection",
command=lambda: self.notebook.select(1), # Connection tab (index 1)
width=25
).pack(side=tk.LEFT, padx=5)
btn_width = 18
ttk.Button(
actions_inner,
text="📁 Import Excel File",
command=lambda: self.notebook.select(2), # Import tab (index 2)
width=25
).pack(side=tk.LEFT, padx=5)
actions_inner, text="🔗 Connection",
command=lambda: self.notebook.select(1), width=btn_width
).grid(row=0, column=0, padx=3, pady=3, sticky='ew')
ttk.Button(
actions_inner,
text="📋 Manage Templates",
command=lambda: self.notebook.select(3), # Templates tab (index 3)
width=25
).pack(side=tk.LEFT, padx=5)
actions_inner, text="📁 Import File",
command=lambda: self.notebook.select(2), width=btn_width
).grid(row=0, column=1, padx=3, pady=3, sticky='ew')
ttk.Button(
actions_inner,
text="🔍 QuickBooks Lookup",
command=lambda: self.notebook.select(4), # Lookup tab (index 4)
width=25
).pack(side=tk.LEFT, padx=5)
actions_inner, text="📋 Templates",
command=lambda: self.notebook.select(3), width=btn_width
).grid(row=0, column=2, padx=3, pady=3, sticky='ew')
# Supported data types
types_frame = ttk.LabelFrame(tab, text="Supported Data Types", padding=15)
types_frame.pack(fill=tk.BOTH, expand=True, pady=10)
ttk.Button(
actions_inner, text="🔍 Lookup",
command=lambda: self.notebook.select(4), width=btn_width
).grid(row=0, column=3, padx=3, pady=3, sticky='ew')
types_grid = ttk.Frame(types_frame)
types_grid.pack(fill=tk.BOTH, expand=True)
for i in range(4):
actions_inner.columnconfigure(i, weight=1)
# Supported data types - compact grid layout
types_frame = ttk.LabelFrame(tab, text="Supported Data Types", padding=10)
types_frame.pack(fill=tk.BOTH, expand=True)
type_info = [
("✓ Checks", "Import check payments with payee, amount, and account details."),
@@ -437,16 +439,21 @@ class QBOExcelSyncApp:
("✓ Accounts", "Create accounts with types, numbers, and descriptions."),
]
# Always use 3 columns, compact layout
types_grid = ttk.Frame(types_frame)
types_grid.pack(fill=tk.BOTH, expand=True)
for i, (title, desc) in enumerate(type_info):
row = i // 3
col = i % 3
type_card = ttk.Frame(types_grid, padding=10)
type_card.grid(row=row, column=col, sticky='nsew', padx=5, pady=5)
type_card = ttk.Frame(types_grid, padding=5)
type_card.grid(row=row, column=col, sticky='nsew', padx=3, pady=3)
ttk.Label(type_card, text=title, font=('Segoe UI', 10, 'bold')).pack(anchor=tk.W)
ttk.Label(type_card, text=desc, wraplength=200, foreground='gray').pack(anchor=tk.W)
ttk.Label(type_card, text=desc, wraplength=220, foreground='gray', font=('Segoe UI', 9)).pack(anchor=tk.W)
for col in range(3):
types_grid.columnconfigure(col, weight=1)
def _create_stat_card(self, parent, title: str, value: str, detail: str) -> ttk.Frame:
@@ -467,34 +474,33 @@ class QBOExcelSyncApp:
def _create_import_tab(self):
"""Create the import tab."""
tab = ttk.Frame(self.notebook, padding=20)
tab = ttk.Frame(self.notebook, padding=15)
self.notebook.add(tab, text=" Import ")
# Left panel - File selection and options
left_frame = ttk.Frame(tab)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
# Top section - File selection and options
top_frame = ttk.Frame(tab)
top_frame.pack(fill=tk.X, pady=(0, 10))
# File selection
file_frame = ttk.LabelFrame(left_frame, text="1. Select Excel File", padding=15)
file_frame.pack(fill=tk.X, pady=(0, 10))
file_frame = ttk.LabelFrame(top_frame, text="1. Select Excel File", padding=10)
file_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
file_inner = ttk.Frame(file_frame)
file_inner.pack(fill=tk.X)
self.file_entry = ttk.Entry(file_inner, width=50)
self.file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
self.file_entry = ttk.Entry(file_inner, width=40)
self.file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(file_inner, text="Browse...", command=self._open_file).pack(side=tk.LEFT)
ttk.Button(file_inner, text="Refresh", command=self._refresh_file).pack(side=tk.LEFT, padx=(5, 0))
ttk.Button(file_inner, text="", command=self._refresh_file, width=3).pack(side=tk.LEFT, padx=(3, 0))
self.file_info_label = ttk.Label(file_frame, text="No file selected", foreground='gray')
self.file_info_label.pack(anchor=tk.W, pady=(10, 0))
self.file_info_label.pack(anchor=tk.W, pady=(5, 0))
# Data type and template selection
type_frame = ttk.LabelFrame(left_frame, text="2. Select Data Type & Template", padding=15)
type_frame.pack(fill=tk.X, pady=10)
# Data type selection
type_frame = ttk.LabelFrame(top_frame, text="2. Data Type", padding=10)
type_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=(5, 5))
# Data type radio buttons
self.data_type_var = tk.StringVar(value="check")
type_grid = ttk.Frame(type_frame)
@@ -508,11 +514,15 @@ class QBOExcelSyncApp:
value=value,
command=self._on_data_type_change
)
rb.grid(row=i // 3, column=i % 3, sticky=tk.W, padx=10, pady=5)
rb.grid(row=i // 2, column=i % 2, sticky=tk.W, padx=5, pady=2)
# Template selection
template_row = ttk.Frame(type_frame)
template_row.pack(fill=tk.X, pady=(10, 0))
# Template and mapping
mapping_frame = ttk.LabelFrame(top_frame, text="3. Template & Mapping", padding=10)
mapping_frame.pack(side=tk.LEFT, fill=tk.BOTH, padx=(5, 0))
# Template selection row
template_row = ttk.Frame(mapping_frame)
template_row.pack(fill=tk.X)
ttk.Label(template_row, text="Template:").pack(side=tk.LEFT)
@@ -521,66 +531,32 @@ class QBOExcelSyncApp:
template_row,
textvariable=self.template_var,
values=["(Default Template)"],
width=30,
width=20,
state='readonly'
)
self.template_combo.pack(side=tk.LEFT, padx=(10, 0))
self.template_combo.pack(side=tk.LEFT, padx=(5, 0))
self.template_combo.current(0)
self.template_combo.bind('<<ComboboxSelected>>', self._on_template_change)
ttk.Button(template_row, text="", command=self._refresh_template_list, width=3).pack(side=tk.LEFT, padx=(5, 0))
ttk.Button(template_row, text="", command=self._refresh_template_list, width=3).pack(side=tk.LEFT, padx=(3, 0))
# Field mapping
mapping_frame = ttk.LabelFrame(left_frame, text="3. Field Mapping", padding=15)
mapping_frame.pack(fill=tk.BOTH, expand=True, pady=10)
# Mapping buttons
mapping_btns = ttk.Frame(mapping_frame)
mapping_btns.pack(fill=tk.X, pady=(8, 0))
# Mapping treeview
mapping_inner = ttk.Frame(mapping_frame)
mapping_inner.pack(fill=tk.BOTH, expand=True)
ttk.Button(mapping_btns, text="📝 Edit Mapping...", command=self._open_mapping_dialog).pack(side=tk.LEFT, padx=(0, 3))
ttk.Button(mapping_btns, text="Auto-Map", command=self._auto_map_fields).pack(side=tk.LEFT, padx=3)
ttk.Button(mapping_btns, text="Save Template", command=self._save_template).pack(side=tk.LEFT, padx=3)
columns = ('excel_column', 'qbo_field')
self.mapping_tree = ttk.Treeview(mapping_inner, columns=columns, show='headings', height=10)
self.mapping_tree.heading('excel_column', text='Excel Column')
self.mapping_tree.heading('qbo_field', text='QuickBooks Field')
self.mapping_tree.column('excel_column', width=200)
self.mapping_tree.column('qbo_field', width=200)
mapping_scroll = ttk.Scrollbar(mapping_inner, orient=tk.VERTICAL, command=self.mapping_tree.yview)
self.mapping_tree.configure(yscrollcommand=mapping_scroll.set)
self.mapping_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
mapping_scroll.pack(side=tk.RIGHT, fill=tk.Y)
mapping_buttons = ttk.Frame(mapping_frame)
mapping_buttons.pack(fill=tk.X, pady=(10, 0))
ttk.Button(mapping_buttons, text="Auto-Map", command=self._auto_map_fields).pack(side=tk.LEFT)
ttk.Button(mapping_buttons, text="Edit Mapping...", command=self._edit_mapping).pack(side=tk.LEFT, padx=5)
ttk.Button(mapping_buttons, text="Save as Template", command=self._save_template).pack(side=tk.LEFT)
# Right panel - Preview and import
right_frame = ttk.Frame(tab)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
# Data preview
preview_frame = ttk.LabelFrame(right_frame, text="Data Preview", padding=15)
preview_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self.preview_tree = ttk.Treeview(preview_frame, show='headings', height=10)
preview_scroll_y = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=self.preview_tree.yview)
preview_scroll_x = ttk.Scrollbar(preview_frame, orient=tk.HORIZONTAL, command=self.preview_tree.xview)
self.preview_tree.configure(yscrollcommand=preview_scroll_y.set, xscrollcommand=preview_scroll_x.set)
self.preview_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
preview_scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
# Progress and import
import_frame = ttk.LabelFrame(right_frame, text="Import", padding=15)
# Middle section - Import controls and progress
import_frame = ttk.LabelFrame(tab, text="Import", padding=10)
import_frame.pack(fill=tk.X, pady=10)
# Progress bar
self.progress_frame = ProgressFrame(import_frame)
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_frame.pack(fill=tk.X, pady=(0, 8))
# Import buttons
import_buttons = ttk.Frame(import_frame)
import_buttons.pack(fill=tk.X)
@@ -588,7 +564,7 @@ class QBOExcelSyncApp:
import_buttons,
text="▶ Start Import",
command=self._start_import,
width=20
width=18
)
self.import_btn.pack(side=tk.LEFT)
@@ -601,12 +577,26 @@ class QBOExcelSyncApp:
)
self.stop_btn.pack(side=tk.LEFT, padx=10)
# Log viewer
log_frame = ttk.LabelFrame(right_frame, text="Import Log", padding=10)
ttk.Button(
import_buttons,
text="Preview Data",
command=self._preview_data,
width=12
).pack(side=tk.LEFT)
# Mapping status label
self.mapping_status_label = ttk.Label(import_buttons, text="No mappings configured", foreground='gray')
self.mapping_status_label.pack(side=tk.RIGHT, padx=10)
# Bottom section - Import log (takes most space)
log_frame = ttk.LabelFrame(tab, text="Import Log", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_viewer = LogViewer(log_frame)
self.log_viewer.pack(fill=tk.BOTH, expand=True)
# Hidden preview tree for data preview dialog
self.preview_tree = None
def _create_connection_tab(self):
"""Create the connection tab."""
@@ -777,15 +767,14 @@ class QBOExcelSyncApp:
def _create_templates_tab(self):
"""Create the templates management tab."""
tab = ttk.Frame(self.notebook, padding=20)
tab = ttk.Frame(self.notebook, padding=15)
self.notebook.add(tab, text=" Templates ")
# Header
header = ttk.Frame(tab)
header.pack(fill=tk.X, pady=(0, 20))
header.pack(fill=tk.X, pady=(0, 10))
ttk.Label(header, text="Field Mapping Templates", style='Title.TLabel').pack(side=tk.LEFT)
ttk.Button(header, text="+ New Template", command=self._create_template).pack(side=tk.RIGHT)
# Templates list
list_frame = ttk.Frame(tab)
@@ -804,15 +793,25 @@ class QBOExcelSyncApp:
self.templates_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
templates_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# Template actions
# Template actions - all buttons in one row
actions = ttk.Frame(tab)
actions.pack(fill=tk.X, pady=(15, 0))
actions.pack(fill=tk.X, pady=(10, 0))
ttk.Button(actions, text="Edit", command=self._edit_template).pack(side=tk.LEFT)
ttk.Button(actions, text="Duplicate", command=self._duplicate_template).pack(side=tk.LEFT, padx=5)
ttk.Button(actions, text="Delete", command=self._delete_template).pack(side=tk.LEFT)
ttk.Button(actions, text="+ New", command=self._create_template).pack(side=tk.LEFT)
ttk.Button(actions, text="Edit", command=self._edit_template).pack(side=tk.LEFT, padx=5)
ttk.Button(actions, text="Duplicate", command=self._duplicate_template).pack(side=tk.LEFT)
ttk.Button(actions, text="Delete", command=self._delete_template).pack(side=tk.LEFT, padx=5)
ttk.Button(actions, text="Refresh", command=self._refresh_templates).pack(side=tk.RIGHT)
# Import/Export actions
io_frame = ttk.Frame(tab)
io_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(io_frame, text="Import/Export:", foreground='gray').pack(side=tk.LEFT)
ttk.Button(io_frame, text="Import Template", command=self._import_template).pack(side=tk.LEFT, padx=(10, 5))
ttk.Button(io_frame, text="Export Selected", command=self._export_template).pack(side=tk.LEFT, padx=5)
ttk.Button(io_frame, text="Export All", command=self._export_all_templates).pack(side=tk.LEFT, padx=5)
# Load templates
self._refresh_templates()
@@ -1885,7 +1884,48 @@ except Exception as e:
def _qbd_connect_error(self, error: str):
"""Handle QBD connection error."""
self.qbd_connect_btn.config(state=tk.NORMAL, text="Connect to QuickBooks Desktop")
messagebox.showerror("Connection Failed", f"Failed to connect: {error}")
# Provide helpful error messages for common issues
error_lower = str(error).lower()
if 'no module named' in error_lower and 'pythoncom' in error_lower:
helpful_msg = (
"QuickBooks Desktop connection requires the 'pywin32' package.\n\n"
"Please open Command Prompt and run:\n"
" pip install pywin32\n\n"
"Then restart the application."
)
messagebox.showerror("Missing Dependency", helpful_msg)
elif 'no module named' in error_lower and 'win32com' in error_lower:
helpful_msg = (
"QuickBooks Desktop connection requires the 'pywin32' package.\n\n"
"Please open Command Prompt and run:\n"
" pip install pywin32\n\n"
"Then restart the application."
)
messagebox.showerror("Missing Dependency", helpful_msg)
elif 'no module named' in error_lower and 'encodings' in error_lower:
helpful_msg = (
"Python installation not found or corrupted.\n\n"
"Please install Python from https://www.python.org/downloads/\n"
"Make sure to check 'Add Python to PATH' during installation.\n\n"
"Then install pywin32:\n"
" pip install pywin32"
)
messagebox.showerror("Python Not Found", helpful_msg)
elif 'could not find' in error_lower or 'not found' in error_lower:
helpful_msg = (
"Could not connect to QuickBooks Desktop.\n\n"
"Please ensure:\n"
"1. QuickBooks Desktop is open\n"
"2. A company file is open\n"
"3. QuickBooks is not showing any dialogs\n\n"
f"Error: {error}"
)
messagebox.showerror("Connection Failed", helpful_msg)
else:
messagebox.showerror("Connection Failed", f"Failed to connect: {error}")
logger.error(f"QBD connection failed: {error}")
def _disconnect(self):
@@ -1928,21 +1968,35 @@ except Exception as e:
# Status bar
self.status_bar.set_connected(self.company_name, self.connection_type)
# Dashboard card
self.conn_card.value_label.config(text="Connected")
self.conn_card.detail_label.config(text=self.company_name)
# Dashboard connection label (top right)
type_text = "Desktop" if self.connection_type == 'qbd' else "Online"
if hasattr(self, 'dashboard_conn_label'):
self.dashboard_conn_label.config(
text=f"● Connected: {self.company_name} ({type_text})",
foreground='green'
)
# Lookup tab connection label
if hasattr(self, 'lookup_conn_label'):
self.lookup_conn_label.config(
text=f"● Connected: {self.company_name} ({type_text})",
foreground='green'
)
# Connection tab
type_text = "Desktop" if self.connection_type == 'qbd' else "Online"
self.conn_status_label.config(text=f"● Connected ({type_text})", foreground='green')
self.conn_company_label.config(text=self.company_name)
else:
# Status bar
self.status_bar.set_disconnected()
# Dashboard card
self.conn_card.value_label.config(text="Not Connected")
self.conn_card.detail_label.config(text="Connect to QuickBooks to start")
# Dashboard connection label (top right)
if hasattr(self, 'dashboard_conn_label'):
self.dashboard_conn_label.config(text="Not connected", foreground='gray')
# Lookup tab connection label
if hasattr(self, 'lookup_conn_label'):
self.lookup_conn_label.config(text="Not connected", foreground='gray')
# Connection tab
self.conn_status_label.config(text="● Not Connected", foreground='red')
@@ -2410,45 +2464,20 @@ except Exception as e:
messagebox.showerror("Parse Error", f"Failed to parse file: {str(e)}")
def _update_preview(self):
"""Update the data preview tree."""
# Clear existing
for item in self.preview_tree.get_children():
self.preview_tree.delete(item)
"""Update the data preview (now just updates internal state, preview shown via dialog)."""
# Preview tree was removed from main UI - data is shown via _preview_data dialog
# Just ensure data is loaded
if not hasattr(self, 'excel_data') or not self.excel_data:
return
# Get columns
columns = self.excel_columns if hasattr(self, 'excel_columns') else []
if not columns and self.excel_data:
# Fallback: get columns from first row
columns = list(self.excel_data[0].keys())
# Configure columns
self.preview_tree['columns'] = columns
for col in columns:
self.preview_tree.heading(col, text=col)
self.preview_tree.column(col, width=100)
# Add rows (limit to 100 for performance)
for row in self.excel_data[:100]:
values = [row.get(col, '') for col in columns]
self.preview_tree.insert('', tk.END, values=values)
# Update mapping status when data changes
self._update_mapping_status()
def _update_mapping(self):
"""Update the field mapping tree."""
# Clear existing
for item in self.mapping_tree.get_children():
self.mapping_tree.delete(item)
if not hasattr(self, 'excel_columns') or not self.excel_columns:
return
# Add to tree
for col in self.excel_columns:
qbo_field = self.field_mappings.get(col, '(unmapped)')
self.mapping_tree.insert('', tk.END, values=(col, qbo_field))
"""Update the field mapping status (tree was removed, now uses status label)."""
# Mapping tree was removed from main UI - mapping is done via dialog
# Just update the status label
self._update_mapping_status()
def _on_data_type_change(self):
"""Handle data type selection change."""
@@ -2658,12 +2687,77 @@ except Exception as e:
self.field_mappings[col] = qbo_field
break
self._update_mapping()
self._update_mapping_status()
mapped_count = len(self.field_mappings)
total_count = len(excel_columns)
messagebox.showinfo("Auto-Map Complete", f"Mapped {mapped_count} of {total_count} fields.")
def _open_mapping_dialog(self):
"""Open the field mapping dialog - wrapper for _edit_mapping."""
self._edit_mapping()
def _update_mapping_status(self):
"""Update the mapping status label in Import tab."""
if hasattr(self, 'mapping_status_label'):
count = len(self.field_mappings) if self.field_mappings else 0
if count > 0:
self.mapping_status_label.config(
text=f"{count} field(s) mapped",
foreground='green'
)
else:
self.mapping_status_label.config(
text="No mappings configured",
foreground='gray'
)
def _preview_data(self):
"""Show data preview dialog."""
if not self.excel_data:
messagebox.showwarning("No Data", "Please load a file first.")
return
dialog = tk.Toplevel(self.root)
dialog.title("Data Preview")
dialog.geometry("900x500")
dialog.transient(self.root)
# Info label
info_text = f"Showing {min(50, len(self.excel_data))} of {len(self.excel_data)} rows"
ttk.Label(dialog, text=info_text, font=('Segoe UI', 10)).pack(pady=10)
# Treeview for preview
tree_frame = ttk.Frame(dialog)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
columns = self.excel_columns if self.excel_columns else []
preview_tree = ttk.Treeview(tree_frame, columns=columns, show='headings', height=20)
for col in columns:
preview_tree.heading(col, text=col)
preview_tree.column(col, width=100, minwidth=50)
# Scrollbars
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=preview_tree.yview)
hsb = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=preview_tree.xview)
preview_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
preview_tree.grid(row=0, column=0, sticky='nsew')
vsb.grid(row=0, column=1, sticky='ns')
hsb.grid(row=1, column=0, sticky='ew')
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# Load data (first 50 rows)
for row_data in self.excel_data[:50]:
values = [row_data.get(col, '') for col in columns]
preview_tree.insert('', tk.END, values=values)
# Close button
ttk.Button(dialog, text="Close", command=dialog.destroy).pack(pady=10)
def _edit_mapping(self):
"""Open mapping editor dialog."""
if not hasattr(self, 'excel_columns') or not self.excel_columns:
@@ -2718,7 +2812,7 @@ except Exception as e:
def save_mappings():
self.field_mappings = {col: var.get() for col, var in mapping_vars.items() if var.get()}
self._update_mapping()
self._update_mapping_status()
dialog.destroy()
logger.info(f"[EDIT] Field mappings updated: {len(self.field_mappings)} fields mapped")
@@ -3395,7 +3489,7 @@ except Exception as e:
def _create_template(self):
"""Create a new template."""
messagebox.showinfo("Create Template", "Load a file and map fields first, then use 'Save as Template'.")
self.notebook.select(1) # Switch to Import tab
self.notebook.select(2) # Switch to Import tab (index 2)
def _edit_template(self):
"""Edit selected template."""
@@ -3427,8 +3521,8 @@ except Exception as e:
self.field_mappings[mapping.get('excel_column', '')] = mapping.get('qbo_field', '')
self.data_type_var.set(template.data_type if hasattr(template, 'data_type') else 'check')
self._update_mapping()
self.notebook.select(1) # Switch to Import tab
self._update_mapping_status()
self.notebook.select(2) # Switch to Import tab (index 2)
logger.info(f"[EDIT] Loaded template: {template_name}")
def _duplicate_template(self):
@@ -3480,6 +3574,201 @@ except Exception as e:
if messagebox.askyesno("Confirm Delete", f"Delete template '{template_name}'?"):
self.settings.delete_template(template_name)
self._refresh_templates()
logger.info(f"[TEMPLATE] Deleted template: {template_name}")
def _import_template(self):
"""Import template(s) from a JSON file."""
file_path = filedialog.askopenfilename(
title="Import Template",
filetypes=[
("JSON Files", "*.json"),
("All Files", "*.*")
]
)
if not file_path:
return
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Handle both single template and multiple templates
if isinstance(data, list):
templates_data = data
elif isinstance(data, dict):
templates_data = [data]
else:
raise ValueError("Invalid template format")
imported_count = 0
skipped_count = 0
for template_data in templates_data:
# Validate required fields
if 'name' not in template_data or 'data_type' not in template_data:
skipped_count += 1
continue
template_name = template_data['name']
# Check if template already exists
existing_templates = self.settings.get_templates()
exists = any(t.name == template_name for t in existing_templates)
if exists:
overwrite = messagebox.askyesno(
"Template Exists",
f"Template '{template_name}' already exists. Overwrite?"
)
if not overwrite:
skipped_count += 1
continue
# Import the FieldMapping class
from src.config.settings import MappingTemplate, FieldMapping
# Create MappingTemplate from data
mappings = []
for m in template_data.get('mappings', []):
mapping = FieldMapping(
excel_column=m.get('excel_column', ''),
qbo_field=m.get('qbo_field', ''),
transform=m.get('transform'),
default_value=m.get('default_value'),
required=m.get('required', False)
)
mappings.append(mapping)
template = MappingTemplate(
name=template_data['name'],
data_type=template_data['data_type'],
mappings=mappings,
created_at=template_data.get('created_at', datetime.now().isoformat()),
updated_at=datetime.now().isoformat()
)
self.settings.save_template(template)
imported_count += 1
logger.info(f"[TEMPLATE] Imported template: {template_name}")
# Refresh the list
self._refresh_templates()
# Show result
msg = f"Imported {imported_count} template(s)."
if skipped_count > 0:
msg += f"\nSkipped {skipped_count} template(s)."
messagebox.showinfo("Import Complete", msg)
except json.JSONDecodeError as e:
messagebox.showerror("Import Error", f"Invalid JSON file: {str(e)}")
logger.error(f"[TEMPLATE] Import failed - invalid JSON: {e}")
except Exception as e:
messagebox.showerror("Import Error", f"Failed to import template: {str(e)}")
logger.error(f"[TEMPLATE] Import failed: {e}")
def _export_template(self):
"""Export the selected template to a JSON file."""
selection = self.templates_tree.selection()
if not selection:
messagebox.showwarning("No Selection", "Please select a template to export.")
return
item = self.templates_tree.item(selection[0])
template_name = item['values'][0]
# Find the template
templates = self.settings.get_templates()
template = None
for t in templates:
if t.name == template_name:
template = t
break
if not template:
messagebox.showerror("Error", f"Template '{template_name}' not found.")
return
# Prepare export data
from dataclasses import asdict
export_data = {
'name': template.name,
'data_type': template.data_type,
'mappings': [asdict(m) for m in template.mappings],
'created_at': template.created_at,
'updated_at': template.updated_at
}
# Get save location
safe_name = "".join(c for c in template_name if c.isalnum() or c in " -_").strip()
file_path = filedialog.asksaveasfilename(
title="Export Template",
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
initialfile=f"template_{safe_name}.json"
)
if not file_path:
return
try:
with open(file_path, 'w') as f:
json.dump(export_data, f, indent=2)
messagebox.showinfo("Export Complete", f"Template '{template_name}' exported successfully.")
logger.info(f"[TEMPLATE] Exported template: {template_name} to {file_path}")
except Exception as e:
messagebox.showerror("Export Error", f"Failed to export template: {str(e)}")
logger.error(f"[TEMPLATE] Export failed: {e}")
def _export_all_templates(self):
"""Export all templates to a single JSON file."""
templates = self.settings.get_templates()
if not templates:
messagebox.showwarning("No Templates", "No templates to export.")
return
# Prepare export data
from dataclasses import asdict
export_data = []
for template in templates:
template_dict = {
'name': template.name,
'data_type': template.data_type,
'mappings': [asdict(m) for m in template.mappings],
'created_at': template.created_at,
'updated_at': template.updated_at
}
export_data.append(template_dict)
# Get save location
file_path = filedialog.asksaveasfilename(
title="Export All Templates",
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
initialfile=f"templates_backup_{datetime.now().strftime('%Y%m%d')}.json"
)
if not file_path:
return
try:
with open(file_path, 'w') as f:
json.dump(export_data, f, indent=2)
messagebox.showinfo(
"Export Complete",
f"Exported {len(templates)} template(s) successfully."
)
logger.info(f"[TEMPLATE] Exported {len(templates)} templates to {file_path}")
except Exception as e:
messagebox.showerror("Export Error", f"Failed to export templates: {str(e)}")
logger.error(f"[TEMPLATE] Export all failed: {e}")
# =========================================================================
# Other UI Methods