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 = ttk.Notebook(self.main_frame)
|
||||||
self.notebook.pack(fill=tk.BOTH, expand=True)
|
self.notebook.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Create tabs
|
# Create tabs - Order: Dashboard | Connection | Import | Templates | Lookup
|
||||||
self._create_dashboard_tab()
|
self._create_dashboard_tab()
|
||||||
self._create_import_tab()
|
|
||||||
self._create_connection_tab()
|
self._create_connection_tab()
|
||||||
|
self._create_import_tab()
|
||||||
self._create_templates_tab()
|
self._create_templates_tab()
|
||||||
self._create_lookup_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))
|
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
|
# Supported types card
|
||||||
types_card = self._create_stat_card(
|
types_card = self._create_stat_card(
|
||||||
stats_frame,
|
stats_frame,
|
||||||
@@ -404,22 +395,29 @@ class QBOExcelSyncApp:
|
|||||||
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
actions_inner,
|
actions_inner,
|
||||||
text="📁 Import Excel File",
|
text="🔗 Manage Connection",
|
||||||
command=lambda: self.notebook.select(1), # Switch to Import tab
|
command=lambda: self.notebook.select(1), # Connection tab (index 1)
|
||||||
width=25
|
width=25
|
||||||
).pack(side=tk.LEFT, padx=5)
|
).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
actions_inner,
|
actions_inner,
|
||||||
text="🔗 Manage Connection",
|
text="📁 Import Excel File",
|
||||||
command=lambda: self.notebook.select(2), # Switch to Connection tab
|
command=lambda: self.notebook.select(2), # Import tab (index 2)
|
||||||
width=25
|
width=25
|
||||||
).pack(side=tk.LEFT, padx=5)
|
).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
actions_inner,
|
actions_inner,
|
||||||
text="📋 Manage Templates",
|
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
|
width=25
|
||||||
).pack(side=tk.LEFT, padx=5)
|
).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="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)
|
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
|
# Separator
|
||||||
ttk.Separator(self.qbo_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
|
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)
|
sys.exit(1)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# Get Python executable (handles PyInstaller bundled case)
|
||||||
|
python_exe = self._get_python_executable()
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, '-c', connect_script],
|
[python_exe, '-c', connect_script],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60
|
timeout=60
|
||||||
@@ -2146,6 +2156,195 @@ except Exception as e:
|
|||||||
messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' deleted.")
|
messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' deleted.")
|
||||||
logger.info(f"[PROFILE] Deleted profile: {profile_name}")
|
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
|
# Import Methods
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -3649,6 +3848,46 @@ except Exception as e:
|
|||||||
|
|
||||||
return []
|
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]:
|
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."""
|
"""Fetch entities from QuickBooks Desktop using subprocess for COM compatibility."""
|
||||||
if not HAS_QBD:
|
if not HAS_QBD:
|
||||||
@@ -3767,8 +4006,11 @@ except Exception as e:
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Get Python executable (handles PyInstaller bundled case)
|
||||||
|
python_exe = self._get_python_executable()
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, '-c', query_script],
|
[python_exe, '-c', query_script],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120
|
timeout=120
|
||||||
@@ -3969,74 +4211,254 @@ except Exception as e:
|
|||||||
dialog.title("Application Logs")
|
dialog.title("Application Logs")
|
||||||
dialog.geometry("800x500")
|
dialog.geometry("800x500")
|
||||||
|
|
||||||
# Read log file
|
# Read log file - check multiple possible locations
|
||||||
log_file = PROJECT_ROOT / 'desktop_app.log'
|
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 = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Consolas', 9))
|
||||||
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
if log_file.exists():
|
if log_file and log_file.exists():
|
||||||
with open(log_file, 'r') as f:
|
try:
|
||||||
text.insert(tk.END, f.read())
|
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:
|
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)
|
text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Scroll to end
|
||||||
|
text.see(tk.END)
|
||||||
|
|
||||||
btn_frame = ttk.Frame(dialog)
|
btn_frame = ttk.Frame(dialog)
|
||||||
btn_frame.pack(fill=tk.X, padx=10, pady=5)
|
btn_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||||
|
|
||||||
def clear_logs():
|
def clear_logs():
|
||||||
if messagebox.askyesno("Confirm", "Clear all logs?"):
|
if messagebox.askyesno("Confirm", "Clear all logs?"):
|
||||||
if log_file.exists():
|
if log_file and log_file.exists():
|
||||||
log_file.unlink()
|
log_file.unlink()
|
||||||
text.config(state=tk.NORMAL)
|
text.config(state=tk.NORMAL)
|
||||||
text.delete(1.0, tk.END)
|
text.delete(1.0, tk.END)
|
||||||
text.insert(tk.END, "Logs cleared.")
|
text.insert(tk.END, "Logs cleared.")
|
||||||
text.config(state=tk.DISABLED)
|
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="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)
|
ttk.Button(btn_frame, text="Close", command=dialog.destroy).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
def _show_help(self):
|
def _show_help(self):
|
||||||
"""Show help documentation."""
|
"""Show help documentation."""
|
||||||
help_text = """
|
help_text = """
|
||||||
QBO Excel Sync - Desktop Application
|
╔═══════════════════════════════════════════════════════════════════════════╗
|
||||||
=====================================
|
║ QBO EXCEL SYNC - USER GUIDE ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
This application allows you to import Excel data into QuickBooks Online
|
TABLE OF CONTENTS
|
||||||
or QuickBooks Desktop.
|
─────────────────
|
||||||
|
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)
|
1. INSTALLATION & SETUP
|
||||||
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"
|
|
||||||
|
|
||||||
Supported Data Types:
|
FOR EXECUTABLE VERSION (.exe):
|
||||||
- Checks: Payment checks with payee and amount
|
• Simply double-click QBO_Excel_Sync.exe to run
|
||||||
- Invoices: Customer invoices with line items
|
• No installation required
|
||||||
- Bills: Vendor bills with expense accounts
|
• For QuickBooks Desktop: Python must be installed on your system
|
||||||
- Customers: New customer records
|
|
||||||
- Vendors: New vendor records
|
|
||||||
- Accounts: Chart of accounts entries
|
|
||||||
|
|
||||||
Tips:
|
FOR SOURCE VERSION:
|
||||||
- Use "Auto-Map" to automatically match common field names
|
1. Install Python 3.8 or higher from https://www.python.org
|
||||||
- Save frequently used mappings as templates
|
2. Install dependencies: pip install -r requirements.txt
|
||||||
- Preview your data before importing
|
3. Run: python desktop_app.py
|
||||||
- Check the import log for any errors
|
|
||||||
|
|
||||||
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 = tk.Toplevel(self.root)
|
||||||
dialog.title("Help")
|
dialog.title("Help - User Guide")
|
||||||
dialog.geometry("600x500")
|
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.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
text.insert(tk.END, help_text)
|
text.insert(tk.END, help_text)
|
||||||
text.config(state=tk.DISABLED)
|
text.config(state=tk.DISABLED)
|
||||||
|
|||||||
Reference in New Issue
Block a user