bmc_hub/app/timetracking/backend/economic_export.py

301 lines
11 KiB
Python
Raw Normal View History

"""
e-conomic Export Service for Time Tracking Module
==================================================
🚨 KRITISK: Denne service skal respektere safety flags.
Eksporterer ordrer til e-conomic som draft orders.
Safety Flags:
- TIMETRACKING_ECONOMIC_READ_ONLY = True (default)
- TIMETRACKING_ECONOMIC_DRY_RUN = True (default)
"""
import logging
from typing import Optional, Dict, Any
import aiohttp
from fastapi import HTTPException
from app.core.config import settings
from app.core.database import execute_query, execute_update
from app.timetracking.backend.models import (
TModuleEconomicExportRequest,
TModuleEconomicExportResult
)
from app.timetracking.backend.audit import audit
logger = logging.getLogger(__name__)
class EconomicExportService:
"""
e-conomic integration for Time Tracking Module.
🔒 SAFETY-FIRST service - all writes controlled by flags.
"""
def __init__(self):
self.api_url = settings.ECONOMIC_API_URL
self.app_secret_token = settings.ECONOMIC_APP_SECRET_TOKEN
self.agreement_grant_token = settings.ECONOMIC_AGREEMENT_GRANT_TOKEN
# Safety flags
self.read_only = settings.TIMETRACKING_ECONOMIC_READ_ONLY
self.dry_run = settings.TIMETRACKING_ECONOMIC_DRY_RUN
self.export_type = settings.TIMETRACKING_EXPORT_TYPE
# Log safety status
if self.read_only:
logger.warning("🔒 TIMETRACKING e-conomic READ-ONLY mode: Enabled")
if self.dry_run:
logger.warning("🏃 TIMETRACKING e-conomic DRY-RUN mode: Enabled")
if not self.read_only:
logger.error("⚠️ WARNING: TIMETRACKING e-conomic READ-ONLY disabled!")
def _get_headers(self) -> Dict[str, str]:
"""Get e-conomic API headers"""
return {
'X-AppSecretToken': self.app_secret_token,
'X-AgreementGrantToken': self.agreement_grant_token,
'Content-Type': 'application/json'
}
def _check_write_permission(self, operation: str) -> bool:
"""
Check om write operation er tilladt.
Returns:
True hvis operationen udføres, False hvis blokeret
"""
if self.read_only:
logger.error(f"🚫 BLOCKED: {operation} - READ_ONLY mode enabled")
return False
if self.dry_run:
logger.warning(f"🏃 DRY-RUN: {operation} - Would execute but not sending")
return False
logger.warning(f"⚠️ EXECUTING WRITE: {operation}")
return True
async def test_connection(self) -> bool:
"""Test e-conomic connection"""
try:
logger.info("🔍 Testing e-conomic connection...")
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.api_url}/self",
headers=self._get_headers(),
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
logger.info("✅ e-conomic connection successful")
return True
else:
logger.error(f"❌ e-conomic connection failed: {response.status}")
return False
except Exception as e:
logger.error(f"❌ e-conomic connection error: {e}")
return False
async def export_order(
self,
request: TModuleEconomicExportRequest,
user_id: Optional[int] = None
) -> TModuleEconomicExportResult:
"""
Eksporter ordre til e-conomic som draft order.
Args:
request: Export request med order_id
user_id: ID brugeren der eksporterer
Returns:
Export result med success status
"""
try:
# Hent ordre med linjer
order_query = """
SELECT o.*, c.name as customer_name, c.vtiger_id as customer_vtiger_id
FROM tmodule_orders o
JOIN tmodule_customers c ON o.customer_id = c.id
WHERE o.id = %s
"""
order = execute_query(order_query, (request.order_id,), fetchone=True)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
# Check if already exported
if order['economic_draft_id'] and not request.force:
raise HTTPException(
status_code=400,
detail=f"Order already exported (draft ID: {order['economic_draft_id']}). Use force=true to re-export."
)
# Hent ordre-linjer
lines_query = """
SELECT * FROM tmodule_order_lines
WHERE order_id = %s
ORDER BY line_number
"""
lines = execute_query(lines_query, (request.order_id,))
if not lines:
raise HTTPException(
status_code=400,
detail="Order has no lines"
)
# Log export start
audit.log_export_started(
order_id=request.order_id,
user_id=user_id
)
# Check safety flags
operation = f"Export order {request.order_id} to e-conomic"
if not self._check_write_permission(operation):
# Dry-run or read-only mode
result = TModuleEconomicExportResult(
success=True,
dry_run=True,
order_id=request.order_id,
economic_draft_id=None,
economic_order_number=None,
message=f"DRY-RUN: Would export order {order['order_number']} to e-conomic",
details={
"order_number": order['order_number'],
"customer_name": order['customer_name'],
"total_amount": float(order['total_amount']),
"line_count": len(lines),
"read_only": self.read_only,
"dry_run": self.dry_run
}
)
# Log dry-run completion
audit.log_export_completed(
order_id=request.order_id,
economic_draft_id=None,
economic_order_number=None,
dry_run=True,
user_id=user_id
)
return result
# REAL EXPORT (kun hvis safety flags er disabled)
logger.warning(f"⚠️ REAL EXPORT STARTING for order {request.order_id}")
# TODO: Implementer rigtig e-conomic API call her
# Denne kode vil kun køre hvis READ_ONLY og DRY_RUN begge er False
# Build e-conomic draft order payload
economic_payload = {
"date": order['order_date'].isoformat(),
"currency": "DKK",
"exchangeRate": 100,
"netAmount": float(order['subtotal']),
"grossAmount": float(order['total_amount']),
"vatAmount": float(order['vat_amount']),
"notes": {
"heading": f"Tidsregistrering - {order['order_number']}",
"textLine1": order.get('notes', '')
},
"lines": [
{
"lineNumber": line['line_number'],
"description": line['description'],
"quantity": float(line['quantity']),
"unitNetPrice": float(line['unit_price']),
"totalNetAmount": float(line['line_total'])
}
for line in lines
]
}
# Call e-conomic API
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.api_url}/orders/drafts",
headers=self._get_headers(),
json=economic_payload,
timeout=aiohttp.ClientTimeout(total=30)
) as response:
if response.status not in [200, 201]:
error_text = await response.text()
logger.error(f"❌ e-conomic export failed: {response.status} - {error_text}")
# Log failed export
audit.log_export_failed(
order_id=request.order_id,
error=error_text,
user_id=user_id
)
raise HTTPException(
status_code=response.status,
detail=f"e-conomic API error: {error_text}"
)
result_data = await response.json()
economic_draft_id = result_data.get('draftOrderNumber')
economic_order_number = result_data.get('orderNumber', str(economic_draft_id))
# Update order med e-conomic IDs
execute_update(
"""UPDATE tmodule_orders
SET economic_draft_id = %s,
economic_order_number = %s,
exported_at = CURRENT_TIMESTAMP,
exported_by = %s,
status = 'exported'
WHERE id = %s""",
(economic_draft_id, economic_order_number, user_id, request.order_id)
)
# Log successful export
audit.log_export_completed(
order_id=request.order_id,
economic_draft_id=economic_draft_id,
economic_order_number=economic_order_number,
dry_run=False,
user_id=user_id
)
logger.info(f"✅ Exported order {request.order_id} → e-conomic draft {economic_draft_id}")
return TModuleEconomicExportResult(
success=True,
dry_run=False,
order_id=request.order_id,
economic_draft_id=economic_draft_id,
economic_order_number=economic_order_number,
message=f"Successfully exported to e-conomic draft {economic_draft_id}",
details=result_data
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Error exporting order: {e}")
# Log failed export
audit.log_export_failed(
order_id=request.order_id,
error=str(e),
user_id=user_id
)
raise HTTPException(status_code=500, detail=str(e))
# Singleton instance
economic_service = EconomicExportService()