Desktop app update
This commit is contained in:
+476
-187
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user