Compare commits
2 Commits
fa45aab5a9
...
8dc699ec9e
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dc699ec9e | |||
| af00495cee |
@@ -39,7 +39,7 @@ class KommunikationSyncManager:
|
|||||||
# ========== BIDIRECTIONAL SYNC ==========
|
# ========== BIDIRECTIONAL SYNC ==========
|
||||||
|
|
||||||
async def sync_bidirectional(self, beteiligte_id: str, betnr: int,
|
async def sync_bidirectional(self, beteiligte_id: str, betnr: int,
|
||||||
direction: str = 'both') -> Dict[str, Any]:
|
direction: str = 'both', force_espo_wins: bool = False) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Bidirektionale Synchronisation mit intelligentem Diffing
|
Bidirektionale Synchronisation mit intelligentem Diffing
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ class KommunikationSyncManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
direction: 'both', 'to_espocrm', 'to_advoware'
|
direction: 'both', 'to_espocrm', 'to_advoware'
|
||||||
|
force_espo_wins: Erzwingt EspoCRM-wins Konfliktlösung (für Stammdaten-Konflikte)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Combined results mit detaillierten Änderungen
|
Combined results mit detaillierten Änderungen
|
||||||
@@ -102,13 +103,28 @@ class KommunikationSyncManager:
|
|||||||
# ========== 3-WAY DIFFING MIT HASH-BASIERTER KONFLIKT-ERKENNUNG ==========
|
# ========== 3-WAY DIFFING MIT HASH-BASIERTER KONFLIKT-ERKENNUNG ==========
|
||||||
diff = self._compute_diff(advo_kommunikationen, espo_emails, espo_phones, advo_bet, espo_bet)
|
diff = self._compute_diff(advo_kommunikationen, espo_emails, espo_phones, advo_bet, espo_bet)
|
||||||
|
|
||||||
|
# WICHTIG: force_espo_wins überschreibt den Hash-basierten Konflikt-Check
|
||||||
|
if force_espo_wins:
|
||||||
|
diff['espo_wins'] = True
|
||||||
|
self.logger.info(f"[KOMM] ⚠️ force_espo_wins=True → EspoCRM WINS (override)")
|
||||||
|
|
||||||
|
# Konvertiere Var3 (advo_deleted) → Var1 (espo_new)
|
||||||
|
# Bei Konflikt müssen gelöschte Advoware-Einträge wiederhergestellt werden
|
||||||
|
if diff['advo_deleted']:
|
||||||
|
self.logger.info(f"[KOMM] 🔄 Converting {len(diff['advo_deleted'])} Var3→Var1 (force EspoCRM wins)")
|
||||||
|
for value, espo_item in diff['advo_deleted']:
|
||||||
|
diff['espo_new'].append((value, espo_item))
|
||||||
|
diff['advo_deleted'] = [] # Leeren, da jetzt als Var1 behandelt
|
||||||
|
|
||||||
espo_wins = diff.get('espo_wins', False)
|
espo_wins = diff.get('espo_wins', False)
|
||||||
|
|
||||||
self.logger.info(f"[KOMM] ===== DIFF RESULTS =====")
|
self.logger.info(f"[KOMM] ===== DIFF RESULTS =====")
|
||||||
self.logger.info(f"[KOMM] Diff: {len(diff['advo_changed'])} Advoware changed, {len(diff['espo_changed'])} EspoCRM changed, "
|
self.logger.info(f"[KOMM] Diff: {len(diff['advo_changed'])} Advoware changed, {len(diff['espo_changed'])} EspoCRM changed, "
|
||||||
f"{len(diff['advo_new'])} Advoware new, {len(diff['espo_new'])} EspoCRM new, "
|
f"{len(diff['advo_new'])} Advoware new, {len(diff['espo_new'])} EspoCRM new, "
|
||||||
f"{len(diff['advo_deleted'])} Advoware deleted, {len(diff['espo_deleted'])} EspoCRM deleted")
|
f"{len(diff['advo_deleted'])} Advoware deleted, {len(diff['espo_deleted'])} EspoCRM deleted")
|
||||||
self.logger.info(f"[KOMM] ===== CONFLICT STATUS: espo_wins={espo_wins} =====")
|
|
||||||
|
force_status = " (force=True)" if force_espo_wins else ""
|
||||||
|
self.logger.info(f"[KOMM] ===== CONFLICT STATUS: espo_wins={espo_wins}{force_status} =====")
|
||||||
|
|
||||||
# ========== APPLY CHANGES ==========
|
# ========== APPLY CHANGES ==========
|
||||||
|
|
||||||
@@ -146,6 +162,12 @@ class KommunikationSyncManager:
|
|||||||
await self._create_empty_slot(betnr, komm)
|
await self._create_empty_slot(betnr, komm)
|
||||||
result['espocrm_to_advoware']['deleted'] += 1
|
result['espocrm_to_advoware']['deleted'] += 1
|
||||||
|
|
||||||
|
# Var3: Wiederherstellung gelöschter Einträge (kein separater Code nötig)
|
||||||
|
# → Wird über Var1 in _apply_espocrm_to_advoware behandelt
|
||||||
|
# Die gelöschten Einträge sind noch in EspoCRM vorhanden und werden als "espo_new" erkannt
|
||||||
|
if len(diff['advo_deleted']) > 0:
|
||||||
|
self.logger.info(f"[KOMM] ℹ️ {len(diff['advo_deleted'])} Var3 entries (deleted in Advoware) will be restored via espo_new")
|
||||||
|
|
||||||
# 2. EspoCRM → Advoware (Var1: Neu in EspoCRM, Var2: Gelöscht in EspoCRM, Var5: Geändert in EspoCRM)
|
# 2. EspoCRM → Advoware (Var1: Neu in EspoCRM, Var2: Gelöscht in EspoCRM, Var5: Geändert in EspoCRM)
|
||||||
if sync_to_advoware:
|
if sync_to_advoware:
|
||||||
advo_result = await self._apply_espocrm_to_advoware(
|
advo_result = await self._apply_espocrm_to_advoware(
|
||||||
@@ -594,14 +616,69 @@ class KommunikationSyncManager:
|
|||||||
try:
|
try:
|
||||||
advo_kommunikationen = advo_bet.get('kommunikation', [])
|
advo_kommunikationen = advo_bet.get('kommunikation', [])
|
||||||
|
|
||||||
# Var2: In EspoCRM gelöscht → Empty Slot in Advoware
|
# OPTIMIERUNG: Matche Var2 (Delete) + Var1 (New) mit gleichem kommKz
|
||||||
|
# → Direkt UPDATE statt DELETE+RELOAD+CREATE
|
||||||
|
var2_by_kommkz = {} # kommKz → [komm, ...]
|
||||||
|
var1_by_kommkz = {} # kommKz → [(value, espo_item), ...]
|
||||||
|
|
||||||
|
# Gruppiere Var2 nach kommKz
|
||||||
|
for komm in diff['espo_deleted']:
|
||||||
|
bemerkung = komm.get('bemerkung') or ''
|
||||||
|
marker = parse_marker(bemerkung)
|
||||||
|
if marker:
|
||||||
|
kommkz = marker['kommKz']
|
||||||
|
if kommkz not in var2_by_kommkz:
|
||||||
|
var2_by_kommkz[kommkz] = []
|
||||||
|
var2_by_kommkz[kommkz].append(komm)
|
||||||
|
|
||||||
|
# Gruppiere Var1 nach kommKz
|
||||||
|
for value, espo_item in diff['espo_new']:
|
||||||
|
espo_type = espo_item.get('type', 'email' if '@' in value else None)
|
||||||
|
kommkz = detect_kommkz(value, advo_bet, espo_type=espo_type)
|
||||||
|
if kommkz not in var1_by_kommkz:
|
||||||
|
var1_by_kommkz[kommkz] = []
|
||||||
|
var1_by_kommkz[kommkz].append((value, espo_item))
|
||||||
|
|
||||||
|
# Matche und führe direkte Updates aus
|
||||||
|
matched_var2_ids = set()
|
||||||
|
matched_var1_indices = {} # kommkz → set of matched indices
|
||||||
|
|
||||||
|
for kommkz in var2_by_kommkz.keys():
|
||||||
|
if kommkz in var1_by_kommkz:
|
||||||
|
var2_list = var2_by_kommkz[kommkz]
|
||||||
|
var1_list = var1_by_kommkz[kommkz]
|
||||||
|
|
||||||
|
# Matche paarweise
|
||||||
|
for i, (value, espo_item) in enumerate(var1_list):
|
||||||
|
if i < len(var2_list):
|
||||||
|
komm = var2_list[i]
|
||||||
|
komm_id = komm['id']
|
||||||
|
|
||||||
|
self.logger.info(f"[KOMM] 🔄 Var2+Var1 Match: kommKz={kommkz}, updating slot {komm_id} with '{value[:30]}...'")
|
||||||
|
|
||||||
|
# Direktes UPDATE statt DELETE+CREATE
|
||||||
|
await self.advoware.update_kommunikation(betnr, komm_id, {
|
||||||
|
'tlf': value,
|
||||||
|
'online': espo_item['primary'],
|
||||||
|
'bemerkung': create_marker(value, kommkz)
|
||||||
|
})
|
||||||
|
|
||||||
|
matched_var2_ids.add(komm_id)
|
||||||
|
if kommkz not in matched_var1_indices:
|
||||||
|
matched_var1_indices[kommkz] = set()
|
||||||
|
matched_var1_indices[kommkz].add(i)
|
||||||
|
|
||||||
|
result['created'] += 1
|
||||||
|
self.logger.info(f"[KOMM] ✅ Slot updated (optimized merge)")
|
||||||
|
|
||||||
|
# Unmatched Var2: Erstelle Empty Slots
|
||||||
for komm in diff['espo_deleted']:
|
for komm in diff['espo_deleted']:
|
||||||
komm_id = komm.get('id')
|
komm_id = komm.get('id')
|
||||||
tlf = (komm.get('tlf') or '').strip()
|
if komm_id not in matched_var2_ids:
|
||||||
self.logger.info(f"[KOMM] 🗑️ Var2: Deleted in EspoCRM - komm_id={komm_id}, value='{tlf[:30]}...'")
|
synced_value = komm.get('_synced_value', '')
|
||||||
await self._create_empty_slot(betnr, komm)
|
self.logger.info(f"[KOMM] 🗑️ Var2: Deleted in EspoCRM - komm_id={komm_id}, synced_value='{synced_value[:30]}...'")
|
||||||
self.logger.info(f"[KOMM] ✅ Empty slot created for komm_id={komm_id}")
|
await self._create_empty_slot(betnr, komm, synced_value=synced_value)
|
||||||
result['deleted'] += 1
|
result['deleted'] += 1
|
||||||
|
|
||||||
# Var5: In EspoCRM geändert (z.B. primary Flag)
|
# Var5: In EspoCRM geändert (z.B. primary Flag)
|
||||||
for value, advo_komm, espo_item in diff['espo_changed']:
|
for value, advo_komm, espo_item in diff['espo_changed']:
|
||||||
@@ -630,12 +707,16 @@ class KommunikationSyncManager:
|
|||||||
result['updated'] += 1
|
result['updated'] += 1
|
||||||
|
|
||||||
# Var1: Neu in EspoCRM → Create oder reuse Slot in Advoware
|
# Var1: Neu in EspoCRM → Create oder reuse Slot in Advoware
|
||||||
for value, espo_item in diff['espo_new']:
|
# Überspringe bereits gematchte Einträge (Var2+Var1 merged)
|
||||||
self.logger.info(f"[KOMM] ➕ Var1: New in EspoCRM '{value[:30]}...', type={espo_item.get('type')}")
|
for idx, (value, espo_item) in enumerate(diff['espo_new']):
|
||||||
|
|
||||||
# Erkenne kommKz mit espo_type
|
|
||||||
espo_type = espo_item.get('type', 'email' if '@' in value else None)
|
espo_type = espo_item.get('type', 'email' if '@' in value else None)
|
||||||
kommkz = detect_kommkz(value, advo_bet, espo_type=espo_type)
|
kommkz = detect_kommkz(value, advo_bet, espo_type=espo_type)
|
||||||
|
|
||||||
|
# Skip wenn bereits als Var2+Var1 Match verarbeitet
|
||||||
|
if kommkz in matched_var1_indices and idx in matched_var1_indices[kommkz]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.info(f"[KOMM] ➕ Var1: New in EspoCRM '{value[:30]}...', type={espo_item.get('type')}")
|
||||||
self.logger.info(f"[KOMM] 🔍 kommKz detected: espo_type={espo_type}, kommKz={kommkz}")
|
self.logger.info(f"[KOMM] 🔍 kommKz detected: espo_type={espo_type}, kommKz={kommkz}")
|
||||||
|
|
||||||
# Suche leeren Slot
|
# Suche leeren Slot
|
||||||
@@ -673,10 +754,15 @@ class KommunikationSyncManager:
|
|||||||
|
|
||||||
# ========== HELPER METHODS ==========
|
# ========== HELPER METHODS ==========
|
||||||
|
|
||||||
async def _create_empty_slot(self, betnr: int, advo_komm: Dict) -> None:
|
async def _create_empty_slot(self, betnr: int, advo_komm: Dict, synced_value: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
Erstellt leeren Slot für gelöschten Eintrag
|
Erstellt leeren Slot für gelöschten Eintrag
|
||||||
|
|
||||||
|
Args:
|
||||||
|
betnr: Beteiligten-Nummer
|
||||||
|
advo_komm: Kommunikations-Eintrag aus Advoware
|
||||||
|
synced_value: Optional - Original-Wert aus EspoCRM (nur für Logging)
|
||||||
|
|
||||||
Verwendet für:
|
Verwendet für:
|
||||||
- Var2: In EspoCRM gelöscht (hat Marker)
|
- Var2: In EspoCRM gelöscht (hat Marker)
|
||||||
- Var4 bei Konflikt: Neu in Advoware aber EspoCRM wins (hat KEINEN Marker)
|
- Var4 bei Konflikt: Neu in Advoware aber EspoCRM wins (hat KEINEN Marker)
|
||||||
@@ -700,13 +786,14 @@ class KommunikationSyncManager:
|
|||||||
slot_marker = create_slot_marker(kommkz)
|
slot_marker = create_slot_marker(kommkz)
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
'tlf': '',
|
'tlf': '', # Empty Slot = leerer Wert
|
||||||
'bemerkung': slot_marker,
|
'bemerkung': slot_marker,
|
||||||
'online': False
|
'online': False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_value = synced_value if synced_value else tlf
|
||||||
await self.advoware.update_kommunikation(betnr, komm_id, update_data)
|
await self.advoware.update_kommunikation(betnr, komm_id, update_data)
|
||||||
self.logger.info(f"[KOMM] ✅ Created empty slot: komm_id={komm_id}, kommKz={kommkz}")
|
self.logger.info(f"[KOMM] ✅ Created empty slot: komm_id={komm_id}, kommKz={kommkz}, original_value='{log_value[:30]}...'")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
@@ -158,19 +158,20 @@ async def handler(event_data, context):
|
|||||||
context.logger.error(traceback.format_exc())
|
context.logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
async def run_kommunikation_sync(entity_id: str, betnr: int, komm_sync, context, direction: str = 'both') -> Dict[str, Any]:
|
async def run_kommunikation_sync(entity_id: str, betnr: int, komm_sync, context, direction: str = 'both', force_espo_wins: bool = False) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Helper: Führt Kommunikation-Sync aus mit Error-Handling
|
Helper: Führt Kommunikation-Sync aus mit Error-Handling
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
direction: 'both' (bidirektional), 'to_advoware' (nur EspoCRM→Advoware), 'to_espocrm' (nur Advoware→EspoCRM)
|
direction: 'both' (bidirektional), 'to_advoware' (nur EspoCRM→Advoware), 'to_espocrm' (nur Advoware→EspoCRM)
|
||||||
|
force_espo_wins: Erzwingt EspoCRM-wins Konfliktlösung (für Stammdaten-Konflikte)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Sync-Ergebnis oder None bei Fehler
|
Sync-Ergebnis oder None bei Fehler
|
||||||
"""
|
"""
|
||||||
context.logger.info(f"📞 Starte Kommunikation-Sync (direction={direction})...")
|
context.logger.info(f"📞 Starte Kommunikation-Sync (direction={direction})...")
|
||||||
try:
|
try:
|
||||||
komm_result = await komm_sync.sync_bidirectional(entity_id, betnr, direction=direction)
|
komm_result = await komm_sync.sync_bidirectional(entity_id, betnr, direction=direction, force_espo_wins=force_espo_wins)
|
||||||
context.logger.info(f"✅ Kommunikation synced: {komm_result}")
|
context.logger.info(f"✅ Kommunikation synced: {komm_result}")
|
||||||
return komm_result
|
return komm_result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -419,7 +420,7 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
)
|
)
|
||||||
|
|
||||||
# KOMMUNIKATION SYNC: NUR EspoCRM→Advoware (EspoCRM wins!)
|
# KOMMUNIKATION SYNC: NUR EspoCRM→Advoware (EspoCRM wins!)
|
||||||
await run_kommunikation_sync(entity_id, betnr, komm_sync, context, direction='to_advoware')
|
await run_kommunikation_sync(entity_id, betnr, komm_sync, context, direction='to_advoware', force_espo_wins=True)
|
||||||
|
|
||||||
# Release Lock NACH Kommunikation-Sync
|
# Release Lock NACH Kommunikation-Sync
|
||||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
await sync_utils.release_sync_lock(entity_id, 'clean')
|
||||||
|
|||||||
Reference in New Issue
Block a user