Update desktop app
This commit is contained in:
+474
-52
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user