From 807e7f639571931e26fc20c9beaeca25c229fa84 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 23 Dec 2025 01:44:14 +0100 Subject: [PATCH] Feature: Opdater vTiger Timelog med Hub ordre ID efter eksport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tilføjet update_timelog_billed() metode til vtiger_sync.py - Opdaterer billed_via_thehub_id felt i vTiger Timelog records - Kaldes automatisk efter succesfuld e-conomic eksport - Respekterer READ_ONLY og DRY_RUN safety flags - Fejler ikke eksporten hvis vTiger update fejler (bare logger warning) --- app/timetracking/backend/economic_export.py | 27 ++++++++ app/timetracking/backend/vtiger_sync.py | 71 +++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/app/timetracking/backend/economic_export.py b/app/timetracking/backend/economic_export.py index a97f77f..b29b6c4 100644 --- a/app/timetracking/backend/economic_export.py +++ b/app/timetracking/backend/economic_export.py @@ -24,6 +24,7 @@ from app.timetracking.backend.models import ( TModuleEconomicExportResult ) from app.timetracking.backend.audit import audit +from app.timetracking.backend.vtiger_sync import vtiger_service logger = logging.getLogger(__name__) @@ -412,6 +413,32 @@ class EconomicExportService: (request.order_id,) ) + # Hent vTiger IDs for tidsregistreringerne + vtiger_ids_query = """ + SELECT t.vtiger_id + FROM tmodule_times t + WHERE t.id IN ( + SELECT UNNEST(time_entry_ids) + FROM tmodule_order_lines + WHERE order_id = %s + ) + """ + vtiger_time_records = execute_query(vtiger_ids_query, (request.order_id,)) + vtiger_ids = [r['vtiger_id'] for r in vtiger_time_records] + + # Opdater Timelog records i vTiger med Hub ordre ID + if vtiger_ids: + logger.info(f"📝 Updating {len(vtiger_ids)} timelogs in vTiger with Hub order {request.order_id}...") + try: + vtiger_update_result = await vtiger_service.update_timelog_billed( + vtiger_ids=vtiger_ids, + hub_order_id=request.order_id + ) + logger.info(f"✅ vTiger update: {vtiger_update_result}") + except Exception as vtiger_error: + # Don't fail export if vTiger update fails - just log it + logger.error(f"⚠️ Could not update vTiger timelogs: {vtiger_error}") + # Log successful export audit.log_export_completed( order_id=request.order_id, diff --git a/app/timetracking/backend/vtiger_sync.py b/app/timetracking/backend/vtiger_sync.py index 4ff77f4..b6ef9af 100644 --- a/app/timetracking/backend/vtiger_sync.py +++ b/app/timetracking/backend/vtiger_sync.py @@ -210,6 +210,77 @@ class TimeTrackingVTigerService: logger.error(f"❌ vTiger connection error: {e}") return False + async def update_timelog_billed(self, vtiger_ids: List[str], hub_order_id: int) -> Dict[str, int]: + """ + Update Timelog records in vTiger with Hub ordre ID (markér som faktureret). + + 🚨 WRITE operation - respekterer safety flags + + Args: + vtiger_ids: Liste af vTiger Timelog IDs (format: "43x1234") + hub_order_id: Hub ordre ID til at sætte i billed_via_thehub_id felt + + Returns: + Dict with success/error counts + """ + if self.read_only: + logger.warning(f"🔒 READ-ONLY mode: Would update {len(vtiger_ids)} timelogs with Hub order {hub_order_id}") + return {"updated": 0, "errors": 0, "message": "READ-ONLY mode"} + + if self.dry_run: + logger.warning(f"🏃 DRY-RUN mode: Would update {len(vtiger_ids)} timelogs with Hub order {hub_order_id}") + return {"updated": 0, "errors": 0, "message": "DRY-RUN mode"} + + stats = {"updated": 0, "errors": 0} + + logger.info(f"📝 Updating {len(vtiger_ids)} timelogs in vTiger with Hub order {hub_order_id}...") + + try: + async with aiohttp.ClientSession() as session: + for vtiger_id in vtiger_ids: + try: + # Build update payload + update_url = f"{self.rest_endpoint}/update" + payload = { + "elementType": "Timelog", + "element": { + "id": vtiger_id, + "billed_via_thehub_id": str(hub_order_id) + } + } + + async with session.post( + update_url, + json=payload, + auth=self._get_auth(), + timeout=aiohttp.ClientTimeout(total=30) + ) as response: + text = await response.text() + + if response.status == 200: + data = json.loads(text) + if data.get('success'): + logger.debug(f"✅ Updated timelog {vtiger_id}") + stats["updated"] += 1 + else: + error_msg = data.get('error', {}).get('message', 'Unknown error') + logger.error(f"❌ vTiger error for {vtiger_id}: {error_msg}") + stats["errors"] += 1 + else: + logger.error(f"❌ HTTP {response.status} for {vtiger_id}: {text[:200]}") + stats["errors"] += 1 + + except Exception as e: + logger.error(f"❌ Error updating timelog {vtiger_id}: {e}") + stats["errors"] += 1 + + except Exception as e: + logger.error(f"❌ Batch update error: {e}") + stats["errors"] += len(vtiger_ids) + + logger.info(f"✅ vTiger update complete: {stats['updated']} updated, {stats['errors']} errors") + return stats + async def _fetch_user_name(self, user_id: str) -> str: """ Fetch user name from vTiger using retrieve API.