Initial Codes

This commit is contained in:
2026-02-13 10:38:45 -05:00
parent 0dd81218e0
commit 40a151787f
24 changed files with 7627 additions and 1 deletions
+351
View File
@@ -0,0 +1,351 @@
"""
Application settings and configuration management.
Handles OAuth tokens, mapping templates, and user preferences.
"""
import json
import os
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional, Dict, List, Any
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
@dataclass
class QBOCredentials:
"""QuickBooks Online OAuth credentials."""
client_id: str = ""
client_secret: str = ""
redirect_uri: str = "http://localhost:5000/oauth/callback"
environment: str = "sandbox" # 'sandbox' or 'production'
# Token data (stored securely)
access_token: Optional[str] = None
refresh_token: Optional[str] = None
realm_id: Optional[str] = None
token_expiry: Optional[str] = None
@dataclass
class FieldMapping:
"""Single field mapping configuration."""
excel_column: str
qbo_field: str
transform: Optional[str] = None # 'date', 'currency', 'lookup', etc.
default_value: Optional[str] = None
required: bool = False
@dataclass
class MappingTemplate:
"""Complete mapping template for a data type."""
name: str
data_type: str # 'check', 'invoice', 'bill', 'customer', 'vendor', 'account'
mappings: List[FieldMapping] = field(default_factory=list)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
@dataclass
class ImportSettings:
"""Import behavior settings."""
skip_duplicates: bool = True
duplicate_check_fields: List[str] = field(default_factory=lambda: ["DocNumber"])
batch_size: int = 50
validate_before_import: bool = True
create_missing_references: bool = False # Auto-create missing customers/vendors
date_format: str = "%Y-%m-%d"
decimal_separator: str = "."
thousand_separator: str = ","
class Settings:
"""Application settings manager."""
APP_NAME = "QBOExcelSyncWeb"
def __init__(self):
self.config_dir = self._get_config_dir()
self.config_file = self.config_dir / "config.json"
self.templates_dir = self.config_dir / "templates"
self.credentials_file = self.config_dir / ".credentials"
# Ensure directories exist
self.config_dir.mkdir(parents=True, exist_ok=True)
self.templates_dir.mkdir(parents=True, exist_ok=True)
# Load or create default config
self._config = self._load_config()
# Credentials (stored separately for security)
self._credentials: Optional[QBOCredentials] = None
logger.info(f"Settings initialized. Config dir: {self.config_dir}")
def _get_config_dir(self) -> Path:
"""Get platform-specific config directory."""
# For web app, use a local directory
base = Path(__file__).parent.parent.parent
return base / "data"
def _load_config(self) -> Dict[str, Any]:
"""Load configuration from file."""
default_config = {
"import_settings": asdict(ImportSettings()),
"recent_files": [],
"last_template": None,
"qbo_environment": "sandbox",
}
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
loaded = json.load(f)
# Merge with defaults
for key, value in default_config.items():
if key not in loaded:
loaded[key] = value
return loaded
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to load config: {e}")
return default_config
return default_config
def save(self):
"""Save configuration to file."""
try:
with open(self.config_file, 'w') as f:
json.dump(self._config, f, indent=2)
logger.info("Configuration saved successfully")
except IOError as e:
logger.error(f"Failed to save config: {e}")
@property
def import_settings(self) -> ImportSettings:
"""Get import settings."""
return ImportSettings(**self._config.get("import_settings", {}))
@import_settings.setter
def import_settings(self, settings: ImportSettings):
"""Set import settings."""
self._config["import_settings"] = asdict(settings)
self.save()
logger.info("Import settings updated")
@property
def recent_files(self) -> List[str]:
"""Get list of recently opened files."""
return self._config.get("recent_files", [])
def add_recent_file(self, filepath: str):
"""Add file to recent files list."""
files = self.recent_files
if filepath in files:
files.remove(filepath)
files.insert(0, filepath)
self._config["recent_files"] = files[:10] # Keep only 10 recent
self.save()
@property
def qbo_environment(self) -> str:
"""Get QBO environment (sandbox/production)."""
return self._config.get("qbo_environment", "sandbox")
@qbo_environment.setter
def qbo_environment(self, env: str):
"""Set QBO environment."""
if env not in ("sandbox", "production"):
raise ValueError("Environment must be 'sandbox' or 'production'")
self._config["qbo_environment"] = env
self.save()
logger.info(f"QBO environment set to: {env}")
# Credential storage (file-based for web app)
def get_credentials(self) -> QBOCredentials:
"""Get QBO credentials from file storage."""
if self._credentials is None:
self._credentials = QBOCredentials()
try:
if self.credentials_file.exists():
with open(self.credentials_file, 'r') as f:
creds_data = json.load(f)
self._credentials = QBOCredentials(**creds_data)
except Exception as e:
logger.warning(f"Failed to load credentials: {e}")
return self._credentials
def save_credentials(self, credentials: QBOCredentials):
"""Save QBO credentials to file storage."""
self._credentials = credentials
try:
with open(self.credentials_file, 'w') as f:
json.dump(asdict(credentials), f)
os.chmod(self.credentials_file, 0o600) # Restrict permissions
logger.info("Credentials saved successfully")
except Exception as e:
logger.error(f"Failed to save credentials: {e}")
def clear_credentials(self):
"""Clear stored credentials."""
self._credentials = None
if self.credentials_file.exists():
self.credentials_file.unlink()
logger.info("Credentials cleared")
# Template management
def get_templates(self, data_type: Optional[str] = None) -> List[MappingTemplate]:
"""Get all mapping templates, optionally filtered by data type."""
templates = []
for template_file in self.templates_dir.glob("*.json"):
try:
with open(template_file, 'r') as f:
data = json.load(f)
# Reconstruct FieldMapping objects
mappings = [FieldMapping(**m) for m in data.get("mappings", [])]
template = MappingTemplate(
name=data["name"],
data_type=data["data_type"],
mappings=mappings,
created_at=data.get("created_at", ""),
updated_at=data.get("updated_at", "")
)
if data_type is None or template.data_type == data_type:
templates.append(template)
except (json.JSONDecodeError, IOError, KeyError) as e:
logger.warning(f"Failed to load template {template_file}: {e}")
continue
return sorted(templates, key=lambda t: t.name)
def save_template(self, template: MappingTemplate):
"""Save a mapping template."""
template.updated_at = datetime.now().isoformat()
# Convert to serializable format
data = {
"name": template.name,
"data_type": template.data_type,
"mappings": [asdict(m) for m in template.mappings],
"created_at": template.created_at,
"updated_at": template.updated_at
}
# Safe filename
safe_name = "".join(c for c in template.name if c.isalnum() or c in " -_").strip()
filepath = self.templates_dir / f"{safe_name}.json"
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Template saved: {template.name}")
def delete_template(self, template_name: str):
"""Delete a mapping template."""
safe_name = "".join(c for c in template_name if c.isalnum() or c in " -_").strip()
filepath = self.templates_dir / f"{safe_name}.json"
if filepath.exists():
filepath.unlink()
logger.info(f"Template deleted: {template_name}")
def get_default_templates(self) -> Dict[str, MappingTemplate]:
"""Get built-in default templates for each data type."""
return {
"check": MappingTemplate(
name="Default Check Import",
data_type="check",
mappings=[
FieldMapping("Payee", "EntityRef.value", required=True),
FieldMapping("Bank Acct", "AccountRef.value", required=True),
FieldMapping("Date", "TxnDate", transform="date", required=True),
FieldMapping("Amount", "Line.Amount", transform="currency", required=True),
FieldMapping("Number", "DocNumber"),
FieldMapping("Posting Acct", "Line.AccountBasedExpenseLineDetail.AccountRef.value"),
FieldMapping("Period Memo", "PrivateNote"),
FieldMapping("Period Memo", "Line.Description"),
]
),
"invoice": MappingTemplate(
name="Default Invoice Import",
data_type="invoice",
mappings=[
FieldMapping("CustomerRef", "CustomerRef.value", required=True),
FieldMapping("TxnDate", "TxnDate", transform="date", required=True),
FieldMapping("DueDate", "DueDate", transform="date"),
FieldMapping("DocNumber", "DocNumber"),
FieldMapping("ItemRef", "Line.SalesItemLineDetail.ItemRef.value"),
FieldMapping("Description", "Line.Description"),
FieldMapping("Qty", "Line.SalesItemLineDetail.Qty", transform="number"),
FieldMapping("UnitPrice", "Line.SalesItemLineDetail.UnitPrice", transform="currency"),
FieldMapping("Amount", "Line.Amount", transform="currency"),
FieldMapping("PrivateNote", "PrivateNote"),
]
),
"bill": MappingTemplate(
name="Default Bill Import",
data_type="bill",
mappings=[
FieldMapping("VendorRef", "VendorRef.value", required=True),
FieldMapping("TxnDate", "TxnDate", transform="date", required=True),
FieldMapping("DueDate", "DueDate", transform="date"),
FieldMapping("DocNumber", "DocNumber"),
FieldMapping("AccountRef", "Line.AccountBasedExpenseLineDetail.AccountRef.value"),
FieldMapping("Description", "Line.Description"),
FieldMapping("Amount", "Line.Amount", transform="currency"),
FieldMapping("PrivateNote", "PrivateNote"),
]
),
"customer": MappingTemplate(
name="Default Customer Import",
data_type="customer",
mappings=[
FieldMapping("DisplayName", "DisplayName", required=True),
FieldMapping("CompanyName", "CompanyName"),
FieldMapping("GivenName", "GivenName"),
FieldMapping("FamilyName", "FamilyName"),
FieldMapping("Email", "PrimaryEmailAddr.Address"),
FieldMapping("Phone", "PrimaryPhone.FreeFormNumber"),
FieldMapping("Street", "BillAddr.Line1"),
FieldMapping("City", "BillAddr.City"),
FieldMapping("State", "BillAddr.CountrySubDivisionCode"),
FieldMapping("PostalCode", "BillAddr.PostalCode"),
FieldMapping("Country", "BillAddr.Country"),
]
),
"vendor": MappingTemplate(
name="Default Vendor Import",
data_type="vendor",
mappings=[
FieldMapping("DisplayName", "DisplayName", required=True),
FieldMapping("CompanyName", "CompanyName"),
FieldMapping("GivenName", "GivenName"),
FieldMapping("FamilyName", "FamilyName"),
FieldMapping("Email", "PrimaryEmailAddr.Address"),
FieldMapping("Phone", "PrimaryPhone.FreeFormNumber"),
FieldMapping("Street", "BillAddr.Line1"),
FieldMapping("City", "BillAddr.City"),
FieldMapping("State", "BillAddr.CountrySubDivisionCode"),
FieldMapping("PostalCode", "BillAddr.PostalCode"),
FieldMapping("TaxIdentifier", "TaxIdentifier"),
]
),
"account": MappingTemplate(
name="Default Chart of Accounts Import",
data_type="account",
mappings=[
FieldMapping("Name", "Name", required=True),
FieldMapping("AccountType", "AccountType", required=True),
FieldMapping("AccountSubType", "AccountSubType"),
FieldMapping("AcctNum", "AcctNum"),
FieldMapping("Description", "Description"),
FieldMapping("CurrentBalance", "CurrentBalance", transform="currency"),
]
),
}