Compare commits

..
10 Commits
Author SHA1 Message Date
nngo 335488223d Feb27 2026: Implement backup/restore tool 2026-02-27 10:26:04 -05:00
nngo a29135304d Desktop app update 2026-02-18 12:42:36 -05:00
nngo 0f913ae3c9 Update desktop app 2026-02-18 10:23:36 -05:00
nngo 34d73a6d2c Update app callback server 2026-02-17 17:31:32 -05:00
nngo 2b4a485819 Update for desktop executable file 2026-02-17 14:28:19 -05:00
nngo 03bcff5ff2 Add desktop app 2026-02-17 12:23:22 -05:00
nngo dedb2db026 Update with QB Desktop connection 2026-02-16 12:15:16 -05:00
nngo 2f4326ec15 Update import function 2026-02-13 17:24:16 -05:00
nngo da41509840 Update app's listen port to match production 2026-02-13 16:26:41 -05:00
nngo c8211600ec Remove credentails from tracking 2026-02-13 10:54:28 -05:00
12 changed files with 8388 additions and 373 deletions
+4
View File
@@ -242,3 +242,7 @@ User actions (create, edit, delete) are automatically logged with:
## License ## License
Proprietary - Internal Use Only Proprietary - Internal Use Only
## Build Windows executable app command
pyinstaller .\build_exe.spec --clean --noconfirm
+739 -55
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
{"client_id": "", "client_secret": "", "redirect_uri": "http://localhost:5000/callback", "environment": "sandbox", "access_token": "", "refresh_token": "RT1-89-H0-17797182624khhi6gb525o89u1m5sf", "realm_id": "9341456380310090", "token_expiry": ""}
+26 -26
View File
@@ -2,33 +2,12 @@
"name": "LT Payroll", "name": "LT Payroll",
"data_type": "check", "data_type": "check",
"mappings": [ "mappings": [
{
"excel_column": "Payee",
"qbo_field": "EntityRef.value",
"transform": null,
"default_value": null,
"required": true
},
{ {
"excel_column": "Bank Acct", "excel_column": "Bank Acct",
"qbo_field": "AccountRef.value", "qbo_field": "AccountRef.value",
"transform": null, "transform": null,
"default_value": null, "default_value": null,
"required": true "required": false
},
{
"excel_column": "Date",
"qbo_field": "TxnDate",
"transform": "date",
"default_value": null,
"required": true
},
{
"excel_column": "Amount",
"qbo_field": "Line.Amount",
"transform": "currency",
"default_value": null,
"required": true
}, },
{ {
"excel_column": "Number", "excel_column": "Number",
@@ -37,6 +16,20 @@
"default_value": null, "default_value": null,
"required": false "required": false
}, },
{
"excel_column": "Date",
"qbo_field": "TxnDate",
"transform": null,
"default_value": null,
"required": false
},
{
"excel_column": "Payee",
"qbo_field": "EntityRef.value",
"transform": null,
"default_value": null,
"required": false
},
{ {
"excel_column": "Posting Acct", "excel_column": "Posting Acct",
"qbo_field": "Line.AccountBasedExpenseLineDetail.AccountRef.value", "qbo_field": "Line.AccountBasedExpenseLineDetail.AccountRef.value",
@@ -45,8 +38,15 @@
"required": false "required": false
}, },
{ {
"excel_column": "Period Memo", "excel_column": "Amount",
"qbo_field": "PrivateNote", "qbo_field": "Line.Amount",
"transform": null,
"default_value": null,
"required": false
},
{
"excel_column": "Period",
"qbo_field": "Line.Description",
"transform": null, "transform": null,
"default_value": null, "default_value": null,
"required": false "required": false
@@ -59,6 +59,6 @@
"required": false "required": false
} }
], ],
"created_at": "2026-02-12T12:39:07.304264", "created_at": "2026-02-16T20:16:04.745245",
"updated_at": "2026-02-12T12:39:07.304281" "updated_at": "2026-02-16T20:16:04.745272"
} }
+5407
View File
File diff suppressed because it is too large Load Diff
+368
View File
@@ -0,0 +1,368 @@
"""
QBO Excel Sync - OAuth Callback Server
This is a simple Flask server that handles OAuth callbacks from QuickBooks.
Host this on your own server (e.g., https://yourcompany.com/oauth/callback)
Usage:
1. Deploy this to your server
2. Set up SSL (required for QuickBooks production)
3. Register the callback URL in your QuickBooks app settings
4. Update the desktop app's redirect_uri to match
Environment Variables:
- PORT: Server port (default: 5000)
- SECRET_KEY: Flask secret key for sessions
"""
import os
import logging
from datetime import datetime
from flask import Flask, request, render_template_string, jsonify
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'your-secret-key-change-in-production')
# HTML template for displaying the authorization code
CALLBACK_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QBO Excel Sync - Authorization</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 600px;
width: 100%;
text-align: center;
}
.success-icon {
width: 80px;
height: 80px;
background: #10b981;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
.success-icon svg {
width: 40px;
height: 40px;
fill: white;
}
.error-icon {
width: 80px;
height: 80px;
background: #ef4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
}
.error-icon svg {
width: 40px;
height: 40px;
fill: white;
}
h1 {
color: #1f2937;
font-size: 28px;
margin-bottom: 16px;
}
.subtitle {
color: #6b7280;
font-size: 16px;
margin-bottom: 32px;
}
.code-section {
background: #f3f4f6;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.code-label {
color: #374151;
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.code-box {
background: white;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
color: #1f2937;
word-break: break-all;
margin-bottom: 16px;
max-height: 120px;
overflow-y: auto;
}
.copy-btn {
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.copy-btn:hover {
background: #2563eb;
transform: translateY(-1px);
}
.copy-btn:active {
transform: translateY(0);
}
.copy-btn.copied {
background: #10b981;
}
.realm-info {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
text-align: left;
}
.realm-info strong {
color: #92400e;
}
.instructions {
text-align: left;
color: #4b5563;
font-size: 14px;
line-height: 1.6;
}
.instructions ol {
margin-left: 20px;
margin-top: 12px;
}
.instructions li {
margin-bottom: 8px;
}
.error-message {
background: #fef2f2;
border: 1px solid #ef4444;
border-radius: 8px;
padding: 16px;
color: #991b1b;
text-align: left;
}
.footer {
margin-top: 32px;
color: #9ca3af;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
{% if error %}
<div class="error-icon">
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
</div>
<h1>Authorization Failed</h1>
<div class="error-message">
<strong>Error:</strong> {{ error }}<br>
{% if error_description %}
<strong>Details:</strong> {{ error_description }}
{% endif %}
</div>
{% else %}
<div class="success-icon">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<h1>Authorization Successful!</h1>
<p class="subtitle">Copy the information below and paste it into the QBO Excel Sync app.</p>
<div class="realm-info">
<strong>Company ID (Realm ID):</strong> {{ realm_id }}
</div>
<div class="code-section">
<div class="code-label">Authorization Code</div>
<div class="code-box" id="authCode">{{ code }}</div>
<button class="copy-btn" onclick="copyCode()">
<span id="btnText">Copy Authorization Code</span>
</button>
</div>
<div class="instructions">
<strong>Next Steps:</strong>
<ol>
<li>Go back to the <strong>QBO Excel Sync</strong> desktop application</li>
<li>Click <strong>"Enter Code Manually"</strong> button</li>
<li>Paste the <strong>Authorization Code</strong> and <strong>Company ID</strong></li>
<li>Click <strong>Connect</strong></li>
</ol>
</div>
{% endif %}
<div class="footer">
QBO Excel Sync &copy; {{ year }} | You can close this window after copying the code.
</div>
</div>
<script>
function copyCode() {
const code = document.getElementById('authCode').textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = document.querySelector('.copy-btn');
const btnText = document.getElementById('btnText');
btn.classList.add('copied');
btnText.textContent = '✓ Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btnText.textContent = 'Copy Authorization Code';
}, 2000);
});
}
</script>
</body>
</html>
"""
@app.route('/')
def index():
"""Health check endpoint."""
return jsonify({
'status': 'ok',
'service': 'QBO Excel Sync OAuth Callback Server',
'timestamp': datetime.now().isoformat()
})
@app.route('/oauth/callback')
def oauth_callback():
"""
Handle OAuth callback from QuickBooks.
QuickBooks redirects here with:
- code: Authorization code (on success)
- realmId: Company ID
- state: State parameter we sent
- error: Error code (on failure)
- error_description: Error details (on failure)
"""
# Get parameters
code = request.args.get('code')
realm_id = request.args.get('realmId')
state = request.args.get('state')
error = request.args.get('error')
error_description = request.args.get('error_description')
# Log the callback
logger.info(f"OAuth callback received - realm_id: {realm_id}, state: {state}, error: {error}")
if error:
logger.error(f"OAuth error: {error} - {error_description}")
return render_template_string(
CALLBACK_TEMPLATE,
error=error,
error_description=error_description,
year=datetime.now().year
)
if not code:
logger.error("No authorization code received")
return render_template_string(
CALLBACK_TEMPLATE,
error="No authorization code received",
error_description="The authorization server did not return a code.",
year=datetime.now().year
)
# Success - display the code to the user
logger.info(f"Authorization successful for realm {realm_id}")
return render_template_string(
CALLBACK_TEMPLATE,
code=code,
realm_id=realm_id,
state=state,
error=None,
year=datetime.now().year
)
@app.route('/oauth/callback/json')
def oauth_callback_json():
"""
JSON endpoint for programmatic access.
Can be used if you want to implement automatic code relay.
"""
code = request.args.get('code')
realm_id = request.args.get('realmId')
state = request.args.get('state')
error = request.args.get('error')
error_description = request.args.get('error_description')
if error:
return jsonify({
'success': False,
'error': error,
'error_description': error_description
}), 400
return jsonify({
'success': True,
'code': code,
'realm_id': realm_id,
'state': state
})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
print(f"""
QBO Excel Sync - OAuth Callback Server
Server running on port {port}
Callback URL: http://localhost:{port}/oauth/callback
For production, deploy with HTTPS (required by QuickBooks)
Example: https://yourcompany.com/oauth/callback
""")
app.run(host='0.0.0.0', port=port, debug=debug)
+12
View File
@@ -0,0 +1,12 @@
# QBO Excel Sync Desktop App Requirements
# Install with: pip install -r requirements_desktop.txt
# Core dependencies (same as web app)
requests>=2.31.0
openpyxl>=3.1.2
# Windows-specific (for QuickBooks Desktop)
pywin32>=306; sys_platform == 'win32'
# Note: tkinter is included with Python on Windows
# On Linux, install with: sudo apt-get install python3-tk
+1
View File
@@ -2,3 +2,4 @@ Flask>=2.3.0
Flask-Session>=0.5.0 Flask-Session>=0.5.0
openpyxl>=3.1.0 openpyxl>=3.1.0
requests>=2.31.0 requests>=2.31.0
pywin32>=306; sys_platform == 'win32'
+758
View File
@@ -0,0 +1,758 @@
"""
QuickBooks Desktop API Client
Handles COM-based communication with QuickBooks Desktop via QBXMLRP2.
Requires pywin32 and QuickBooks Desktop SDK installed on Windows.
"""
import os
import sys
import logging
import xml.etree.ElementTree as ET
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
from datetime import datetime
logger = logging.getLogger(__name__)
# Check if we're on Windows
IS_WINDOWS = sys.platform == 'win32'
if IS_WINDOWS:
try:
import pythoncom
from win32com.client import Dispatch
HAS_WIN32COM = True
except ImportError:
HAS_WIN32COM = False
logger.warning("pywin32 not installed. QuickBooks Desktop integration unavailable.")
else:
HAS_WIN32COM = False
logger.info("Not running on Windows. QuickBooks Desktop integration unavailable.")
# Connection type constants (from QBXMLRP2 SDK)
class QBConnectionType:
localQBD = 1 # Local QuickBooks Desktop
remoteQBD = 2 # Remote QuickBooks Desktop
localQBDLaunchUI = 3 # Launch QuickBooks if not running
# File mode constants
class QBFileMode:
qbFileOpenDoNotCare = 0 # Open any file
qbFileOpenSingleUser = 1 # Open in single-user mode
qbFileOpenMultiUser = 2 # Open in multi-user mode
@dataclass
class QBDCredentials:
"""QuickBooks Desktop connection settings."""
application_name: str = "QBO Excel Sync"
application_id: str = ""
company_file: str = "" # Empty string = currently open company file
connection_type: int = QBConnectionType.localQBD
file_mode: int = QBFileMode.qbFileOpenDoNotCare
class QBDesktopError(Exception):
"""QuickBooks Desktop specific error."""
def __init__(self, message: str, code: str = None, detail: str = None):
self.message = message
self.code = code
self.detail = detail
super().__init__(self.message)
class QBDesktopClient:
"""QuickBooks Desktop API Client using COM interface."""
QBXML_VERSION = "13.0"
COUNTRY = "US"
def __init__(self, credentials: QBDCredentials = None):
self.credentials = credentials or QBDCredentials()
self.request_processor = None
self.session_ticket = None
self._connected = False
self._company_name = None
@property
def is_available(self) -> bool:
"""Check if QBD integration is available on this system."""
return IS_WINDOWS and HAS_WIN32COM
@property
def is_connected(self) -> bool:
"""Check if connected to QuickBooks Desktop."""
return self._connected and self.session_ticket is not None
@property
def company_name(self) -> Optional[str]:
"""Get the connected company name."""
return self._company_name
def connect(self) -> bool:
"""Connect to QuickBooks Desktop."""
if not self.is_available:
raise QBDesktopError("QuickBooks Desktop integration not available on this system")
try:
# Initialize COM for this thread
# CRITICAL: Flask runs in multi-threaded mode, so we need proper COM initialization
# Use CoInitializeEx with COINIT_APARTMENTTHREADED for COM automation objects
try:
# First, try to uninitialize any previous COM state
try:
pythoncom.CoUninitialize()
except:
pass
# Initialize with single-threaded apartment (STA) - required for UI automation objects
pythoncom.CoInitializeEx(pythoncom.COINIT_APARTMENTTHREADED)
logger.info("COM initialized with COINIT_APARTMENTTHREADED")
except pythoncom.com_error as e:
# If already initialized, that's OK
if e.hresult == -2147417850: # RPC_E_CHANGED_MODE
logger.info("COM already initialized in different mode, continuing...")
else:
logger.warning(f"COM initialization warning: {e}")
except Exception as e:
logger.warning(f"COM initialization warning: {e}")
# Create the request processor using Dispatch (not DispatchEx)
# Dispatch works better with apartment-threaded COM objects
try:
self.request_processor = Dispatch("QBXMLRP2.RequestProcessor")
logger.info("QBXMLRP2.RequestProcessor created successfully")
except Exception as e:
logger.error(f"Failed to create RequestProcessor: {e}")
raise QBDesktopError(
f"Cannot create QuickBooks RequestProcessor. "
f"Make sure QuickBooks Desktop SDK is installed. Error: {str(e)}",
code="SDK_NOT_FOUND"
)
# Open connection
try:
self.request_processor.OpenConnection2(
self.credentials.application_id,
self.credentials.application_name,
self.credentials.connection_type
)
logger.info("OpenConnection2 successful")
except Exception as e:
logger.error(f"OpenConnection2 failed: {e}")
raise QBDesktopError(
f"Failed to open connection to QuickBooks. Error: {str(e)}",
code="CONNECTION_FAILED"
)
# Begin session
# NOTE: This may trigger an authorization dialog in QuickBooks Desktop
# The user must click "Yes, always" in QuickBooks for this to succeed
logger.info("Attempting BeginSession - check QuickBooks for authorization dialog...")
try:
self.session_ticket = self.request_processor.BeginSession(
self.credentials.company_file,
self.credentials.file_mode
)
logger.info(f"BeginSession successful, ticket: {self.session_ticket[:20] if self.session_ticket else 'None'}...")
except Exception as e:
error_msg = str(e)
logger.error(f"BeginSession failed: {e}")
# Try to close the connection
try:
self.request_processor.CloseConnection()
except:
pass
# Parse common session errors
if "80040408" in error_msg:
raise QBDesktopError(
"QuickBooks is not running or no company file is open. "
"Please open QuickBooks Desktop with a company file first.",
code="QB_NOT_RUNNING"
)
elif "80040416" in error_msg or "80040417" in error_msg:
raise QBDesktopError(
"Access denied. When QuickBooks prompts you to allow access, "
"please click 'Yes, always allow access'. You may need to "
"run QuickBooks as Administrator.",
code="ACCESS_DENIED"
)
elif "80040422" in error_msg:
raise QBDesktopError(
"No company file is open in QuickBooks. "
"Please open a company file and try again.",
code="NO_COMPANY_FILE"
)
elif "80040423" in error_msg:
raise QBDesktopError(
"The company file is in use in single-user mode. "
"Switch to multi-user mode or close other connections.",
code="SINGLE_USER_MODE"
)
else:
raise QBDesktopError(
f"Failed to start QuickBooks session: {error_msg}",
code="SESSION_FAILED"
)
self._connected = True
# Get company info
self._fetch_company_info()
logger.info(f"Connected to QuickBooks Desktop: {self._company_name}")
return True
except QBDesktopError:
self._connected = False
raise
except Exception as e:
self._connected = False
error_msg = str(e)
logger.error(f"Unexpected error connecting to QBD: {error_msg}")
# Parse common errors
if "80040408" in error_msg:
raise QBDesktopError(
"QuickBooks is not running. Please open QuickBooks Desktop first.",
code="QB_NOT_RUNNING"
)
elif "80040416" in error_msg:
raise QBDesktopError(
"Access denied. Please authorize this application in QuickBooks.",
code="ACCESS_DENIED"
)
elif "80040422" in error_msg:
raise QBDesktopError(
"No company file is open in QuickBooks.",
code="NO_COMPANY_FILE"
)
else:
raise QBDesktopError(f"Failed to connect to QuickBooks Desktop: {error_msg}")
def disconnect(self) -> bool:
"""Disconnect from QuickBooks Desktop."""
try:
if self.session_ticket:
self.request_processor.EndSession(self.session_ticket)
self.session_ticket = None
if self.request_processor:
self.request_processor.CloseConnection()
self.request_processor = None
self._connected = False
self._company_name = None
# Uninitialize COM
pythoncom.CoUninitialize()
logger.info("Disconnected from QuickBooks Desktop")
return True
except Exception as e:
logger.error(f"Error disconnecting from QuickBooks Desktop: {e}")
return False
def _fetch_company_info(self):
"""Fetch company information from QuickBooks."""
try:
response = self._send_request("CompanyQuery", {})
if response:
company = response.get("CompanyRet", {})
self._company_name = company.get("CompanyName", "Unknown Company")
except Exception as e:
logger.warning(f"Could not fetch company info: {e}")
self._company_name = "Connected"
def _build_qbxml_request(self, request_type: str, request_data: Dict = None) -> str:
"""Build a QBXML request string."""
request_data = request_data or {}
# Build the request XML
xml_parts = [
'<?xml version="1.0" encoding="utf-8"?>',
'<?qbxml version="{}">'.format(self.QBXML_VERSION),
'<QBXML>',
'<QBXMLMsgsRq onError="stopOnError">'
]
# Add the specific request
xml_parts.append(f'<{request_type}Rq>')
if request_type.endswith("Query"):
# Query request
xml_parts.append(self._dict_to_xml(request_data))
elif request_type.endswith("Add"):
# Add request
base_type = request_type[:-3] # Remove "Add"
xml_parts.append(f'<{base_type}Add>')
xml_parts.append(self._dict_to_xml(request_data))
xml_parts.append(f'</{base_type}Add>')
elif request_type.endswith("Mod"):
# Modify request
base_type = request_type[:-3] # Remove "Mod"
xml_parts.append(f'<{base_type}Mod>')
xml_parts.append(self._dict_to_xml(request_data))
xml_parts.append(f'</{base_type}Mod>')
else:
xml_parts.append(self._dict_to_xml(request_data))
xml_parts.append(f'</{request_type}Rq>')
xml_parts.append('</QBXMLMsgsRq>')
xml_parts.append('</QBXML>')
return ''.join(xml_parts)
def _dict_to_xml(self, data: Dict, indent: int = 0) -> str:
"""Convert a dictionary to XML string."""
xml_parts = []
for key, value in data.items():
if value is None:
continue
elif isinstance(value, dict):
xml_parts.append(f'<{key}>')
xml_parts.append(self._dict_to_xml(value, indent + 1))
xml_parts.append(f'</{key}>')
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
xml_parts.append(f'<{key}>')
xml_parts.append(self._dict_to_xml(item, indent + 1))
xml_parts.append(f'</{key}>')
else:
xml_parts.append(f'<{key}>{self._escape_xml(str(item))}</{key}>')
else:
xml_parts.append(f'<{key}>{self._escape_xml(str(value))}</{key}>')
return ''.join(xml_parts)
def _escape_xml(self, text: str) -> str:
"""Escape special XML characters."""
return (text
.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
.replace("'", '&apos;'))
def _send_request(self, request_type: str, request_data: Dict = None) -> Dict:
"""Send a request to QuickBooks and parse the response."""
if not self.is_connected:
raise QBDesktopError("Not connected to QuickBooks Desktop")
request_xml = self._build_qbxml_request(request_type, request_data)
logger.debug(f"Sending QBXML request: {request_type}")
logger.debug(f"Request XML: {request_xml[:500]}...")
try:
response_xml = self.request_processor.ProcessRequest(
self.session_ticket,
request_xml
)
logger.debug(f"Response XML: {response_xml[:500]}...")
return self._parse_response(response_xml, request_type)
except Exception as e:
raise QBDesktopError(f"Request failed: {str(e)}")
def _parse_response(self, response_xml: str, request_type: str) -> Dict:
"""Parse QBXML response into a dictionary."""
try:
root = ET.fromstring(response_xml)
# Find the response element
msgs_rs = root.find(".//QBXMLMsgsRs")
if msgs_rs is None:
raise QBDesktopError("Invalid QBXML response")
# Get the specific response
response_tag = f"{request_type}Rs"
response_elem = msgs_rs.find(response_tag)
if response_elem is None:
raise QBDesktopError(f"No response found for {request_type}")
# Check status
status_code = response_elem.get("statusCode", "0")
status_message = response_elem.get("statusMessage", "")
if status_code != "0":
raise QBDesktopError(
f"QuickBooks error: {status_message}",
code=status_code,
detail=status_message
)
# Parse response data
return self._xml_to_dict(response_elem)
except ET.ParseError as e:
raise QBDesktopError(f"Failed to parse response XML: {e}")
def _xml_to_dict(self, element: ET.Element) -> Dict:
"""Convert XML element to dictionary."""
result = {}
for child in element:
if len(child) > 0:
# Has children - recurse
child_dict = self._xml_to_dict(child)
if child.tag in result:
# Multiple elements with same tag - make a list
if not isinstance(result[child.tag], list):
result[child.tag] = [result[child.tag]]
result[child.tag].append(child_dict)
else:
result[child.tag] = child_dict
else:
# Leaf node
if child.tag in result:
if not isinstance(result[child.tag], list):
result[child.tag] = [result[child.tag]]
result[child.tag].append(child.text)
else:
result[child.tag] = child.text
return result
# ==========================================================================
# Query Methods
# ==========================================================================
def get_customers(self, max_results: int = 1000) -> List[Dict]:
"""Get all customers."""
response = self._send_request("CustomerQuery", {
"MaxReturned": str(max_results)
})
customers = response.get("CustomerRet", [])
if isinstance(customers, dict):
customers = [customers]
return customers
def get_vendors(self, max_results: int = 1000) -> List[Dict]:
"""Get all vendors."""
response = self._send_request("VendorQuery", {
"MaxReturned": str(max_results)
})
vendors = response.get("VendorRet", [])
if isinstance(vendors, dict):
vendors = [vendors]
return vendors
def get_accounts(self, max_results: int = 1000) -> List[Dict]:
"""Get all accounts."""
response = self._send_request("AccountQuery", {
"MaxReturned": str(max_results)
})
accounts = response.get("AccountRet", [])
if isinstance(accounts, dict):
accounts = [accounts]
return accounts
def get_items(self, max_results: int = 1000) -> List[Dict]:
"""Get all items/products."""
response = self._send_request("ItemQuery", {
"MaxReturned": str(max_results)
})
# Items can be different types
items = []
for item_type in ["ItemServiceRet", "ItemInventoryRet", "ItemNonInventoryRet",
"ItemOtherChargeRet", "ItemGroupRet", "ItemDiscountRet"]:
type_items = response.get(item_type, [])
if isinstance(type_items, dict):
type_items = [type_items]
items.extend(type_items)
return items
def get_employees(self, max_results: int = 1000) -> List[Dict]:
"""Get all employees."""
response = self._send_request("EmployeeQuery", {
"MaxReturned": str(max_results)
})
employees = response.get("EmployeeRet", [])
if isinstance(employees, dict):
employees = [employees]
return employees
# ==========================================================================
# Create Methods
# ==========================================================================
def create_check(self, data: Dict) -> Dict:
"""Create a check in QuickBooks Desktop."""
# Build the check request
check_data = {
"AccountRef": data.get("AccountRef"),
"PayeeEntityRef": data.get("EntityRef"),
"TxnDate": data.get("TxnDate"),
"RefNumber": data.get("DocNumber"),
"Memo": data.get("PrivateNote", ""),
}
# Add expense lines
if "Line" in data:
expense_lines = []
for line in data["Line"]:
if line.get("DetailType") == "AccountBasedExpenseLineDetail":
detail = line.get("AccountBasedExpenseLineDetail", {})
expense_lines.append({
"AccountRef": detail.get("AccountRef"),
"Amount": line.get("Amount"),
"Memo": line.get("Description", "")
})
if expense_lines:
check_data["ExpenseLineAdd"] = expense_lines
response = self._send_request("CheckAdd", check_data)
return response.get("CheckRet", {})
def create_invoice(self, data: Dict) -> Dict:
"""Create an invoice in QuickBooks Desktop."""
invoice_data = {
"CustomerRef": data.get("CustomerRef"),
"TxnDate": data.get("TxnDate"),
"RefNumber": data.get("DocNumber"),
"DueDate": data.get("DueDate"),
"Memo": data.get("PrivateNote", ""),
}
# Add invoice lines
if "Line" in data:
invoice_lines = []
for line in data["Line"]:
if line.get("DetailType") == "SalesItemLineDetail":
detail = line.get("SalesItemLineDetail", {})
invoice_lines.append({
"ItemRef": detail.get("ItemRef"),
"Quantity": detail.get("Qty"),
"Rate": detail.get("UnitPrice"),
"Amount": line.get("Amount"),
"Desc": line.get("Description", "")
})
if invoice_lines:
invoice_data["InvoiceLineAdd"] = invoice_lines
response = self._send_request("InvoiceAdd", invoice_data)
return response.get("InvoiceRet", {})
def create_bill(self, data: Dict) -> Dict:
"""Create a bill in QuickBooks Desktop."""
bill_data = {
"VendorRef": data.get("VendorRef"),
"TxnDate": data.get("TxnDate"),
"RefNumber": data.get("DocNumber"),
"DueDate": data.get("DueDate"),
"Memo": data.get("PrivateNote", ""),
}
# Add expense lines
if "Line" in data:
expense_lines = []
for line in data["Line"]:
if line.get("DetailType") == "AccountBasedExpenseLineDetail":
detail = line.get("AccountBasedExpenseLineDetail", {})
expense_lines.append({
"AccountRef": detail.get("AccountRef"),
"Amount": line.get("Amount"),
"Memo": line.get("Description", "")
})
if expense_lines:
bill_data["ExpenseLineAdd"] = expense_lines
response = self._send_request("BillAdd", bill_data)
return response.get("BillRet", {})
def create_customer(self, data: Dict) -> Dict:
"""Create a customer in QuickBooks Desktop."""
customer_data = {
"Name": data.get("DisplayName"),
"CompanyName": data.get("CompanyName"),
"FirstName": data.get("GivenName"),
"LastName": data.get("FamilyName"),
"Phone": data.get("PrimaryPhone", {}).get("FreeFormNumber") if isinstance(data.get("PrimaryPhone"), dict) else None,
"Email": data.get("PrimaryEmailAddr", {}).get("Address") if isinstance(data.get("PrimaryEmailAddr"), dict) else None,
}
# Add billing address
if "BillAddr" in data:
addr = data["BillAddr"]
customer_data["BillAddress"] = {
"Addr1": addr.get("Line1"),
"City": addr.get("City"),
"State": addr.get("CountrySubDivisionCode"),
"PostalCode": addr.get("PostalCode"),
"Country": addr.get("Country")
}
response = self._send_request("CustomerAdd", customer_data)
return response.get("CustomerRet", {})
def create_vendor(self, data: Dict) -> Dict:
"""Create a vendor in QuickBooks Desktop."""
vendor_data = {
"Name": data.get("DisplayName"),
"CompanyName": data.get("CompanyName"),
"FirstName": data.get("GivenName"),
"LastName": data.get("FamilyName"),
"Phone": data.get("PrimaryPhone", {}).get("FreeFormNumber") if isinstance(data.get("PrimaryPhone"), dict) else None,
"Email": data.get("PrimaryEmailAddr", {}).get("Address") if isinstance(data.get("PrimaryEmailAddr"), dict) else None,
}
# Add address
if "BillAddr" in data:
addr = data["BillAddr"]
vendor_data["VendorAddress"] = {
"Addr1": addr.get("Line1"),
"City": addr.get("City"),
"State": addr.get("CountrySubDivisionCode"),
"PostalCode": addr.get("PostalCode"),
"Country": addr.get("Country")
}
response = self._send_request("VendorAdd", vendor_data)
return response.get("VendorRet", {})
def create_account(self, data: Dict) -> Dict:
"""Create an account in QuickBooks Desktop."""
# Map QBO account types to QBD account types
account_type_map = {
"Bank": "Bank",
"Accounts Receivable": "AccountsReceivable",
"Other Current Asset": "OtherCurrentAsset",
"Fixed Asset": "FixedAsset",
"Other Asset": "OtherAsset",
"Accounts Payable": "AccountsPayable",
"Credit Card": "CreditCard",
"Other Current Liability": "OtherCurrentLiability",
"Long Term Liability": "LongTermLiability",
"Equity": "Equity",
"Income": "Income",
"Cost of Goods Sold": "CostOfGoodsSold",
"Expense": "Expense",
"Other Income": "OtherIncome",
"Other Expense": "OtherExpense"
}
qbo_type = data.get("AccountType", "Expense")
qbd_type = account_type_map.get(qbo_type, "Expense")
account_data = {
"Name": data.get("Name"),
"AccountType": qbd_type,
"Desc": data.get("Description"),
"AccountNumber": data.get("AcctNum"),
}
response = self._send_request("AccountAdd", account_data)
return response.get("AccountRet", {})
def __enter__(self):
"""Context manager entry."""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.disconnect()
return False
def check_qbd_availability() -> Dict[str, Any]:
"""Check if QuickBooks Desktop integration is available."""
result = {
"available": False,
"is_windows": IS_WINDOWS,
"has_pywin32": HAS_WIN32COM,
"qb_running": False,
"sdk_installed": False,
"error": None,
"details": ""
}
if not IS_WINDOWS:
result["error"] = "QuickBooks Desktop is only available on Windows"
result["details"] = "This feature requires Windows operating system"
return result
if not HAS_WIN32COM:
result["error"] = "pywin32 is not installed"
result["details"] = "Run: pip install pywin32"
return result
# Try to create the RequestProcessor to check if SDK is installed
rp = None
try:
pythoncom.CoInitialize()
# Try to create RequestProcessor
try:
rp = Dispatch("QBXMLRP2.RequestProcessor")
result["sdk_installed"] = True
result["available"] = True
logger.info("QBXMLRP2.RequestProcessor created successfully in availability check")
except Exception as e:
error_msg = str(e)
logger.warning(f"Failed to create RequestProcessor: {error_msg}")
if "80040154" in error_msg or "Class not registered" in error_msg:
result["error"] = "QuickBooks Desktop SDK not installed"
result["details"] = (
"The QBXMLRP2 component is not registered. "
"This usually means QuickBooks Desktop is not installed, "
"or the SDK components are missing. "
"Try repairing your QuickBooks installation."
)
else:
result["error"] = f"Cannot create QuickBooks interface: {error_msg}"
return result
# Try to open a connection to see if QB is running
try:
rp.OpenConnection2("", "QBD Availability Check", QBConnectionType.localQBD)
result["qb_running"] = True
logger.info("OpenConnection2 successful - QuickBooks is running")
# Close the connection
try:
rp.CloseConnection()
except:
pass
except Exception as e:
error_msg = str(e)
logger.info(f"OpenConnection2 check result: {error_msg}")
# QB SDK is available but QB might not be running
if "80040408" in error_msg:
result["qb_running"] = False
result["error"] = "QuickBooks Desktop is not running"
result["details"] = "Please open QuickBooks Desktop with a company file"
elif "80040402" in error_msg:
# This can happen when QB is running but locked
result["qb_running"] = True
result["error"] = "QuickBooks is busy or locked"
result["details"] = "QuickBooks may be showing a dialog. Please check QuickBooks."
else:
# Unknown error but SDK is there
result["qb_running"] = False
result["error"] = f"QuickBooks not accessible: {error_msg}"
except Exception as e:
error_msg = str(e)
logger.error(f"Unexpected error in availability check: {error_msg}")
result["error"] = f"Error checking QuickBooks: {error_msg}"
finally:
try:
pythoncom.CoUninitialize()
except:
pass
return result
+141 -2
View File
@@ -29,6 +29,23 @@ class QBOCredentials:
token_expiry: Optional[str] = None token_expiry: Optional[str] = None
@dataclass
class ConnectionProfile:
"""Connection profile for QuickBooks Online."""
name: str
client_id: str = ""
client_secret: str = ""
redirect_uri: str = "http://localhost:9000/oauth/callback"
environment: str = "sandbox" # 'sandbox' or 'production'
access_token: Optional[str] = None
refresh_token: Optional[str] = None
realm_id: Optional[str] = None
company_name: Optional[str] = None
token_expiry: Optional[str] = None
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
@dataclass @dataclass
class FieldMapping: class FieldMapping:
"""Single field mapping configuration.""" """Single field mapping configuration."""
@@ -60,6 +77,8 @@ class ImportSettings:
date_format: str = "%Y-%m-%d" date_format: str = "%Y-%m-%d"
decimal_separator: str = "." decimal_separator: str = "."
thousand_separator: str = "," thousand_separator: str = ","
skip_empty_rows: bool = True
stop_on_error: bool = False
class Settings: class Settings:
@@ -87,8 +106,36 @@ class Settings:
def _get_config_dir(self) -> Path: def _get_config_dir(self) -> Path:
"""Get platform-specific config directory.""" """Get platform-specific config directory."""
# For web app, use a local directory import sys
# Use user's AppData folder for persistent storage
# This works correctly for both normal Python and PyInstaller executables
if sys.platform == 'win32':
# Windows: Use AppData/Local
app_data = os.environ.get('LOCALAPPDATA')
if app_data:
return Path(app_data) / self.APP_NAME
# Fallback to APPDATA if LOCALAPPDATA not available
app_data = os.environ.get('APPDATA')
if app_data:
return Path(app_data) / self.APP_NAME
elif sys.platform == 'darwin':
# macOS: Use ~/Library/Application Support
home = Path.home()
return home / "Library" / "Application Support" / self.APP_NAME
else:
# Linux/Unix: Use ~/.config
home = Path.home()
return home / ".config" / self.APP_NAME.lower()
# Final fallback: Use a 'data' folder relative to executable/script
if getattr(sys, 'frozen', False):
# Running as PyInstaller executable
base = Path(sys.executable).parent
else:
# Running as script
base = Path(__file__).parent.parent.parent base = Path(__file__).parent.parent.parent
return base / "data" return base / "data"
def _load_config(self) -> Dict[str, Any]: def _load_config(self) -> Dict[str, Any]:
@@ -136,6 +183,21 @@ class Settings:
self.save() self.save()
logger.info("Import settings updated") logger.info("Import settings updated")
def get_import_settings(self) -> ImportSettings:
"""Get import settings (method version for compatibility)."""
try:
settings_data = self._config.get("import_settings", {})
return ImportSettings(**settings_data)
except Exception as e:
logger.warning(f"Error loading import settings: {e}")
return ImportSettings()
def save_import_settings(self, settings: ImportSettings):
"""Save import settings (method version for compatibility)."""
self._config["import_settings"] = asdict(settings)
self.save()
logger.info("Import settings saved")
@property @property
def recent_files(self) -> List[str]: def recent_files(self) -> List[str]:
"""Get list of recently opened files.""" """Get list of recently opened files."""
@@ -186,7 +248,11 @@ class Settings:
try: try:
with open(self.credentials_file, 'w') as f: with open(self.credentials_file, 'w') as f:
json.dump(asdict(credentials), f) json.dump(asdict(credentials), f)
os.chmod(self.credentials_file, 0o600) # Restrict permissions # Restrict file permissions (Unix only, skip on Windows)
try:
os.chmod(self.credentials_file, 0o600)
except (OSError, AttributeError):
pass # chmod not supported on Windows
logger.info("Credentials saved successfully") logger.info("Credentials saved successfully")
except Exception as e: except Exception as e:
logger.error(f"Failed to save credentials: {e}") logger.error(f"Failed to save credentials: {e}")
@@ -198,6 +264,79 @@ class Settings:
self.credentials_file.unlink() self.credentials_file.unlink()
logger.info("Credentials cleared") logger.info("Credentials cleared")
# Connection Profile management
def get_profiles(self) -> List[ConnectionProfile]:
"""Get all connection profiles."""
profiles = []
profiles_file = self.config_dir / "profiles.json"
if profiles_file.exists():
try:
with open(profiles_file, 'r') as f:
profiles_data = json.load(f)
for p in profiles_data:
profiles.append(ConnectionProfile(**p))
except Exception as e:
logger.warning(f"Failed to load profiles: {e}")
return sorted(profiles, key=lambda p: p.name)
def get_profile(self, name: str) -> Optional[ConnectionProfile]:
"""Get a specific profile by name."""
profiles = self.get_profiles()
for p in profiles:
if p.name == name:
return p
return None
def save_profile(self, profile: ConnectionProfile):
"""Save a connection profile."""
profile.updated_at = datetime.now().isoformat()
profiles = self.get_profiles()
# Update existing or add new
found = False
for i, p in enumerate(profiles):
if p.name == profile.name:
profiles[i] = profile
found = True
break
if not found:
profiles.append(profile)
# Save to file
profiles_file = self.config_dir / "profiles.json"
try:
with open(profiles_file, 'w') as f:
json.dump([asdict(p) for p in profiles], f, indent=2)
logger.info(f"Profile saved: {profile.name}")
except Exception as e:
logger.error(f"Failed to save profile: {e}")
def delete_profile(self, name: str):
"""Delete a connection profile."""
profiles = self.get_profiles()
profiles = [p for p in profiles if p.name != name]
profiles_file = self.config_dir / "profiles.json"
try:
with open(profiles_file, 'w') as f:
json.dump([asdict(p) for p in profiles], f, indent=2)
logger.info(f"Profile deleted: {name}")
except Exception as e:
logger.error(f"Failed to delete profile: {e}")
def get_active_profile_name(self) -> Optional[str]:
"""Get the name of the currently active profile."""
return self._config.get("active_profile", None)
def set_active_profile(self, name: Optional[str]):
"""Set the active profile name."""
self._config["active_profile"] = name
self.save()
logger.info(f"Active profile set to: {name}")
# Template management # Template management
def get_templates(self, data_type: Optional[str] = None) -> List[MappingTemplate]: def get_templates(self, data_type: Optional[str] = None) -> List[MappingTemplate]:
"""Get all mapping templates, optionally filtered by data type.""" """Get all mapping templates, optionally filtered by data type."""
+583 -128
View File
@@ -6,10 +6,54 @@
<div class="connection-page"> <div class="connection-page">
<div class="page-header"> <div class="page-header">
<h2>QuickBooks Connection</h2> <h2>QuickBooks Connection</h2>
<p>Connect to your QuickBooks Online account to start importing data.</p> <p>Connect to QuickBooks Online or QuickBooks Desktop to start importing data.</p>
</div> </div>
<div class="connection-layout"> <div class="connection-layout">
<!-- Connection Type Selector -->
<section class="card">
<div class="card-header">
<h3>Connection Type</h3>
</div>
<div class="card-body">
<div class="connection-type-selector">
<label class="connection-type-option {% if connection_type != 'qbd' %}active{% endif %}">
<input type="radio" name="connection_type" value="qbo" {% if connection_type != 'qbd' %}checked{% endif %}>
<div class="option-content">
<div class="option-icon qbo-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12l2 2 4-4"/>
</svg>
</div>
<div class="option-text">
<strong>QuickBooks Online</strong>
<span>Cloud-based, OAuth authentication</span>
</div>
</div>
</label>
<label class="connection-type-option {% if connection_type == 'qbd' %}active{% endif %} {% if not qbd_available %}disabled{% endif %}">
<input type="radio" name="connection_type" value="qbd" {% if connection_type == 'qbd' %}checked{% endif %} {% if not qbd_available %}disabled{% endif %}>
<div class="option-content">
<div class="option-icon qbd-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
<div class="option-text">
<strong>QuickBooks Desktop</strong>
<span>{% if qbd_available %}Local installation, direct COM connection{% else %}Windows only, requires QuickBooks Desktop{% endif %}</span>
</div>
</div>
</label>
</div>
</div>
</section>
<!-- QBO Section -->
<div id="qbo-section" class="{% if connection_type == 'qbd' %}hidden{% endif %}">
<!-- Credentials Section --> <!-- Credentials Section -->
<section class="card collapsible"> <section class="card collapsible">
<div class="card-header" onclick="toggleSection(this)"> <div class="card-header" onclick="toggleSection(this)">
@@ -64,20 +108,15 @@
</div> </div>
</section> </section>
<!-- Connection Status Section --> <!-- QBO Connection Status Section -->
<section class="card collapsible"> <section class="card">
<div class="card-header" onclick="toggleSection(this)"> <div class="card-header">
<h3>Connection Status</h3> <h3>QuickBooks Online Status</h3>
<span class="collapse-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</span>
</div> </div>
<div class="card-body collapsible-content"> <div class="card-body">
<div class="connection-status-display {% if is_connected %}connected{% else %}disconnected{% endif %}"> <div class="connection-status-display {% if is_connected and connection_type != 'qbd' %}connected{% else %}disconnected{% endif %}">
<div class="status-icon"> <div class="status-icon">
{% if is_connected %} {% if is_connected and connection_type != 'qbd' %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/> <polyline points="22 4 12 14.01 9 11.01"/>
@@ -91,23 +130,23 @@
{% endif %} {% endif %}
</div> </div>
<div class="status-details"> <div class="status-details">
<h4>{% if is_connected %}Connected{% else %}Not Connected{% endif %}</h4> <h4>{% if is_connected and connection_type != 'qbd' %}Connected{% else %}Not Connected{% endif %}</h4>
{% if is_connected and company_name %} {% if is_connected and company_name and connection_type != 'qbd' %}
<p class="company-name">{{ company_name }}</p> <p class="company-name">{{ company_name }}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="connection-actions"> <div class="connection-actions">
{% if is_connected %} {% if is_connected and connection_type != 'qbd' %}
<button type="button" id="test-connection" class="btn btn-secondary"> <button type="button" id="test-connection-qbo" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/> <polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/> <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg> </svg>
Test Connection Test Connection
</button> </button>
<button type="button" id="disconnect" class="btn btn-danger"> <button type="button" id="disconnect-qbo" class="btn btn-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/> <path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
<line x1="12" y1="2" x2="12" y2="12"/> <line x1="12" y1="2" x2="12" y2="12"/>
@@ -120,7 +159,7 @@
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/> <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg> </svg>
Connect to QuickBooks Connect to QuickBooks Online
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -175,139 +214,440 @@
</form> </form>
</div> </div>
</section> </section>
</div>
<!-- Help Section --> <!-- QBD Section -->
<section class="card help-card collapsible collapsed"> <div id="qbd-section" class="{% if connection_type != 'qbd' %}hidden{% endif %}">
<div class="card-header" onclick="toggleSection(this)"> <section class="card">
<h3>Setup Instructions</h3> <div class="card-header">
<span class="collapse-icon"> <h3>QuickBooks Desktop Connection</h3>
</div>
<div class="card-body">
{% if not qbd_available %}
<div class="alert alert-warning">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/> <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg> </svg>
<div>
<strong>QuickBooks Desktop not available</strong>
<p>QuickBooks Desktop integration requires:</p>
<ul>
<li>Windows operating system</li>
<li>QuickBooks Desktop installed and running</li>
<li>pywin32 Python package (<code>pip install pywin32</code>)</li>
</ul>
</div>
</div>
{% else %}
<div class="qbd-info">
<div class="info-item">
<span class="info-label">Status:</span>
<span class="info-value" id="qbd-status">
{% if qbd_running %}
<span class="status-badge success">QuickBooks is running</span>
{% else %}
<span class="status-badge warning">QuickBooks not detected</span>
{% endif %}
</span> </span>
</div> </div>
<div class="card-body collapsible-content">
<div class="help-section">
<h4>Option 1: OAuth Flow (Recommended)</h4>
<ol>
<li>Go to <a href="https://developer.intuit.com" target="_blank">developer.intuit.com</a></li>
<li>Create or select your app → "Keys & OAuth" section</li>
<li>Add redirect URI: <code>http://localhost:5000/callback</code></li>
<li>Copy your Client ID and Client Secret</li>
<li>Save credentials above, then click "Connect to QuickBooks"</li>
</ol>
</div> </div>
<div class="help-section"> <div class="connection-status-display {% if is_connected and connection_type == 'qbd' %}connected{% else %}disconnected{% endif %}" id="qbd-connection-status">
<h4>Option 2: Manual Token Entry</h4> <div class="status-icon">
<ol> {% if is_connected and connection_type == 'qbd' %}
<li>In the developer portal, click "OAuth 2.0 Playground"</li> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<li>Authorize your sandbox/production company</li> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<li>Copy the Access Token, Refresh Token, and Realm ID</li> <polyline points="22 4 12 14.01 9 11.01"/>
<li>Paste them in the "Manual Token Entry" section above</li> </svg>
</ol> {% else %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{% endif %}
</div>
<div class="status-details">
<h4 id="qbd-status-text">{% if is_connected and connection_type == 'qbd' %}Connected{% else %}Not Connected{% endif %}</h4>
{% if is_connected and company_name and connection_type == 'qbd' %}
<p class="company-name" id="qbd-company-name">{{ company_name }}</p>
{% else %}
<p class="company-name" id="qbd-company-name"></p>
{% endif %}
</div>
</div> </div>
<div class="help-note"> <div class="qbd-instructions">
<strong>Note:</strong> Access tokens expire after 1 hour. The app will automatically refresh using the refresh token. <h4>How to connect:</h4>
<ol>
<li><strong>Open QuickBooks Desktop</strong> on this computer (run as Administrator if needed)</li>
<li><strong>Open a company file</strong> - a company must be open, not just QuickBooks</li>
<li><strong>IMPORTANT: Check Integrated Applications setting</strong>
<ul>
<li>Go to <strong>Edit → Preferences → Integrated Applications</strong></li>
<li>Click <strong>Company Preferences</strong> tab</li>
<li>Make sure <strong>"Don't allow any applications..."</strong> is <strong>UNCHECKED</strong></li>
</ul>
</li>
<li>Click <strong>"Connect to QuickBooks Desktop"</strong> below</li>
<li>When QuickBooks shows an authorization dialog, click <strong>"Yes, always allow access"</strong></li>
</ol>
<div class="qbd-troubleshooting">
<h5>Troubleshooting:</h5>
<ul>
<li><strong>Connection hangs?</strong> Check Edit → Preferences → Integrated Applications → Company Preferences → make sure "Don't allow any applications to access this company file" is UNCHECKED</li>
<li>Try switching to <strong>Single-user mode</strong> (File → Switch to Single-user Mode)</li>
<li>Check for hidden QuickBooks dialogs (Alt+Tab or check taskbar)</li>
<li>Run QuickBooks as Administrator</li>
<li>Make sure you're logged into a company file (not the "No Company Open" screen)</li>
</ul>
</div> </div>
</div> </div>
<div class="qbd-error-display" id="qbd-error-display" style="display: none;">
<div class="alert alert-danger">
<strong>Connection Error:</strong>
<p id="qbd-error-message"></p>
<small id="qbd-error-details"></small>
</div>
</div>
<div class="connection-actions">
{% if is_connected and connection_type == 'qbd' %}
<button type="button" id="test-connection-qbd" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Test Connection
</button>
<button type="button" id="disconnect-qbd" class="btn btn-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
<line x1="12" y1="2" x2="12" y2="12"/>
</svg>
Disconnect
</button>
{% else %}
<button type="button" id="connect-qbd" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
Connect to QuickBooks Desktop
</button>
<button type="button" id="refresh-qbd-status" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh Status
</button>
{% endif %}
</div>
{% endif %}
</div>
</section> </section>
</div> </div>
</div> </div>
</div>
<style> <style>
/* Collapsible Sections */ /* Connection Type Selector */
.card.collapsible .card-header { .connection-type-selector {
cursor: pointer; display: flex;
user-select: none; gap: 16px;
margin-bottom: 8px;
}
.connection-type-option {
flex: 1;
cursor: pointer;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
background: #fff;
}
.connection-type-option:hover:not(.disabled) {
border-color: #94a3b8;
}
.connection-type-option.active {
border-color: #2563eb;
background: #eff6ff;
}
.connection-type-option.disabled {
opacity: 0.6;
cursor: not-allowed;
background: #f8fafc;
}
.connection-type-option input[type="radio"] {
display: none;
}
.option-content {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
gap: 16px;
} }
.card.collapsible .card-header:hover { .option-icon {
background: var(--bg); width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
} }
.header-right { .option-icon.qbo-icon {
background: linear-gradient(135deg, #2CA01C 0%, #1a8a0e 100%);
color: white;
}
.option-icon.qbd-icon {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
}
.option-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.option-text strong {
font-size: 16px;
color: #1e293b;
}
.option-text span {
font-size: 13px;
color: #64748b;
}
/* QBD specific styles */
.qbd-info {
background: #f8fafc;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.info-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.collapse-icon { .info-label {
transition: transform 0.3s ease; font-weight: 500;
display: flex; color: #64748b;
}
.status-badge {
display: inline-flex;
align-items: center; align-items: center;
color: var(--text-muted); padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
} }
.card.collapsible.collapsed .collapse-icon { .status-badge.success {
transform: rotate(-90deg); background: #dcfce7;
color: #166534;
} }
.collapsible-content { .status-badge.warning {
max-height: 2000px; background: #fef3c7;
overflow: hidden; color: #92400e;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
opacity: 1;
} }
.card.collapsible.collapsed .collapsible-content { .qbd-instructions {
max-height: 0; background: #f0f9ff;
padding-top: 0; border: 1px solid #bae6fd;
padding-bottom: 0; border-radius: 8px;
opacity: 0; padding: 16px;
margin: 20px 0;
} }
/* Ensure card-body padding transitions smoothly */ .qbd-instructions h4 {
.card.collapsible .card-body { margin: 0 0 12px 0;
transition: padding 0.3s ease; color: #0369a1;
font-size: 14px;
} }
.card.collapsible.collapsed .card-body { .qbd-instructions ol {
padding: 0 20px; margin: 0;
padding-left: 20px;
color: #0c4a6e;
}
.qbd-instructions li {
margin-bottom: 8px;
font-size: 14px;
}
.qbd-instructions li:last-child {
margin-bottom: 0;
}
.alert {
display: flex;
gap: 12px;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-warning {
background: #fef3c7;
border: 1px solid #fcd34d;
color: #92400e;
}
.alert svg {
flex-shrink: 0;
margin-top: 2px;
}
.alert strong {
display: block;
margin-bottom: 8px;
}
.alert p {
margin: 0 0 8px 0;
font-size: 14px;
}
.alert ul {
margin: 0;
padding-left: 20px;
}
.alert li {
font-size: 13px;
margin-bottom: 4px;
}
.alert code {
background: rgba(0,0,0,0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.alert-danger {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
.alert-danger strong {
color: #7f1d1d;
}
.alert-danger p {
margin: 8px 0;
}
.alert-danger small {
display: block;
margin-top: 8px;
color: #b91c1c;
font-family: monospace;
font-size: 11px;
word-break: break-all;
}
.qbd-troubleshooting {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #bae6fd;
}
.qbd-troubleshooting h5 {
margin: 0 0 8px 0;
color: #0369a1;
font-size: 13px;
}
.qbd-troubleshooting ul {
margin: 0;
padding-left: 20px;
color: #0c4a6e;
}
.qbd-troubleshooting li {
font-size: 13px;
margin-bottom: 6px;
}
.qbd-error-display {
margin: 16px 0;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.connection-type-selector {
flex-direction: column;
}
} }
</style> </style>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script> <script>
// Toggle collapsible section
function toggleSection(header) {
const card = header.closest('.card.collapsible');
card.classList.toggle('collapsed');
// Save state to localStorage
const sectionId = header.querySelector('h3').textContent.trim();
const isCollapsed = card.classList.contains('collapsed');
localStorage.setItem('section_' + sectionId, isCollapsed ? 'collapsed' : 'expanded');
}
// Global variable to track OAuth polling
let oauthPollTimer = null; let oauthPollTimer = null;
// Toggle collapsible sections
function toggleSection(header) {
const card = header.closest('.card');
const content = card.querySelector('.collapsible-content');
card.classList.toggle('collapsed');
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Check if we need to handle OAuth completion on page load // Connection type switching
const oauthComplete = localStorage.getItem('oauth_complete'); document.querySelectorAll('input[name="connection_type"]').forEach(radio => {
if (oauthComplete) { radio.addEventListener('change', async function() {
localStorage.removeItem('oauth_complete'); const type = this.value;
// Page was just reloaded after OAuth - show success message
showToast('Successfully connected to QuickBooks!', 'success');
}
// Restore section states from localStorage // Update UI
document.querySelectorAll('.card.collapsible').forEach(card => { document.querySelectorAll('.connection-type-option').forEach(opt => {
const sectionId = card.querySelector('.card-header h3').textContent.trim(); opt.classList.remove('active');
const savedState = localStorage.getItem('section_' + sectionId); });
this.closest('.connection-type-option').classList.add('active');
if (savedState === 'collapsed') { // Show/hide sections
card.classList.add('collapsed'); document.getElementById('qbo-section').classList.toggle('hidden', type === 'qbd');
} else if (savedState === 'expanded') { document.getElementById('qbd-section').classList.toggle('hidden', type !== 'qbd');
card.classList.remove('collapsed');
// Notify server
try {
await fetch('/api/connection/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: type })
});
} catch (error) {
console.error('Error switching connection type:', error);
} }
}); });
});
// Save credentials form // Save credentials form
document.getElementById('credentials-form').addEventListener('submit', async function(e) { document.getElementById('credentials-form').addEventListener('submit', async function(e) {
@@ -339,7 +679,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// Connect via OAuth // Connect via OAuth (QBO)
const connectBtn = document.getElementById('connect-oauth'); const connectBtn = document.getElementById('connect-oauth');
if (connectBtn) { if (connectBtn) {
connectBtn.addEventListener('click', async function() { connectBtn.addEventListener('click', async function() {
@@ -352,13 +692,8 @@ document.addEventListener('DOMContentLoaded', function() {
const result = await response.json(); const result = await response.json();
if (result.success && result.auth_url) { if (result.success && result.auth_url) {
// Clear any previous OAuth flag
localStorage.removeItem('oauth_complete'); localStorage.removeItem('oauth_complete');
// Open OAuth in new window
const oauthPopup = window.open(result.auth_url, 'QBO_OAuth', 'width=600,height=700'); const oauthPopup = window.open(result.auth_url, 'QBO_OAuth', 'width=600,height=700');
// Start polling for OAuth completion
startOAuthPolling(oauthPopup); startOAuthPolling(oauthPopup);
} else { } else {
showToast(result.error || 'Failed to start OAuth flow', 'error'); showToast(result.error || 'Failed to start OAuth flow', 'error');
@@ -369,10 +704,10 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Test connection // Test connection QBO
const testBtn = document.getElementById('test-connection'); const testBtnQbo = document.getElementById('test-connection-qbo');
if (testBtn) { if (testBtnQbo) {
testBtn.addEventListener('click', async function() { testBtnQbo.addEventListener('click', async function() {
try { try {
const response = await fetch('/api/connection/test'); const response = await fetch('/api/connection/test');
const result = await response.json(); const result = await response.json();
@@ -388,19 +723,14 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Disconnect // Disconnect QBO
const disconnectBtn = document.getElementById('disconnect'); const disconnectBtnQbo = document.getElementById('disconnect-qbo');
if (disconnectBtn) { if (disconnectBtnQbo) {
disconnectBtn.addEventListener('click', async function() { disconnectBtnQbo.addEventListener('click', async function() {
if (!confirm('Are you sure you want to disconnect from QuickBooks?')) { if (!confirm('Are you sure you want to disconnect from QuickBooks Online?')) return;
return;
}
try { try {
const response = await fetch('/api/disconnect', { const response = await fetch('/api/disconnect', { method: 'POST' });
method: 'POST'
});
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
@@ -415,6 +745,136 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Connect to QBD
const connectBtnQbd = document.getElementById('connect-qbd');
if (connectBtnQbd) {
connectBtnQbd.addEventListener('click', async function() {
this.disabled = true;
this.innerHTML = '<span class="spinner"></span> Connecting... (Check QuickBooks for authorization dialog)';
// Show a helpful message
showToast('Connecting... Please check QuickBooks Desktop for an authorization dialog and click "Yes, always allow access"', 'info', 10000);
// Hide previous error
const errorDisplay = document.getElementById('qbd-error-display');
if (errorDisplay) errorDisplay.style.display = 'none';
try {
const response = await fetch('/api/qbd/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const result = await response.json();
console.log('QBD Connect result:', result);
if (result.success) {
showToast(`Connected to ${result.company_name}`, 'success');
setTimeout(() => location.reload(), 1000);
} else {
// Show detailed error
const errorMsg = result.error || 'Failed to connect to QuickBooks Desktop';
const errorDetails = result.details || result.code || '';
showToast(errorMsg, 'error');
// Show in error display area
if (errorDisplay) {
document.getElementById('qbd-error-message').textContent = errorMsg;
document.getElementById('qbd-error-details').textContent = errorDetails;
errorDisplay.style.display = 'block';
}
// Log full response for debugging
console.error('QBD connection failed:', result);
if (result.traceback) {
console.error('Traceback:', result.traceback);
}
this.disabled = false;
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Connect to QuickBooks Desktop';
}
} catch (error) {
console.error('QBD connection error:', error);
showToast('Error connecting: ' + error.message, 'error');
this.disabled = false;
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Connect to QuickBooks Desktop';
}
});
}
// Test connection QBD
const testBtnQbd = document.getElementById('test-connection-qbd');
if (testBtnQbd) {
testBtnQbd.addEventListener('click', async function() {
try {
const response = await fetch('/api/qbd/test');
const result = await response.json();
if (result.success) {
showToast(`Connected to ${result.company_name}. Found ${result.customers_count} customers, ${result.vendors_count} vendors, ${result.accounts_count} accounts.`, 'success');
} else {
showToast(result.error || 'Connection test failed', 'error');
}
} catch (error) {
showToast('Error testing connection: ' + error.message, 'error');
}
});
}
// Disconnect QBD
const disconnectBtnQbd = document.getElementById('disconnect-qbd');
if (disconnectBtnQbd) {
disconnectBtnQbd.addEventListener('click', async function() {
if (!confirm('Are you sure you want to disconnect from QuickBooks Desktop?')) return;
try {
const response = await fetch('/api/qbd/disconnect', { method: 'POST' });
const result = await response.json();
if (result.success) {
showToast('Disconnected successfully', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.error || 'Failed to disconnect', 'error');
}
} catch (error) {
showToast('Error disconnecting: ' + error.message, 'error');
}
});
}
// Refresh QBD status
const refreshBtnQbd = document.getElementById('refresh-qbd-status');
if (refreshBtnQbd) {
refreshBtnQbd.addEventListener('click', async function() {
try {
const response = await fetch('/api/qbd/status');
const result = await response.json();
if (result.success) {
const statusEl = document.querySelector('#qbd-status .status-badge');
const connectBtn = document.getElementById('connect-qbd');
if (result.qb_running) {
statusEl.className = 'status-badge success';
statusEl.textContent = 'QuickBooks is running';
if (connectBtn) connectBtn.disabled = false;
} else {
statusEl.className = 'status-badge warning';
statusEl.textContent = 'QuickBooks not detected';
if (connectBtn) connectBtn.disabled = true;
}
showToast('Status refreshed', 'info');
}
} catch (error) {
showToast('Error refreshing status: ' + error.message, 'error');
}
});
}
// Manual token form // Manual token form
document.getElementById('manual-token-form').addEventListener('submit', async function(e) { document.getElementById('manual-token-form').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
@@ -453,18 +913,16 @@ document.addEventListener('DOMContentLoaded', function() {
// Function to poll for OAuth completion // Function to poll for OAuth completion
function startOAuthPolling(popup) { function startOAuthPolling(popup) {
// Clear any existing timer
if (oauthPollTimer) { if (oauthPollTimer) {
clearInterval(oauthPollTimer); clearInterval(oauthPollTimer);
} }
let checkCount = 0; let checkCount = 0;
const maxChecks = 300; // 5 minutes max (300 * 1000ms) const maxChecks = 300;
oauthPollTimer = setInterval(async function() { oauthPollTimer = setInterval(async function() {
checkCount++; checkCount++;
// Check localStorage for OAuth completion signal
const oauthComplete = localStorage.getItem('oauth_complete'); const oauthComplete = localStorage.getItem('oauth_complete');
if (oauthComplete) { if (oauthComplete) {
clearInterval(oauthPollTimer); clearInterval(oauthPollTimer);
@@ -474,11 +932,9 @@ function startOAuthPolling(popup) {
return; return;
} }
// Check if popup is closed
if (popup && popup.closed) { if (popup && popup.closed) {
clearInterval(oauthPollTimer); clearInterval(oauthPollTimer);
// Wait a moment then check connection status
setTimeout(async function() { setTimeout(async function() {
try { try {
const response = await fetch('/api/connection/test'); const response = await fetch('/api/connection/test');
@@ -497,7 +953,6 @@ function startOAuthPolling(popup) {
return; return;
} }
// Stop polling after max time
if (checkCount >= maxChecks) { if (checkCount >= maxChecks) {
clearInterval(oauthPollTimer); clearInterval(oauthPollTimer);
showToast('OAuth timeout. Please try again.', 'warning'); showToast('OAuth timeout. Please try again.', 'warning');
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
QuickBooks Desktop Connection Test Script
Run this directly on your Windows machine to test the connection.
Usage:
python test_qbd_connection.py
"""
import sys
import time
print("=" * 60)
print("QuickBooks Desktop Connection Test")
print("=" * 60)
# Check platform
print(f"\n1. Platform: {sys.platform}")
if sys.platform != 'win32':
print(" ERROR: This script must be run on Windows!")
sys.exit(1)
print(" OK: Running on Windows")
# Check pywin32
print("\n2. Checking pywin32...")
try:
import pythoncom
from win32com.client import Dispatch
print(" OK: pywin32 is installed")
except ImportError as e:
print(f" ERROR: pywin32 not installed: {e}")
print(" Run: pip install pywin32")
sys.exit(1)
# Initialize COM
print("\n3. Initializing COM...")
try:
pythoncom.CoInitialize()
print(" OK: COM initialized")
except Exception as e:
print(f" ERROR: Failed to initialize COM: {e}")
sys.exit(1)
# Create RequestProcessor
print("\n4. Creating QBXMLRP2.RequestProcessor...")
try:
rp = Dispatch("QBXMLRP2.RequestProcessor")
print(" OK: RequestProcessor created")
except Exception as e:
print(f" ERROR: Failed to create RequestProcessor: {e}")
print("\n This usually means:")
print(" - QuickBooks Desktop is not installed")
print(" - QuickBooks SDK components are not registered")
print(" - Try: regsvr32 \"C:\\Program Files (x86)\\Common Files\\Intuit\\QuickBooks\\qbxmlrp2.dll\"")
pythoncom.CoUninitialize()
sys.exit(1)
# Open Connection
print("\n5. Opening connection (OpenConnection2)...")
try:
# Connection types:
# 1 = localQBD (local QuickBooks Desktop)
# 2 = remoteQBD (remote QuickBooks Desktop)
# 3 = localQBDLaunchUI (launch QB if not running)
rp.OpenConnection2("", "QBD Test Script", 1)
print(" OK: Connection opened")
except Exception as e:
print(f" ERROR: OpenConnection2 failed: {e}")
pythoncom.CoUninitialize()
sys.exit(1)
# Begin Session - THIS IS WHERE IT USUALLY HANGS
print("\n6. Beginning session (BeginSession)...")
print(" NOTE: If this hangs, check QuickBooks for an authorization dialog!")
print(" Also check: Edit > Preferences > Integrated Applications > Company Preferences")
print(" Make sure 'Don't allow any applications...' is UNCHECKED")
print("")
print(" Attempting BeginSession with timeout indicator...")
print(" (If you see dots appearing, the call is hanging)")
# Start time
start_time = time.time()
# We can't easily timeout a COM call, but we can show progress
import threading
stop_dots = False
def print_dots():
count = 0
while not stop_dots:
time.sleep(1)
count += 1
print(f" ... waiting {count}s", end='\r')
if count > 30:
print("\n WARNING: BeginSession is taking too long (>30s)")
print(" This usually means QuickBooks is waiting for user input")
print(" CHECK QUICKBOOKS FOR A DIALOG BOX!")
dot_thread = threading.Thread(target=print_dots)
dot_thread.daemon = True
dot_thread.start()
try:
# File modes:
# 0 = qbFileOpenDoNotCare
# 1 = qbFileOpenSingleUser
# 2 = qbFileOpenMultiUser
ticket = rp.BeginSession("", 0) # Empty string = currently open file
stop_dots = True
elapsed = time.time() - start_time
print(f"\n OK: Session started in {elapsed:.2f}s")
print(f" Session ticket: {ticket[:30]}...")
except Exception as e:
stop_dots = True
elapsed = time.time() - start_time
print(f"\n ERROR: BeginSession failed after {elapsed:.2f}s: {e}")
error_msg = str(e)
if "80040408" in error_msg:
print("\n DIAGNOSIS: QuickBooks is not running or no company file is open")
elif "80040416" in error_msg or "80040417" in error_msg:
print("\n DIAGNOSIS: Access denied - need to authorize in QuickBooks")
elif "80040422" in error_msg:
print("\n DIAGNOSIS: No company file is open")
elif "80040423" in error_msg:
print("\n DIAGNOSIS: Company file is in single-user mode by another user")
try:
rp.CloseConnection()
except:
pass
pythoncom.CoUninitialize()
sys.exit(1)
# If we got here, try a simple query
print("\n7. Testing with a simple Company query...")
try:
# Build a simple QBXML request
request = '''<?xml version="1.0" encoding="utf-8"?>
<?qbxml version="13.0"?>
<QBXML>
<QBXMLMsgsRq onError="stopOnError">
<CompanyQueryRq>
</CompanyQueryRq>
</QBXMLMsgsRq>
</QBXML>'''
response = rp.ProcessRequest(ticket, request)
print(" OK: Query executed successfully")
# Parse response to get company name
import xml.etree.ElementTree as ET
root = ET.fromstring(response)
company_name = root.find(".//CompanyName")
if company_name is not None:
print(f" Company Name: {company_name.text}")
except Exception as e:
print(f" ERROR: Query failed: {e}")
# End session
print("\n8. Ending session...")
try:
rp.EndSession(ticket)
print(" OK: Session ended")
except Exception as e:
print(f" WARNING: EndSession failed: {e}")
# Close connection
print("\n9. Closing connection...")
try:
rp.CloseConnection()
print(" OK: Connection closed")
except Exception as e:
print(f" WARNING: CloseConnection failed: {e}")
# Uninitialize COM
try:
pythoncom.CoUninitialize()
except:
pass
print("\n" + "=" * 60)
print("TEST COMPLETED SUCCESSFULLY!")
print("=" * 60)
print("\nQuickBooks Desktop connection is working properly.")
print("If the web app still doesn't work, the issue may be with")
print("Flask session handling or threading.")