Update app callback server
This commit is contained in:
+368
-20
@@ -25,7 +25,7 @@ PROJECT_ROOT = Path(__file__).parent
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
# Import project modules
|
||||
from src.config.settings import Settings, ImportSettings, QBOCredentials
|
||||
from src.config.settings import Settings, ImportSettings, QBOCredentials, ConnectionProfile
|
||||
from src.api.qbo_client import QBOClient
|
||||
from src.core.excel_parser import ExcelParser, ParseResult
|
||||
from src.core.import_processor import ImportProcessor, ImportResult
|
||||
@@ -644,6 +644,31 @@ class QBOExcelSyncApp:
|
||||
self.qbo_frame = ttk.LabelFrame(tab, text="QuickBooks Online Connection", padding=15)
|
||||
self.qbo_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
# Profile selection row
|
||||
profile_frame = ttk.Frame(self.qbo_frame)
|
||||
profile_frame.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
ttk.Label(profile_frame, text="Connection Profile:", font=('Segoe UI', 10, 'bold')).pack(side=tk.LEFT)
|
||||
|
||||
self.profile_var = tk.StringVar(value="")
|
||||
self.profile_combo = ttk.Combobox(
|
||||
profile_frame,
|
||||
textvariable=self.profile_var,
|
||||
values=[],
|
||||
width=30,
|
||||
state='readonly'
|
||||
)
|
||||
self.profile_combo.pack(side=tk.LEFT, padx=(10, 5))
|
||||
self.profile_combo.bind('<<ComboboxSelected>>', self._on_profile_change)
|
||||
|
||||
ttk.Button(profile_frame, text="Load", command=self._load_selected_profile, width=8).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(profile_frame, text="Save", command=self._save_profile, width=8).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)
|
||||
|
||||
# Separator
|
||||
ttk.Separator(self.qbo_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
|
||||
|
||||
# API Credentials
|
||||
creds_frame = ttk.Frame(self.qbo_frame)
|
||||
creds_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
@@ -670,8 +695,7 @@ class QBOExcelSyncApp:
|
||||
qbo_buttons = ttk.Frame(self.qbo_frame)
|
||||
qbo_buttons.pack(fill=tk.X, pady=10)
|
||||
|
||||
ttk.Button(qbo_buttons, text="Save Credentials", command=self._save_qbo_credentials, width=20).pack(side=tk.LEFT)
|
||||
ttk.Button(qbo_buttons, text="Connect to QuickBooks", command=self._start_oauth, width=25).pack(side=tk.LEFT, padx=10)
|
||||
ttk.Button(qbo_buttons, text="Connect to QuickBooks", command=self._start_oauth, width=25).pack(side=tk.LEFT)
|
||||
|
||||
# Advanced options in a second row
|
||||
qbo_buttons2 = ttk.Frame(self.qbo_frame)
|
||||
@@ -683,7 +707,7 @@ class QBOExcelSyncApp:
|
||||
|
||||
# QBD Connection Frame
|
||||
self.qbd_frame = ttk.LabelFrame(tab, text="QuickBooks Desktop Connection", padding=15)
|
||||
self.qbd_frame.pack(fill=tk.X, pady=10)
|
||||
# Initially hidden - will be shown when QBD is selected
|
||||
|
||||
if HAS_QBD:
|
||||
qbd_info = ttk.Label(
|
||||
@@ -713,25 +737,25 @@ class QBOExcelSyncApp:
|
||||
self.qbd_connect_btn.pack(side=tk.LEFT)
|
||||
|
||||
# Connection status
|
||||
status_frame = ttk.LabelFrame(tab, text="Connection Status", padding=15)
|
||||
status_frame.pack(fill=tk.X, pady=20)
|
||||
self.status_frame = ttk.LabelFrame(tab, text="Connection Status", padding=15)
|
||||
self.status_frame.pack(fill=tk.X, pady=20)
|
||||
|
||||
self.conn_status_label = ttk.Label(
|
||||
status_frame,
|
||||
self.status_frame,
|
||||
text="● Not Connected",
|
||||
font=('Segoe UI', 11)
|
||||
)
|
||||
self.conn_status_label.pack(anchor=tk.W)
|
||||
|
||||
self.conn_company_label = ttk.Label(
|
||||
status_frame,
|
||||
self.status_frame,
|
||||
text="",
|
||||
font=('Segoe UI', 10),
|
||||
foreground='gray'
|
||||
)
|
||||
self.conn_company_label.pack(anchor=tk.W, pady=(5, 0))
|
||||
|
||||
conn_actions = ttk.Frame(status_frame)
|
||||
conn_actions = ttk.Frame(self.status_frame)
|
||||
conn_actions.pack(fill=tk.X, pady=(15, 0))
|
||||
|
||||
test_btn = ttk.Button(conn_actions, text="Test Connection", command=self._test_connection, width=15)
|
||||
@@ -740,7 +764,8 @@ class QBOExcelSyncApp:
|
||||
disconnect_btn = ttk.Button(conn_actions, text="Disconnect", command=self._disconnect, width=12)
|
||||
disconnect_btn.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# Load saved credentials
|
||||
# Load profiles and saved credentials
|
||||
self._refresh_profiles()
|
||||
self._load_saved_credentials()
|
||||
|
||||
def _create_templates_tab(self):
|
||||
@@ -865,10 +890,11 @@ class QBOExcelSyncApp:
|
||||
tree_container = ttk.Frame(results_frame)
|
||||
tree_container.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.lookup_columns = ['Id', 'Name', 'Type', 'SubType', 'Balance', 'Active']
|
||||
self.lookup_columns = ['RawId', 'Id', 'Name', 'Type', 'SubType', 'Balance', 'Active']
|
||||
self.lookup_tree = ttk.Treeview(tree_container, columns=self.lookup_columns, show='headings', height=15)
|
||||
|
||||
# Default column configuration
|
||||
self.lookup_tree.heading('RawId', text='Raw ID')
|
||||
self.lookup_tree.heading('Id', text='ID')
|
||||
self.lookup_tree.heading('Name', text='Name')
|
||||
self.lookup_tree.heading('Type', text='Type')
|
||||
@@ -876,6 +902,7 @@ class QBOExcelSyncApp:
|
||||
self.lookup_tree.heading('Balance', text='Balance')
|
||||
self.lookup_tree.heading('Active', text='Active')
|
||||
|
||||
self.lookup_tree.column('RawId', width=150, minwidth=100)
|
||||
self.lookup_tree.column('Id', width=60, minwidth=50)
|
||||
self.lookup_tree.column('Name', width=200, minwidth=100)
|
||||
self.lookup_tree.column('Type', width=120, minwidth=80)
|
||||
@@ -1108,6 +1135,7 @@ class QBOExcelSyncApp:
|
||||
|
||||
if entity_type == 'account':
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('Name', ''),
|
||||
entity.get('AccountType', ''),
|
||||
@@ -1117,6 +1145,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type in ['customer', 'vendor']:
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('DisplayName', ''),
|
||||
entity.get('CompanyName', ''),
|
||||
@@ -1127,6 +1156,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type == 'item':
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('Name', ''),
|
||||
entity.get('Type', ''),
|
||||
@@ -1136,6 +1166,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type in ['class', 'department']:
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('Name', ''),
|
||||
entity.get('FullyQualifiedName', ''),
|
||||
@@ -1143,6 +1174,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type == 'term':
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('Name', ''),
|
||||
entity.get('DueDays', ''),
|
||||
@@ -1150,6 +1182,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type == 'paymentmethod':
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('Name', ''),
|
||||
entity.get('Type', ''),
|
||||
@@ -1157,6 +1190,7 @@ class QBOExcelSyncApp:
|
||||
)
|
||||
elif entity_type == 'employee':
|
||||
return (
|
||||
entity.get('RawId', entity.get('Id', '')),
|
||||
entity.get('Id', ''),
|
||||
entity.get('DisplayName', ''),
|
||||
entity.get('GivenName', ''),
|
||||
@@ -1164,7 +1198,7 @@ class QBOExcelSyncApp:
|
||||
format_val(entity.get('Active')),
|
||||
)
|
||||
else:
|
||||
return (entity.get('Id', ''), entity.get('Name', ''))
|
||||
return (entity.get('RawId', entity.get('Id', '')), entity.get('Id', ''), entity.get('Name', ''))
|
||||
|
||||
def _copy_lookup_selection(self):
|
||||
"""Copy selected lookup row to clipboard."""
|
||||
@@ -1174,8 +1208,8 @@ class QBOExcelSyncApp:
|
||||
return
|
||||
|
||||
values = self.lookup_tree.item(selection[0])['values']
|
||||
# Format as ID: Name
|
||||
text = f"{values[0]}: {values[1]}"
|
||||
# Format as ID: Name (ID is now at index 1, Name at index 2)
|
||||
text = f"{values[1]}: {values[2]}"
|
||||
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(text)
|
||||
@@ -1259,7 +1293,8 @@ class QBOExcelSyncApp:
|
||||
creds = QBOCredentials(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=redirect_uri
|
||||
redirect_uri=redirect_uri,
|
||||
environment=environment
|
||||
)
|
||||
|
||||
self.settings.qbo_environment = environment
|
||||
@@ -1278,8 +1313,103 @@ class QBOExcelSyncApp:
|
||||
messagebox.showwarning("Missing Information", "Please enter Client ID and Client Secret first.")
|
||||
return
|
||||
|
||||
# Start OAuth with automatic callback capture
|
||||
self._start_oauth_with_callback(client_id, client_secret, redirect_uri)
|
||||
# Check if using localhost or remote callback
|
||||
if 'localhost' in redirect_uri or '127.0.0.1' in redirect_uri:
|
||||
# Local callback - use automatic capture
|
||||
self._start_oauth_with_callback(client_id, client_secret, redirect_uri)
|
||||
else:
|
||||
# Remote callback - open browser and prompt for manual code entry
|
||||
self._start_oauth_remote_callback(client_id, client_secret, redirect_uri)
|
||||
|
||||
def _start_oauth_remote_callback(self, client_id: str, client_secret: str, redirect_uri: str):
|
||||
"""Start OAuth flow with remote callback server (for production)."""
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# Generate OAuth URL
|
||||
auth_base = "https://appcenter.intuit.com/connect/oauth2"
|
||||
|
||||
params = {
|
||||
'client_id': client_id,
|
||||
'response_type': 'code',
|
||||
'scope': 'com.intuit.quickbooks.accounting',
|
||||
'redirect_uri': redirect_uri,
|
||||
'state': 'desktop_app',
|
||||
'prompt': 'consent',
|
||||
}
|
||||
|
||||
auth_url = f"{auth_base}?{urlencode(params)}"
|
||||
|
||||
# Open browser
|
||||
webbrowser.open(auth_url)
|
||||
|
||||
# Show dialog for manual code entry
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title("Complete Authorization")
|
||||
dialog.geometry("550x350")
|
||||
dialog.transient(self.root)
|
||||
dialog.grab_set()
|
||||
|
||||
# Center the dialog
|
||||
dialog.update_idletasks()
|
||||
x = (dialog.winfo_screenwidth() - 550) // 2
|
||||
y = (dialog.winfo_screenheight() - 350) // 2
|
||||
dialog.geometry(f"550x350+{x}+{y}")
|
||||
|
||||
ttk.Label(
|
||||
dialog,
|
||||
text="Complete the authorization in your browser.\n\n"
|
||||
"After authorizing, you'll be redirected to your callback server.\n"
|
||||
"Copy the Authorization Code and Realm ID from that page:",
|
||||
wraplength=500,
|
||||
justify=tk.CENTER,
|
||||
font=('Segoe UI', 10)
|
||||
).pack(padx=20, pady=20)
|
||||
|
||||
# Code entry
|
||||
code_frame = ttk.Frame(dialog)
|
||||
code_frame.pack(fill=tk.X, padx=30, pady=5)
|
||||
ttk.Label(code_frame, text="Authorization Code:", width=18, anchor=tk.W).pack(side=tk.LEFT)
|
||||
code_entry = ttk.Entry(code_frame, width=40)
|
||||
code_entry.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# Realm ID entry
|
||||
realm_frame = ttk.Frame(dialog)
|
||||
realm_frame.pack(fill=tk.X, padx=30, pady=5)
|
||||
ttk.Label(realm_frame, text="Realm ID (Company ID):", width=18, anchor=tk.W).pack(side=tk.LEFT)
|
||||
realm_entry = ttk.Entry(realm_frame, width=40)
|
||||
realm_entry.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
def submit():
|
||||
code = code_entry.get().strip()
|
||||
realm_id = realm_entry.get().strip()
|
||||
|
||||
if not code or not realm_id:
|
||||
messagebox.showwarning("Missing Information", "Please enter both the Authorization Code and Realm ID.")
|
||||
return
|
||||
|
||||
dialog.destroy()
|
||||
self._exchange_code_for_token(code, realm_id)
|
||||
|
||||
def reopen_browser():
|
||||
webbrowser.open(auth_url)
|
||||
|
||||
# Buttons
|
||||
btn_frame = ttk.Frame(dialog)
|
||||
btn_frame.pack(pady=25)
|
||||
|
||||
ttk.Button(btn_frame, text="Connect", command=submit, width=15).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(btn_frame, text="Re-open Browser", command=reopen_browser, width=15).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(btn_frame, text="Cancel", command=dialog.destroy, width=12).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Help text
|
||||
ttk.Label(
|
||||
dialog,
|
||||
text="Tip: The code expires in 5 minutes. Click 'Re-open Browser' if needed.",
|
||||
foreground='gray',
|
||||
font=('Segoe UI', 9)
|
||||
).pack(pady=(0, 10))
|
||||
|
||||
logger.info(f"[OAUTH] Started remote callback OAuth flow, redirect_uri: {redirect_uri}")
|
||||
|
||||
def _start_oauth_with_callback(self, client_id: str, client_secret: str, redirect_uri: str):
|
||||
"""Start OAuth flow with automatic callback capture using a local server."""
|
||||
@@ -1426,7 +1556,10 @@ class QBOExcelSyncApp:
|
||||
'response_type': 'code',
|
||||
'scope': 'com.intuit.quickbooks.accounting',
|
||||
'redirect_uri': redirect_uri,
|
||||
'state': 'desktop_app'
|
||||
'state': 'desktop_app',
|
||||
# Force user to select company file every time
|
||||
# This ensures switching profiles connects to the correct company
|
||||
'prompt': 'consent',
|
||||
}
|
||||
|
||||
auth_url = f"{auth_base}?{urlencode(params)}"
|
||||
@@ -1536,11 +1669,13 @@ class QBOExcelSyncApp:
|
||||
if response.status_code == 200:
|
||||
token_data = response.json()
|
||||
|
||||
# Save credentials
|
||||
# Save credentials with environment setting
|
||||
environment = self.env_var.get()
|
||||
creds = QBOCredentials(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=redirect_uri,
|
||||
environment=environment,
|
||||
access_token=token_data.get('access_token'),
|
||||
refresh_token=token_data.get('refresh_token'),
|
||||
realm_id=realm_id,
|
||||
@@ -1559,6 +1694,24 @@ class QBOExcelSyncApp:
|
||||
|
||||
self._update_connection_ui()
|
||||
|
||||
# Update current profile with tokens if one is selected
|
||||
profile_name = self.profile_var.get() if hasattr(self, 'profile_var') else None
|
||||
if profile_name:
|
||||
profile = ConnectionProfile(
|
||||
name=profile_name,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=redirect_uri,
|
||||
environment=self.env_var.get(),
|
||||
access_token=token_data.get('access_token'),
|
||||
refresh_token=token_data.get('refresh_token'),
|
||||
realm_id=realm_id,
|
||||
company_name=self.company_name,
|
||||
token_expiry=datetime.now().isoformat(),
|
||||
)
|
||||
self.settings.save_profile(profile)
|
||||
logger.info(f"[PROFILE] Updated profile '{profile_name}' with new tokens")
|
||||
|
||||
messagebox.showinfo("Connected", f"Successfully connected to {self.company_name}")
|
||||
logger.info(f"Connected to QBO: {self.company_name}")
|
||||
else:
|
||||
@@ -1603,6 +1756,7 @@ class QBOExcelSyncApp:
|
||||
client_id=self.client_id_entry.get().strip(),
|
||||
client_secret=self.client_secret_entry.get().strip(),
|
||||
redirect_uri=self.redirect_uri_entry.get().strip(),
|
||||
environment=self.env_var.get(),
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
realm_id=realm_id,
|
||||
@@ -1788,9 +1942,13 @@ except Exception as e:
|
||||
"""Handle connection type change."""
|
||||
conn_type = self.conn_type_var.get()
|
||||
if conn_type == 'qbo':
|
||||
self.qbo_frame.pack(fill=tk.X, pady=10, before=self.qbd_frame)
|
||||
# Show QBO frame, hide QBD frame
|
||||
self.qbd_frame.pack_forget()
|
||||
self.qbo_frame.pack(fill=tk.X, pady=10, before=self.status_frame)
|
||||
else:
|
||||
# Show QBD frame, hide QBO frame
|
||||
self.qbo_frame.pack_forget()
|
||||
self.qbd_frame.pack(fill=tk.X, pady=10, before=self.status_frame)
|
||||
|
||||
def _load_saved_credentials(self):
|
||||
"""Load saved credentials into the form."""
|
||||
@@ -1806,6 +1964,188 @@ except Exception as e:
|
||||
self.redirect_uri_entry.delete(0, tk.END)
|
||||
self.redirect_uri_entry.insert(0, creds.redirect_uri)
|
||||
|
||||
# =========================================================================
|
||||
# Profile Management Methods
|
||||
# =========================================================================
|
||||
|
||||
def _refresh_profiles(self):
|
||||
"""Refresh the profile dropdown list."""
|
||||
profiles = self.settings.get_profiles()
|
||||
profile_names = [p.name for p in profiles]
|
||||
self.profile_combo['values'] = profile_names
|
||||
|
||||
# Select active profile if exists
|
||||
active_profile = self.settings.get_active_profile_name()
|
||||
if active_profile and active_profile in profile_names:
|
||||
self.profile_var.set(active_profile)
|
||||
elif profile_names:
|
||||
self.profile_var.set(profile_names[0])
|
||||
else:
|
||||
self.profile_var.set("")
|
||||
|
||||
def _on_profile_change(self, event=None):
|
||||
"""Handle profile selection change."""
|
||||
# Just update the selection, don't auto-load
|
||||
pass
|
||||
|
||||
def _load_selected_profile(self):
|
||||
"""Load the selected profile into the form."""
|
||||
profile_name = self.profile_var.get()
|
||||
if not profile_name:
|
||||
messagebox.showwarning("No Profile", "Please select a profile to load.")
|
||||
return
|
||||
|
||||
profile = self.settings.get_profile(profile_name)
|
||||
if not profile:
|
||||
messagebox.showerror("Error", f"Profile '{profile_name}' not found.")
|
||||
return
|
||||
|
||||
# Load profile data into form
|
||||
self.client_id_entry.delete(0, tk.END)
|
||||
self.client_id_entry.insert(0, profile.client_id or '')
|
||||
|
||||
self.client_secret_entry.delete(0, tk.END)
|
||||
self.client_secret_entry.insert(0, profile.client_secret or '')
|
||||
|
||||
self.redirect_uri_entry.delete(0, tk.END)
|
||||
self.redirect_uri_entry.insert(0, profile.redirect_uri or 'http://localhost:9000/oauth/callback')
|
||||
|
||||
self.env_var.set(profile.environment or 'sandbox')
|
||||
|
||||
# Set as active profile
|
||||
self.settings.set_active_profile(profile_name)
|
||||
|
||||
# If profile has tokens, try to connect
|
||||
if profile.access_token and profile.realm_id:
|
||||
try:
|
||||
creds = QBOCredentials(
|
||||
client_id=profile.client_id,
|
||||
client_secret=profile.client_secret,
|
||||
redirect_uri=profile.redirect_uri,
|
||||
environment=profile.environment or 'sandbox',
|
||||
access_token=profile.access_token,
|
||||
refresh_token=profile.refresh_token,
|
||||
realm_id=profile.realm_id,
|
||||
token_expiry=profile.token_expiry
|
||||
)
|
||||
self.settings.save_credentials(creds)
|
||||
self.qbo_client = QBOClient(creds)
|
||||
|
||||
if self.qbo_client.is_authenticated:
|
||||
company_info = self.qbo_client.get_company_info()
|
||||
self.is_connected = True
|
||||
self.connection_type = 'qbo'
|
||||
self.company_name = company_info.get('CompanyName', profile.company_name or 'Unknown')
|
||||
self._update_connection_ui()
|
||||
logger.info(f"[PROFILE] Connected using profile: {profile_name}")
|
||||
messagebox.showinfo("Profile Loaded", f"Connected to {self.company_name}")
|
||||
else:
|
||||
messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' loaded.\nClick 'Connect to QuickBooks' to authenticate.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not auto-connect with profile: {e}")
|
||||
messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' loaded.\nTokens expired. Please reconnect.")
|
||||
else:
|
||||
messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' loaded.\nClick 'Connect to QuickBooks' to authenticate.")
|
||||
|
||||
logger.info(f"[PROFILE] Loaded profile: {profile_name}")
|
||||
|
||||
def _save_profile(self):
|
||||
"""Save current settings to the selected profile."""
|
||||
profile_name = self.profile_var.get()
|
||||
if not profile_name:
|
||||
# No profile selected, prompt for new name
|
||||
self._save_profile_as()
|
||||
return
|
||||
|
||||
# Confirm overwrite
|
||||
if not messagebox.askyesno("Save Profile", f"Overwrite profile '{profile_name}'?"):
|
||||
return
|
||||
|
||||
self._save_profile_with_name(profile_name)
|
||||
|
||||
def _save_profile_as(self):
|
||||
"""Save current settings as a new profile."""
|
||||
# Ask for profile name
|
||||
name = tk.simpledialog.askstring(
|
||||
"Save Profile As",
|
||||
"Enter a name for this profile:",
|
||||
parent=self.root
|
||||
)
|
||||
|
||||
if not name:
|
||||
return
|
||||
|
||||
name = name.strip()
|
||||
if not name:
|
||||
messagebox.showwarning("Invalid Name", "Please enter a valid profile name.")
|
||||
return
|
||||
|
||||
# Check if profile already exists
|
||||
existing = self.settings.get_profile(name)
|
||||
if existing:
|
||||
if not messagebox.askyesno("Profile Exists", f"Profile '{name}' already exists. Overwrite?"):
|
||||
return
|
||||
|
||||
self._save_profile_with_name(name)
|
||||
|
||||
# Update dropdown
|
||||
self._refresh_profiles()
|
||||
self.profile_var.set(name)
|
||||
|
||||
def _save_profile_with_name(self, name: str):
|
||||
"""Save profile with the given name."""
|
||||
# Get current credentials if connected
|
||||
creds = self.settings.get_credentials()
|
||||
|
||||
profile = ConnectionProfile(
|
||||
name=name,
|
||||
client_id=self.client_id_entry.get().strip(),
|
||||
client_secret=self.client_secret_entry.get().strip(),
|
||||
redirect_uri=self.redirect_uri_entry.get().strip(),
|
||||
environment=self.env_var.get(),
|
||||
access_token=creds.access_token if creds else None,
|
||||
refresh_token=creds.refresh_token if creds else None,
|
||||
realm_id=creds.realm_id if creds else None,
|
||||
company_name=self.company_name if self.is_connected else None,
|
||||
token_expiry=creds.token_expiry if creds else None,
|
||||
)
|
||||
|
||||
self.settings.save_profile(profile)
|
||||
self.settings.set_active_profile(name)
|
||||
|
||||
messagebox.showinfo("Profile Saved", f"Profile '{name}' saved successfully.")
|
||||
logger.info(f"[PROFILE] Saved profile: {name}")
|
||||
|
||||
# Refresh the dropdown
|
||||
self._refresh_profiles()
|
||||
|
||||
def _delete_profile(self):
|
||||
"""Delete the selected profile."""
|
||||
profile_name = self.profile_var.get()
|
||||
if not profile_name:
|
||||
messagebox.showwarning("No Profile", "Please select a profile to delete.")
|
||||
return
|
||||
|
||||
if not messagebox.askyesno("Delete Profile", f"Are you sure you want to delete profile '{profile_name}'?"):
|
||||
return
|
||||
|
||||
self.settings.delete_profile(profile_name)
|
||||
|
||||
# Clear active profile if it was the deleted one
|
||||
if self.settings.get_active_profile_name() == profile_name:
|
||||
self.settings.set_active_profile(None)
|
||||
|
||||
# Refresh dropdown
|
||||
self._refresh_profiles()
|
||||
|
||||
# Clear form if needed
|
||||
if not self.profile_var.get():
|
||||
self.client_id_entry.delete(0, tk.END)
|
||||
self.client_secret_entry.delete(0, tk.END)
|
||||
|
||||
messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' deleted.")
|
||||
logger.info(f"[PROFILE] Deleted profile: {profile_name}")
|
||||
|
||||
# =========================================================================
|
||||
# Import Methods
|
||||
# =========================================================================
|
||||
@@ -3484,6 +3824,7 @@ except Exception as e:
|
||||
if entity_type == 'account':
|
||||
balance = e.get('Balance') or e.get('TotalBalance') or '0'
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name') or e.get('FullName', ''),
|
||||
'AccountType': e.get('AccountType', ''),
|
||||
@@ -3494,6 +3835,7 @@ except Exception as e:
|
||||
elif entity_type in ['customer', 'vendor']:
|
||||
balance = e.get('Balance') or e.get('TotalBalance') or '0'
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'DisplayName': e.get('Name') or e.get('FullName', ''),
|
||||
'CompanyName': e.get('CompanyName', ''),
|
||||
@@ -3505,6 +3847,7 @@ except Exception as e:
|
||||
elif entity_type == 'item':
|
||||
price = e.get('UnitPrice') or e.get('SalesPrice') or '0'
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name') or e.get('FullName', ''),
|
||||
'Type': 'Service',
|
||||
@@ -3514,6 +3857,7 @@ except Exception as e:
|
||||
}
|
||||
elif entity_type == 'employee':
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'DisplayName': e.get('Name') or e.get('FullName', ''),
|
||||
'GivenName': e.get('FirstName', ''),
|
||||
@@ -3522,6 +3866,7 @@ except Exception as e:
|
||||
}
|
||||
elif entity_type == 'class':
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name', ''),
|
||||
'FullyQualifiedName': e.get('FullName', ''),
|
||||
@@ -3529,6 +3874,7 @@ except Exception as e:
|
||||
}
|
||||
elif entity_type == 'paymentmethod':
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name', ''),
|
||||
'Type': e.get('PaymentMethodType', ''),
|
||||
@@ -3536,6 +3882,7 @@ except Exception as e:
|
||||
}
|
||||
elif entity_type == 'term':
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name', ''),
|
||||
'DueDays': e.get('DueDays') or e.get('StdDueDays', ''),
|
||||
@@ -3543,6 +3890,7 @@ except Exception as e:
|
||||
}
|
||||
else:
|
||||
entity = {
|
||||
'RawId': raw_id,
|
||||
'Id': clean_id,
|
||||
'Name': e.get('Name') or e.get('FullName', ''),
|
||||
'Active': str(e.get('IsActive', 'true')).lower() == 'true',
|
||||
|
||||
Reference in New Issue
Block a user