Initial Codes
This commit is contained in:
@@ -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"),
|
||||
]
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user