Update desktop app

This commit is contained in:
2026-02-18 10:23:36 -05:00
parent 34d73a6d2c
commit 0f913ae3c9
+474 -52
View File
@@ -335,10 +335,10 @@ class QBOExcelSyncApp:
self.notebook = ttk.Notebook(self.main_frame)
self.notebook.pack(fill=tk.BOTH, expand=True)
# Create tabs
# Create tabs - Order: Dashboard | Connection | Import | Templates | Lookup
self._create_dashboard_tab()
self._create_import_tab()
self._create_connection_tab()
self._create_import_tab()
self._create_templates_tab()
self._create_lookup_tab()
@@ -377,15 +377,6 @@ class QBOExcelSyncApp:
)
self.conn_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
# Environment card
env_card = self._create_stat_card(
stats_frame,
"Environment",
self.settings.qbo_environment.title(),
"Test mode" if self.settings.qbo_environment == 'sandbox' else "Live data"
)
env_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
# Supported types card
types_card = self._create_stat_card(
stats_frame,
@@ -404,22 +395,29 @@ class QBOExcelSyncApp:
ttk.Button(
actions_inner,
text="📁 Import Excel File",
command=lambda: self.notebook.select(1), # Switch to Import tab
text="🔗 Manage Connection",
command=lambda: self.notebook.select(1), # Connection tab (index 1)
width=25
).pack(side=tk.LEFT, padx=5)
ttk.Button(
actions_inner,
text="🔗 Manage Connection",
command=lambda: self.notebook.select(2), # Switch to Connection tab
text="📁 Import Excel File",
command=lambda: self.notebook.select(2), # Import tab (index 2)
width=25
).pack(side=tk.LEFT, padx=5)
ttk.Button(
actions_inner,
text="📋 Manage Templates",
command=lambda: self.notebook.select(3), # Switch to Templates tab
command=lambda: self.notebook.select(3), # Templates tab (index 3)
width=25
).pack(side=tk.LEFT, padx=5)
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)
@@ -666,6 +664,15 @@ class QBOExcelSyncApp:
ttk.Button(profile_frame, text="Save As...", command=self._save_profile_as, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(profile_frame, text="Delete", command=self._delete_profile, width=8).pack(side=tk.LEFT, padx=2)
# Import/Export buttons row
profile_io_frame = ttk.Frame(self.qbo_frame)
profile_io_frame.pack(fill=tk.X, pady=(5, 0))
ttk.Label(profile_io_frame, text="Profile Import/Export:", foreground='gray').pack(side=tk.LEFT)
ttk.Button(profile_io_frame, text="Import Profile", command=self._import_profile, width=14).pack(side=tk.LEFT, padx=(10, 2))
ttk.Button(profile_io_frame, text="Export Profile", command=self._export_profile, width=14).pack(side=tk.LEFT, padx=2)
ttk.Button(profile_io_frame, text="Export All", command=self._export_all_profiles, width=12).pack(side=tk.LEFT, padx=2)
# Separator
ttk.Separator(self.qbo_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
@@ -1837,8 +1844,11 @@ except Exception as e:
sys.exit(1)
'''
# Get Python executable (handles PyInstaller bundled case)
python_exe = self._get_python_executable()
result = subprocess.run(
[sys.executable, '-c', connect_script],
[python_exe, '-c', connect_script],
capture_output=True,
text=True,
timeout=60
@@ -2146,6 +2156,195 @@ except Exception as e:
messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' deleted.")
logger.info(f"[PROFILE] Deleted profile: {profile_name}")
def _import_profile(self):
"""Import profile(s) from a JSON file."""
file_path = filedialog.askopenfilename(
title="Import Profile",
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 profile and multiple profiles
if isinstance(data, list):
profiles_data = data
elif isinstance(data, dict):
profiles_data = [data]
else:
raise ValueError("Invalid profile format")
imported_count = 0
skipped_count = 0
for profile_data in profiles_data:
# Validate required fields
if 'name' not in profile_data:
skipped_count += 1
continue
profile_name = profile_data['name']
# Check if profile already exists
existing = self.settings.get_profile(profile_name)
if existing:
overwrite = messagebox.askyesno(
"Profile Exists",
f"Profile '{profile_name}' already exists. Overwrite?"
)
if not overwrite:
skipped_count += 1
continue
# Create ConnectionProfile from data
profile = ConnectionProfile(
name=profile_data.get('name', ''),
client_id=profile_data.get('client_id', ''),
client_secret=profile_data.get('client_secret', ''),
redirect_uri=profile_data.get('redirect_uri', 'http://localhost:9000/oauth/callback'),
environment=profile_data.get('environment', 'sandbox'),
access_token=profile_data.get('access_token'),
refresh_token=profile_data.get('refresh_token'),
realm_id=profile_data.get('realm_id'),
company_name=profile_data.get('company_name'),
token_expiry=profile_data.get('token_expiry'),
created_at=profile_data.get('created_at', datetime.now().isoformat()),
updated_at=datetime.now().isoformat(),
)
self.settings.save_profile(profile)
imported_count += 1
logger.info(f"[PROFILE] Imported profile: {profile_name}")
# Refresh the dropdown
self._refresh_profiles()
# Show result
msg = f"Imported {imported_count} profile(s)."
if skipped_count > 0:
msg += f"\nSkipped {skipped_count} profile(s)."
messagebox.showinfo("Import Complete", msg)
except json.JSONDecodeError as e:
messagebox.showerror("Import Error", f"Invalid JSON file: {str(e)}")
logger.error(f"[PROFILE] Import failed - invalid JSON: {e}")
except Exception as e:
messagebox.showerror("Import Error", f"Failed to import profile: {str(e)}")
logger.error(f"[PROFILE] Import failed: {e}")
def _export_profile(self):
"""Export the selected profile to a JSON file."""
profile_name = self.profile_var.get()
if not profile_name:
messagebox.showwarning("No Profile", "Please select a profile to export.")
return
profile = self.settings.get_profile(profile_name)
if not profile:
messagebox.showerror("Error", f"Profile '{profile_name}' not found.")
return
# Ask whether to include tokens (sensitive data)
include_tokens = messagebox.askyesno(
"Include Tokens?",
"Do you want to include access tokens in the export?\n\n"
"• Yes - Include tokens (allows immediate connection on import)\n"
"• No - Exclude tokens (more secure, requires re-authentication)"
)
# Prepare export data
from dataclasses import asdict
export_data = asdict(profile)
if not include_tokens:
export_data['access_token'] = None
export_data['refresh_token'] = None
export_data['token_expiry'] = None
# Get save location
safe_name = "".join(c for c in profile_name if c.isalnum() or c in " -_").strip()
file_path = filedialog.asksaveasfilename(
title="Export Profile",
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
initialfile=f"qbo_profile_{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"Profile '{profile_name}' exported successfully.")
logger.info(f"[PROFILE] Exported profile: {profile_name} to {file_path}")
except Exception as e:
messagebox.showerror("Export Error", f"Failed to export profile: {str(e)}")
logger.error(f"[PROFILE] Export failed: {e}")
def _export_all_profiles(self):
"""Export all profiles to a single JSON file."""
profiles = self.settings.get_profiles()
if not profiles:
messagebox.showwarning("No Profiles", "No profiles to export.")
return
# Ask whether to include tokens (sensitive data)
include_tokens = messagebox.askyesno(
"Include Tokens?",
f"Exporting {len(profiles)} profile(s).\n\n"
"Do you want to include access tokens in the export?\n\n"
"• Yes - Include tokens (allows immediate connection on import)\n"
"• No - Exclude tokens (more secure, requires re-authentication)"
)
# Prepare export data
from dataclasses import asdict
export_data = []
for profile in profiles:
profile_dict = asdict(profile)
if not include_tokens:
profile_dict['access_token'] = None
profile_dict['refresh_token'] = None
profile_dict['token_expiry'] = None
export_data.append(profile_dict)
# Get save location
file_path = filedialog.asksaveasfilename(
title="Export All Profiles",
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
initialfile=f"qbo_profiles_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(profiles)} profile(s) successfully."
)
logger.info(f"[PROFILE] Exported {len(profiles)} profiles to {file_path}")
except Exception as e:
messagebox.showerror("Export Error", f"Failed to export profiles: {str(e)}")
logger.error(f"[PROFILE] Export all failed: {e}")
# =========================================================================
# Import Methods
# =========================================================================
@@ -3649,6 +3848,46 @@ except Exception as e:
return []
def _get_python_executable(self) -> str:
"""Get the path to a Python executable for subprocess calls.
When running as a PyInstaller bundle, sys.executable points to the bundle,
so we need to find the system Python installation."""
import shutil
# Check if we're running as a PyInstaller bundle
if getattr(sys, 'frozen', False):
# Try to find system Python
# Check common Python installation paths on Windows
python_paths = [
shutil.which('python'),
shutil.which('python3'),
shutil.which('py'),
r'C:\Python312\python.exe',
r'C:\Python311\python.exe',
r'C:\Python310\python.exe',
r'C:\Python39\python.exe',
r'C:\Python38\python.exe',
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Python\Python312\python.exe'),
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Python\Python311\python.exe'),
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Python\Python310\python.exe'),
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Python\Python39\python.exe'),
]
for python_path in python_paths:
if python_path and os.path.isfile(python_path):
logger.info(f"Found system Python at: {python_path}")
return python_path
# If no system Python found, raise an error
raise Exception(
"QuickBooks Desktop requires Python to be installed on your system.\n"
"Please install Python from https://www.python.org/downloads/\n"
"and make sure to check 'Add Python to PATH' during installation."
)
else:
# Running as script, use current Python
return sys.executable
def _fetch_qbd_entities(self, entity_type: str, search_term: str = None, type_filter: str = None) -> List[Dict]:
"""Fetch entities from QuickBooks Desktop using subprocess for COM compatibility."""
if not HAS_QBD:
@@ -3767,8 +4006,11 @@ except Exception as e:
'''
try:
# Get Python executable (handles PyInstaller bundled case)
python_exe = self._get_python_executable()
result = subprocess.run(
[sys.executable, '-c', query_script],
[python_exe, '-c', query_script],
capture_output=True,
text=True,
timeout=120
@@ -3969,74 +4211,254 @@ except Exception as e:
dialog.title("Application Logs")
dialog.geometry("800x500")
# Read log file
log_file = PROJECT_ROOT / 'desktop_app.log'
# Read log file - check multiple possible locations
possible_log_paths = [
Path('desktop_app.log'), # Current working directory
PROJECT_ROOT / 'desktop_app.log', # Script directory
Path(sys.executable).parent / 'desktop_app.log', # Executable directory
Path.home() / 'desktop_app.log', # Home directory
]
log_file = None
for path in possible_log_paths:
if path.exists():
log_file = path
break
text = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Consolas', 9))
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
if log_file.exists():
with open(log_file, 'r') as f:
text.insert(tk.END, f.read())
if log_file and log_file.exists():
try:
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
text.insert(tk.END, f.read())
text.insert(tk.END, f"\n\n--- Log file: {log_file} ---")
except Exception as e:
text.insert(tk.END, f"Error reading log file: {e}")
else:
text.insert(tk.END, "No log file found.")
text.insert(tk.END, f"No log file found.\n\nSearched locations:\n")
for path in possible_log_paths:
text.insert(tk.END, f" - {path}\n")
text.config(state=tk.DISABLED)
# Scroll to end
text.see(tk.END)
btn_frame = ttk.Frame(dialog)
btn_frame.pack(fill=tk.X, padx=10, pady=5)
def clear_logs():
if messagebox.askyesno("Confirm", "Clear all logs?"):
if log_file.exists():
if log_file and log_file.exists():
log_file.unlink()
text.config(state=tk.NORMAL)
text.delete(1.0, tk.END)
text.insert(tk.END, "Logs cleared.")
text.config(state=tk.DISABLED)
def open_log_folder():
if log_file:
folder = log_file.parent
else:
folder = Path.cwd()
if sys.platform == 'win32':
os.startfile(folder)
elif sys.platform == 'darwin':
subprocess.run(['open', folder])
else:
subprocess.run(['xdg-open', folder])
ttk.Button(btn_frame, text="Clear Logs", command=clear_logs).pack(side=tk.LEFT)
ttk.Button(btn_frame, text="Refresh", command=lambda: dialog.destroy() or self._show_logs()).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Open Folder", command=open_log_folder).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Refresh", command=lambda: (dialog.destroy(), self._show_logs())).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Close", command=dialog.destroy).pack(side=tk.RIGHT)
def _show_help(self):
"""Show help documentation."""
help_text = """
QBO Excel Sync - Desktop Application
=====================================
╔═══════════════════════════════════════════════════════════════════════════╗
║ QBO EXCEL SYNC - USER GUIDE ║
╚═══════════════════════════════════════════════════════════════════════════╝
This application allows you to import Excel data into QuickBooks Online
or QuickBooks Desktop.
TABLE OF CONTENTS
─────────────────
1. Installation & Setup
2. Connecting to QuickBooks
3. Connection Profiles
4. Importing Data
5. Field Mapping & Templates
6. QuickBooks Lookup
7. Troubleshooting
Quick Start:
1. Connect to QuickBooks (Connection tab)
2. Select an Excel file (Import tab)
3. Choose the data type (Check, Invoice, Bill, etc.)
4. Map Excel columns to QuickBooks fields
5. Click "Start Import"
═══════════════════════════════════════════════════════════════════════════
1. INSTALLATION & SETUP
═══════════════════════════════════════════════════════════════════════════
Supported Data Types:
- Checks: Payment checks with payee and amount
- Invoices: Customer invoices with line items
- Bills: Vendor bills with expense accounts
- Customers: New customer records
- Vendors: New vendor records
- Accounts: Chart of accounts entries
FOR EXECUTABLE VERSION (.exe):
• Simply double-click QBO_Excel_Sync.exe to run
• No installation required
• For QuickBooks Desktop: Python must be installed on your system
Tips:
- Use "Auto-Map" to automatically match common field names
- Save frequently used mappings as templates
- Preview your data before importing
- Check the import log for any errors
FOR SOURCE VERSION:
1. Install Python 3.8 or higher from https://www.python.org
2. Install dependencies: pip install -r requirements.txt
3. Run: python desktop_app.py
For more information, visit the documentation website.
BUILDING THE EXECUTABLE:
1. Run build_windows.bat (Windows)
2. Find executable in dist/QBO_Excel_Sync.exe
═══════════════════════════════════════════════════════════════════════════
2. CONNECTING TO QUICKBOOKS
═══════════════════════════════════════════════════════════════════════════
QUICKBOOKS ONLINE:
1. Go to the Connection tab
2. Select "QuickBooks Online"
3. Enter your API credentials:
• Client ID: From Intuit Developer Portal
• Client Secret: From Intuit Developer Portal
• Redirect URI: Your callback URL
• Environment: sandbox (testing) or production (live)
4. Click "Connect to QuickBooks"
5. Authorize in the browser
6. For remote callback servers: Copy the code back to the app
QUICKBOOKS DESKTOP:
1. Open QuickBooks Desktop with your company file
2. Go to the Connection tab
3. Select "QuickBooks Desktop"
4. Click "Connect to QuickBooks Desktop"
5. Authorize the connection in QuickBooks Desktop
═══════════════════════════════════════════════════════════════════════════
3. CONNECTION PROFILES
═══════════════════════════════════════════════════════════════════════════
Profiles save your connection settings for easy switching between companies.
MANAGING PROFILES:
• Load: Load the selected profile into the form
• Save: Save current settings to the selected profile
• Save As: Create a new profile with current settings
• Delete: Remove the selected profile
IMPORT/EXPORT PROFILES:
• Import Profile: Load profile from a JSON file
• Export Profile: Save selected profile to a JSON file
• Export All: Backup all profiles to a single JSON file
When exporting, you can choose to include or exclude access tokens:
• Include tokens: Can connect immediately after import
• Exclude tokens: More secure, requires re-authentication
═══════════════════════════════════════════════════════════════════════════
4. IMPORTING DATA
═══════════════════════════════════════════════════════════════════════════
SUPPORTED DATA TYPES:
• Checks: Payment checks with payee, bank account, and expense details
• Invoices: Customer invoices with line items
• Bills: Vendor bills with expense accounts
• Customers: Customer records
• Vendors: Vendor records
• Accounts: Chart of accounts entries
IMPORT PROCESS:
1. Click "Browse" to select an Excel file (.xlsx, .xls)
2. Select the data type from the dropdown
3. Map Excel columns to QuickBooks fields (or use Auto-Map)
4. Click "Preview" to verify data
5. Click "Start Import" to begin
IMPORT OPTIONS (Tools → Settings):
• Skip Empty Rows: Ignore rows with no data
• Validate Before Import: Check data before sending
• Stop on Error: Halt import if an error occurs
• Date Format: Expected date format in your Excel file
═══════════════════════════════════════════════════════════════════════════
5. FIELD MAPPING & TEMPLATES
═══════════════════════════════════════════════════════════════════════════
FIELD MAPPING:
• Select an Excel column on the left
• Select a QuickBooks field on the right
• Click "Add Mapping" to create the link
• Use "Auto-Map" to automatically match common field names
TEMPLATES:
• Save frequently used mappings as templates
• Load templates to quickly apply mappings
• Templates are stored locally and persist between sessions
REQUIRED FIELDS (vary by data type):
• Checks: Payee, Bank Account, Date, Amount, Expense Account
• Invoices: Customer, Date, Line Items
• Bills: Vendor, Date, Expense Account, Amount
═══════════════════════════════════════════════════════════════════════════
6. QUICKBOOKS LOOKUP
═══════════════════════════════════════════════════════════════════════════
The Lookup tab lets you search and view QuickBooks data.
SEARCHABLE ENTITIES:
• Accounts (filter by type: Bank, Expense, Income, etc.)
• Customers
• Vendors
• Items
• Employees
• Classes
• Payment Methods
• Terms
FEATURES:
• Search by name
• Filter accounts by type
• Export results to CSV
• Double-click to copy ID and name
• View both Raw ID and Clean ID (for QuickBooks Desktop)
═══════════════════════════════════════════════════════════════════════════
7. TROUBLESHOOTING
═══════════════════════════════════════════════════════════════════════════
CONNECTION ISSUES:
"Invalid account type" error: Verify the Bank Account is type "Bank"
• OAuth errors: Check Client ID, Secret, and Redirect URI match exactly
• Production sandbox mismatch: Ensure Environment setting is correct
QUICKBOOKS DESKTOP:
"No module named encodings": Install Python on your system
• Connection timeout: Ensure QuickBooks Desktop is open with company file
• Authorization denied: Authorize the app in QuickBooks Desktop
IMPORT ERRORS:
"Entity not found": Check that referenced accounts/customers exist
"Invalid date": Verify date format matches your settings
• Account type errors: Use correct account types (Bank for checks, etc.)
LOG FILES:
• View logs: Tools → View Logs
• Log location: Same directory as the application
GETTING HELP:
• Check the log file for detailed error messages
• Verify your data in the Preview before importing
• Use the Lookup tab to find correct account names and IDs
═══════════════════════════════════════════════════════════════════════════
© 2024-2026 QBO Excel Sync
═══════════════════════════════════════════════════════════════════════════
"""
dialog = tk.Toplevel(self.root)
dialog.title("Help")
dialog.geometry("600x500")
dialog.title("Help - User Guide")
dialog.geometry("750x600")
text = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Segoe UI', 10))
text = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Consolas', 10))
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text.insert(tk.END, help_text)
text.config(state=tk.DISABLED)