@@ -39,7 +39,7 @@ class KommunikationSyncManager:
# ========== BIDIRECTIONAL SYNC ==========
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
@@ -53,6 +53,7 @@ class KommunikationSyncManager:
Args:
direction: ' both ' , ' to_espocrm ' , ' to_advoware '
force_espo_wins: Erzwingt EspoCRM-wins Konfliktlösung (für Stammdaten-Konflikte)
Returns:
Combined results mit detaillierten Änderungen
@@ -102,13 +103,28 @@ class KommunikationSyncManager:
# ========== 3-WAY DIFFING MIT HASH-BASIERTER KONFLIKT-ERKENNUNG ==========
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 )
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, "
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 " )
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 ==========
@@ -145,6 +161,12 @@ class KommunikationSyncManager:
for komm in diff [ ' advo_new ' ] :
await self . _create_empty_slot ( betnr , komm )
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)
if sync_to_advoware :
@@ -594,14 +616,69 @@ class KommunikationSyncManager:
try :
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 ' ] :
komm_id = komm . get ( ' id ' )
tlf = ( komm . get ( ' tlf ' ) or ' ' ) . strip ( )
self . logger . info ( f " [KOMM] 🗑️ Var2: Deleted in EspoCRM - komm_id= { komm_id } , value= ' { tlf [ : 30 ] } ... ' " )
await self . _create_empty_slot ( betnr , komm )
self . logger . info ( f " [KOMM] ✅ E mpty slot created for komm_id= { komm_id } " )
result [ ' deleted ' ] + = 1
if komm_id not in matched_var2_ids :
synced_value = komm . get ( ' _synced_value ' , ' ' )
self . logger . info ( f " [KOMM] 🗑️ Var2: Deleted in EspoCRM - komm_id= { komm_id } , synced_value= ' { synced_value [ : 30 ] } ... ' " )
await self . _create_e mpty_ slot( betnr , komm , synced_value = synced_value )
result [ ' deleted ' ] + = 1
# Var5: In EspoCRM geändert (z.B. primary Flag)
for value , advo_komm , espo_item in diff [ ' espo_changed ' ] :
@@ -630,12 +707,16 @@ class KommunikationSyncManager:
result [ ' updated ' ] + = 1
# Var1: Neu in EspoCRM → Create oder reuse Slot in Advoware
for value , espo_item in diff [ ' espo_new ' ] :
self . logger . info ( f " [KOMM] ➕ Var1: New in EspoCRM ' { value [ : 30 ] } ... ' , type= { espo_item . get ( ' type ' ) } " )
# Erkenne kommKz mit espo_type
# Überspringe bereits gematchte Einträge (Var2+Var1 merged)
for idx , ( value , espo_item ) in enumerate ( 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 )
# 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 } " )
# Suche leeren Slot
@@ -673,10 +754,15 @@ class KommunikationSyncManager:
# ========== 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
Args:
betnr: Beteiligten-Nummer
advo_komm: Kommunikations-Eintrag aus Advoware
synced_value: Optional - Original-Wert aus EspoCRM (nur für Logging)
Verwendet für:
- Var2: In EspoCRM gelöscht (hat 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 )
update_data = {
' tlf ' : ' ' ,
' tlf ' : ' ' , # Empty Slot = leerer Wert
' bemerkung ' : slot_marker ,
' online ' : False
}
log_value = synced_value if synced_value else tlf
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 :
import traceback