feat(sync): Implement auto-reset for permanently_failed entities and add retry backoff logic
- Added logic to reset permanently_failed entities that have reached their auto-reset threshold in `beteiligte_sync_cron_step.py`. - Enhanced event handling in `beteiligte_sync_event_step.py` to skip retries if the next retry time has not been reached. - Introduced validation checks after sync operations to ensure data consistency and integrity. - Created detailed documentation outlining the fixes and their impacts on the sync process. - Added scripts for analyzing sync issues and comparing entities to facilitate debugging and validation.
This commit is contained in:
@@ -116,6 +116,14 @@ class KommunikationSyncManager:
|
||||
result['advoware_to_espocrm'] = espo_result
|
||||
elif direction in ['both', 'to_espocrm'] and espo_wins:
|
||||
self.logger.info(f"[KOMM] ⚠️ CONFLICT: EspoCRM wins - skipping Advoware→EspoCRM sync")
|
||||
|
||||
# FIX: Bei Konflikt müssen Var4-Einträge (neu in Advoware) zu Empty Slots gemacht werden!
|
||||
# Sonst bleiben sie in Advoware aber nicht in EspoCRM → Nicht synchron!
|
||||
self.logger.info(f"[KOMM] 🔄 Converting {len(diff['advo_new'])} Var4 entries to Empty Slots (EspoCRM wins)...")
|
||||
for komm in diff['advo_new']:
|
||||
await self._create_empty_slot(betnr, komm)
|
||||
result['espocrm_to_advoware']['deleted'] += 1
|
||||
|
||||
else:
|
||||
self.logger.info(f"[KOMM] ℹ️ Skipping Advoware→EspoCRM (direction={direction})")
|
||||
|
||||
@@ -147,13 +155,21 @@ class KommunikationSyncManager:
|
||||
|
||||
import hashlib
|
||||
final_kommunikationen = advo_bet_final.get('kommunikation', [])
|
||||
komm_rowids = sorted([k.get('rowId', '') for k in final_kommunikationen if k.get('rowId')])
|
||||
|
||||
# FIX #3: Nur sync-relevante Kommunikationen für Hash verwenden
|
||||
# (nicht leere Slots oder nicht-sync-relevante Einträge)
|
||||
sync_relevant_komm = [
|
||||
k for k in final_kommunikationen
|
||||
if should_sync_to_espocrm(k)
|
||||
]
|
||||
|
||||
komm_rowids = sorted([k.get('rowId', '') for k in sync_relevant_komm if k.get('rowId')])
|
||||
new_komm_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
|
||||
|
||||
await self.espocrm.update_entity('CBeteiligte', beteiligte_id, {
|
||||
'kommunikationHash': new_komm_hash
|
||||
})
|
||||
self.logger.info(f"[KOMM] ✅ Updated kommunikationHash: {new_komm_hash}")
|
||||
self.logger.info(f"[KOMM] ✅ Updated kommunikationHash: {new_komm_hash} (based on {len(sync_relevant_komm)} sync-relevant of {len(final_kommunikationen)} total)")
|
||||
|
||||
self.logger.info(f"[KOMM] ✅ Bidirectional Sync complete: {total_changes} total changes")
|
||||
|
||||
@@ -201,8 +217,13 @@ class KommunikationSyncManager:
|
||||
last_sync = espo_bet.get('advowareLastSync')
|
||||
|
||||
# Berechne Hash aus Kommunikations-rowIds
|
||||
# FIX #3: Nur sync-relevante Kommunikationen für Hash verwenden
|
||||
import hashlib
|
||||
komm_rowids = sorted([k.get('rowId', '') for k in advo_kommunikationen if k.get('rowId')])
|
||||
sync_relevant_komm = [
|
||||
k for k in advo_kommunikationen
|
||||
if should_sync_to_espocrm(k)
|
||||
]
|
||||
komm_rowids = sorted([k.get('rowId', '') for k in sync_relevant_komm if k.get('rowId')])
|
||||
current_advo_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
|
||||
stored_komm_hash = espo_bet.get('kommunikationHash')
|
||||
|
||||
@@ -534,17 +555,29 @@ class KommunikationSyncManager:
|
||||
# ========== HELPER METHODS ==========
|
||||
|
||||
async def _create_empty_slot(self, betnr: int, advo_komm: Dict) -> None:
|
||||
"""Erstellt leeren Slot für gelöschten Eintrag"""
|
||||
"""
|
||||
Erstellt leeren Slot für gelöschten Eintrag
|
||||
|
||||
Verwendet für:
|
||||
- Var2: In EspoCRM gelöscht (hat Marker)
|
||||
- Var4 bei Konflikt: Neu in Advoware aber EspoCRM wins (hat KEINEN Marker)
|
||||
"""
|
||||
try:
|
||||
komm_id = advo_komm['id']
|
||||
tlf = (advo_komm.get('tlf') or '').strip()
|
||||
bemerkung = advo_komm.get('bemerkung') or ''
|
||||
marker = parse_marker(bemerkung)
|
||||
|
||||
if not marker:
|
||||
self.logger.warning(f"[KOMM] Kein Marker gefunden für gelöschten Eintrag: {komm_id}")
|
||||
return
|
||||
# Bestimme kommKz
|
||||
if marker:
|
||||
# Hat Marker (Var2)
|
||||
kommkz = marker['kommKz']
|
||||
else:
|
||||
# Kein Marker (Var4 bei Konflikt) - erkenne kommKz aus Wert
|
||||
from services.kommunikation_mapper import detect_kommkz
|
||||
kommkz = detect_kommkz(tlf) if tlf else 1 # Default: TelGesch
|
||||
self.logger.info(f"[KOMM] Var4 ohne Marker: erkenne kommKz={kommkz} aus Wert '{tlf[:20]}...'")
|
||||
|
||||
kommkz = marker['kommKz']
|
||||
slot_marker = create_slot_marker(kommkz)
|
||||
|
||||
update_data = {
|
||||
|
||||
Reference in New Issue
Block a user