- Added KommunikationSyncManager class to handle synchronization logic. - Implemented methods for loading data, computing diffs, and applying changes between Advoware and EspoCRM. - Introduced 3-way diffing mechanism to intelligently resolve conflicts. - Added helper methods for creating empty slots and detecting changes in communications. - Enhanced logging for better traceability during synchronization processes.
396 lines
14 KiB
Python
396 lines
14 KiB
Python
"""
|
|
Matching-Strategie für Kommunikation ohne ID
|
|
|
|
Problem:
|
|
- emailAddressData und phoneNumberData haben KEINE id-Felder
|
|
- Können keine rowId in EspoCRM speichern (keine Custom-Felder)
|
|
- Wie matchen wir Advoware ↔ EspoCRM?
|
|
|
|
Lösungsansätze:
|
|
1. Wert-basiertes Matching (emailAddress/phoneNumber als Schlüssel)
|
|
2. Advoware als Master (One-Way-Sync mit Neuanlage bei Änderung)
|
|
3. Hash-basiertes Matching in bemerkung-Feld
|
|
4. Position-basiertes Matching (primary-Flag)
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
from services.espocrm import EspoCRMAPI
|
|
from services.advoware import AdvowareAPI
|
|
|
|
TEST_BETEILIGTE_ID = '68e4af00172be7924'
|
|
TEST_ADVOWARE_BETNR = 104860
|
|
|
|
|
|
class SimpleContext:
|
|
class Logger:
|
|
def info(self, msg): print(f"[INFO] {msg}")
|
|
def error(self, msg): print(f"[ERROR] {msg}")
|
|
def warning(self, msg): print(f"[WARN] {msg}")
|
|
def debug(self, msg): pass # Suppress debug
|
|
|
|
def __init__(self):
|
|
self.logger = self.Logger()
|
|
|
|
|
|
def print_section(title):
|
|
print("\n" + "="*70)
|
|
print(title)
|
|
print("="*70)
|
|
|
|
|
|
async def test_value_based_matching():
|
|
"""
|
|
Strategie 1: Wert-basiertes Matching
|
|
|
|
Idee: Verwende emailAddress/phoneNumber selbst als Schlüssel
|
|
|
|
Vorteile:
|
|
- Einfach zu implementieren
|
|
- Funktioniert für Duplikats-Erkennung
|
|
|
|
Nachteile:
|
|
- Wenn Wert ändert, verlieren wir Verbindung
|
|
- Keine Change-Detection möglich (kein Timestamp/rowId)
|
|
"""
|
|
print_section("STRATEGIE 1: Wert-basiertes Matching")
|
|
|
|
context = SimpleContext()
|
|
espo = EspoCRMAPI(context)
|
|
advo = AdvowareAPI(context)
|
|
|
|
# Hole Daten
|
|
espo_entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
|
|
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
|
|
|
|
print("\n📧 EspoCRM Emails:")
|
|
espo_emails = {e['emailAddress']: e for e in espo_entity.get('emailAddressData', [])}
|
|
for email, data in espo_emails.items():
|
|
print(f" • {email:40s} primary={data.get('primary', False)}")
|
|
|
|
print("\n📧 Advoware Kommunikation (Typ MailGesch=4, MailPrivat=8):")
|
|
advo_komm = advo_entity.get('kommunikation', [])
|
|
advo_emails = [k for k in advo_komm if k.get('kommKz') in [4, 8]] # Email-Typen
|
|
for k in advo_emails:
|
|
print(f" • {k.get('tlf', ''):40s} Typ={k.get('kommKz')} ID={k.get('id')} "
|
|
f"rowId={k.get('rowId')}")
|
|
|
|
print("\n🔍 Matching-Ergebnis:")
|
|
matched = []
|
|
unmatched_espo = []
|
|
unmatched_advo = []
|
|
|
|
for advo_k in advo_emails:
|
|
email_value = advo_k.get('tlf', '').strip()
|
|
if email_value in espo_emails:
|
|
matched.append((advo_k, espo_emails[email_value]))
|
|
print(f" ✅ MATCH: {email_value}")
|
|
else:
|
|
unmatched_advo.append(advo_k)
|
|
print(f" ❌ Nur in Advoware: {email_value}")
|
|
|
|
for email_value in espo_emails:
|
|
if not any(k.get('tlf', '').strip() == email_value for k in advo_emails):
|
|
unmatched_espo.append(espo_emails[email_value])
|
|
print(f" ⚠️ Nur in EspoCRM: {email_value}")
|
|
|
|
print(f"\n📊 Statistik:")
|
|
print(f" • Matched: {len(matched)}")
|
|
print(f" • Nur Advoware: {len(unmatched_advo)}")
|
|
print(f" • Nur EspoCRM: {len(unmatched_espo)}")
|
|
|
|
# Problem-Szenario: Was wenn Email-Adresse ändert?
|
|
print("\n⚠️ PROBLEM-SZENARIO: Email-Adresse ändert")
|
|
print(" 1. Advoware: max@old.de → max@new.de (UPDATE mit gleicher ID)")
|
|
print(" 2. Wert-Matching findet max@old.de nicht mehr in EspoCRM")
|
|
print(" 3. Sync würde max@new.de NEU anlegen statt UPDATE")
|
|
print(" 4. Ergebnis: Duplikat (max@old.de + max@new.de)")
|
|
|
|
return matched, unmatched_advo, unmatched_espo
|
|
|
|
|
|
async def test_advoware_master_sync():
|
|
"""
|
|
Strategie 2: Advoware als Master (One-Way-Sync)
|
|
|
|
Idee:
|
|
- Ignoriere EspoCRM-Änderungen
|
|
- Bei jedem Sync: Überschreibe komplette Arrays in EspoCRM
|
|
|
|
Vorteile:
|
|
- Sehr einfach
|
|
- Keine Change-Detection nötig
|
|
- Keine Matching-Probleme
|
|
|
|
Nachteile:
|
|
- Verliert EspoCRM-Änderungen
|
|
- Nicht bidirektional
|
|
"""
|
|
print_section("STRATEGIE 2: Advoware als Master (One-Way)")
|
|
|
|
context = SimpleContext()
|
|
advo = AdvowareAPI(context)
|
|
|
|
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
|
|
advo_komm = advo_entity.get('kommunikation', [])
|
|
|
|
print("\n📋 Sync-Ablauf:")
|
|
print(" 1. Hole alle Advoware Kommunikationen")
|
|
print(" 2. Konvertiere zu EspoCRM Format:")
|
|
|
|
# Konvertierung
|
|
email_data = []
|
|
phone_data = []
|
|
|
|
for k in advo_komm:
|
|
komm_kz = k.get('kommKz', 0)
|
|
wert = k.get('tlf', '').strip()
|
|
online = k.get('online', False)
|
|
|
|
# Email-Typen: 4=MailGesch, 8=MailPrivat, 11=EPost
|
|
if komm_kz in [4, 8, 11] and wert:
|
|
email_data.append({
|
|
'emailAddress': wert,
|
|
'lower': wert.lower(),
|
|
'primary': online, # online=true → primary
|
|
'optOut': False,
|
|
'invalid': False
|
|
})
|
|
# Phone-Typen: 1=TelGesch, 2=FaxGesch, 3=Mobil, 6=TelPrivat, 7=FaxPrivat
|
|
elif komm_kz in [1, 2, 3, 6, 7] and wert:
|
|
# Mapping kommKz → EspoCRM type
|
|
type_map = {
|
|
1: 'Office', # TelGesch
|
|
3: 'Mobile', # Mobil
|
|
6: 'Home', # TelPrivat
|
|
2: 'Fax', # FaxGesch
|
|
7: 'Fax', # FaxPrivat
|
|
}
|
|
phone_data.append({
|
|
'phoneNumber': wert,
|
|
'type': type_map.get(komm_kz, 'Other'),
|
|
'primary': online,
|
|
'optOut': False,
|
|
'invalid': False
|
|
})
|
|
|
|
print(f"\n 📧 {len(email_data)} Emails:")
|
|
for e in email_data:
|
|
print(f" • {e['emailAddress']:40s} primary={e['primary']}")
|
|
|
|
print(f"\n 📞 {len(phone_data)} Phones:")
|
|
for p in phone_data:
|
|
print(f" • {p['phoneNumber']:40s} type={p['type']:10s} primary={p['primary']}")
|
|
|
|
print("\n 3. UPDATE CBeteiligte (überschreibt komplette Arrays)")
|
|
print(" → emailAddressData: [...]")
|
|
print(" → phoneNumberData: [...]")
|
|
|
|
print("\n✅ Vorteile:")
|
|
print(" • Sehr einfach zu implementieren")
|
|
print(" • Keine Matching-Logik erforderlich")
|
|
print(" • Advoware ist immer Source of Truth")
|
|
|
|
print("\n❌ Nachteile:")
|
|
print(" • EspoCRM-Änderungen gehen verloren")
|
|
print(" • Nicht bidirektional")
|
|
print(" • User könnten verärgert sein")
|
|
|
|
return email_data, phone_data
|
|
|
|
|
|
async def test_hybrid_strategy():
|
|
"""
|
|
Strategie 3: Hybrid - Advoware Master + EspoCRM Ergänzungen
|
|
|
|
Idee:
|
|
- Advoware-Kommunikationen sind primary=true (wichtig, geschützt)
|
|
- EspoCRM kann zusätzliche Einträge mit primary=false hinzufügen
|
|
- Nur Advoware-Einträge werden synchronisiert
|
|
|
|
Vorteile:
|
|
- Flexibilität für EspoCRM-User
|
|
- Advoware behält Kontrolle über wichtige Daten
|
|
|
|
Nachteile:
|
|
- Komplexere Logik
|
|
- Braucht Markierung (primary-Flag)
|
|
"""
|
|
print_section("STRATEGIE 3: Hybrid (Advoware Primary + EspoCRM Secondary)")
|
|
|
|
context = SimpleContext()
|
|
espo = EspoCRMAPI(context)
|
|
advo = AdvowareAPI(context)
|
|
|
|
# Hole Daten
|
|
espo_entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
|
|
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
|
|
|
|
advo_komm = advo_entity.get('kommunikation', [])
|
|
espo_emails = espo_entity.get('emailAddressData', [])
|
|
|
|
print("\n📋 Regel:")
|
|
print(" • primary=true → Kommt von Advoware (synchronisiert)")
|
|
print(" • primary=false → Nur in EspoCRM (wird NICHT zu Advoware)")
|
|
|
|
print("\n📧 Aktuelle EspoCRM Emails:")
|
|
for e in espo_emails:
|
|
source = "Advoware" if e.get('primary') else "EspoCRM"
|
|
print(f" • {e['emailAddress']:40s} primary={e.get('primary')} → {source}")
|
|
|
|
print("\n🔄 Sync-Logik:")
|
|
print(" 1. Hole Advoware Kommunikationen")
|
|
print(" 2. Konvertiere zu EspoCRM (mit primary=true)")
|
|
print(" 3. Hole aktuelle EspoCRM Einträge mit primary=false")
|
|
print(" 4. Merge: Advoware (primary) + EspoCRM (secondary)")
|
|
print(" 5. UPDATE CBeteiligte mit gemergtem Array")
|
|
|
|
# Simulation
|
|
advo_emails = [k for k in advo_komm if k.get('kommKz') in [4, 8, 11]]
|
|
|
|
merged_emails = []
|
|
|
|
# Von Advoware (primary=true)
|
|
for k in advo_emails:
|
|
merged_emails.append({
|
|
'emailAddress': k.get('tlf', ''),
|
|
'lower': k.get('tlf', '').lower(),
|
|
'primary': True, # Immer primary für Advoware
|
|
'optOut': False,
|
|
'invalid': False
|
|
})
|
|
|
|
# Von EspoCRM (nur non-primary behalten)
|
|
for e in espo_emails:
|
|
if not e.get('primary', False):
|
|
merged_emails.append(e)
|
|
|
|
print(f"\n📊 Merge-Ergebnis: {len(merged_emails)} Emails")
|
|
for e in merged_emails:
|
|
source = "Advoware" if e.get('primary') else "EspoCRM"
|
|
print(f" • {e['emailAddress']:40s} [{source}]")
|
|
|
|
print("\n✅ Vorteile:")
|
|
print(" • Advoware behält Kontrolle")
|
|
print(" • EspoCRM-User können ergänzen")
|
|
print(" • Kein Datenverlust")
|
|
|
|
print("\n⚠️ Einschränkungen:")
|
|
print(" • EspoCRM kann Advoware-Daten NICHT ändern")
|
|
print(" • primary-Flag muss geschützt werden")
|
|
|
|
return merged_emails
|
|
|
|
|
|
async def test_bemerkung_tracking():
|
|
"""
|
|
Strategie 4: Tracking via bemerkung-Feld
|
|
|
|
Idee: Speichere Advoware-ID in bemerkung
|
|
|
|
Format: "Advoware-ID: 149331 | Tatsächliche Bemerkung"
|
|
|
|
Vorteile:
|
|
- Stabiles Matching möglich
|
|
- Kann Änderungen tracken
|
|
|
|
Nachteile:
|
|
- bemerkung-Feld wird "verschmutzt"
|
|
- User sichtbar
|
|
- Fragil (User könnte löschen)
|
|
"""
|
|
print_section("STRATEGIE 4: Tracking via bemerkung-Feld")
|
|
|
|
context = SimpleContext()
|
|
advo = AdvowareAPI(context)
|
|
|
|
advo_entity = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_ADVOWARE_BETNR}')
|
|
advo_komm = advo_entity.get('kommunikation', [])
|
|
|
|
print("\n⚠️ PROBLEM: EspoCRM emailAddressData/phoneNumberData haben KEIN bemerkung-Feld!")
|
|
print("\nStruktur emailAddressData:")
|
|
print(" {")
|
|
print(" 'emailAddress': 'max@example.com',")
|
|
print(" 'lower': 'max@example.com',")
|
|
print(" 'primary': true,")
|
|
print(" 'optOut': false,")
|
|
print(" 'invalid': false")
|
|
print(" }")
|
|
print("\n ❌ Kein 'bemerkung' oder 'notes' Feld verfügbar")
|
|
print(" ❌ Kein Custom-Feld möglich in Standard-Arrays")
|
|
|
|
print("\n📋 Alternative: Advoware bemerkung nutzen")
|
|
print(" → Speichere EspoCRM-Wert in Advoware bemerkung")
|
|
|
|
for k in advo_komm[:3]: # Erste 3 als Beispiel
|
|
advo_id = k.get('id')
|
|
wert = k.get('tlf', '')
|
|
bemerkung = k.get('bemerkung', '')
|
|
|
|
print(f"\n Advoware ID {advo_id}:")
|
|
print(f" Wert: {wert}")
|
|
print(f" Bemerkung: {bemerkung or '(leer)'}")
|
|
print(f" → Neue Bemerkung: 'EspoCRM: {wert} | {bemerkung}'")
|
|
|
|
print("\n✅ Matching-Strategie:")
|
|
print(" 1. Parse bemerkung: Extrahiere 'EspoCRM: <wert>'")
|
|
print(" 2. Matche Advoware ↔ EspoCRM via Wert in bemerkung")
|
|
print(" 3. Wenn Wert ändert: Update bemerkung")
|
|
|
|
print("\n❌ Nachteile:")
|
|
print(" • bemerkung für User sichtbar und änderbar")
|
|
print(" • Fragil wenn User bemerkung bearbeitet")
|
|
print(" • Komplexe Parse-Logik")
|
|
|
|
|
|
async def main():
|
|
print("\n" + "="*70)
|
|
print("KOMMUNIKATION MATCHING-STRATEGIEN OHNE ID")
|
|
print("="*70)
|
|
|
|
try:
|
|
# Test alle Strategien
|
|
await test_value_based_matching()
|
|
await test_advoware_master_sync()
|
|
await test_hybrid_strategy()
|
|
await test_bemerkung_tracking()
|
|
|
|
print_section("EMPFEHLUNG")
|
|
|
|
print("\n🎯 BESTE LÖSUNG: Strategie 3 (Hybrid)")
|
|
print("\n✅ Begründung:")
|
|
print(" 1. Advoware behält Kontrolle (primary=true)")
|
|
print(" 2. EspoCRM kann ergänzen (primary=false)")
|
|
print(" 3. Einfach zu implementieren")
|
|
print(" 4. Kein Datenverlust")
|
|
print(" 5. primary-Flag ist Standard in EspoCRM")
|
|
|
|
print("\n📋 Implementation:")
|
|
print(" • Advoware → EspoCRM: Setze primary=true")
|
|
print(" • EspoCRM → Advoware: Ignoriere primary=false Einträge")
|
|
print(" • Matching: Via Wert (emailAddress/phoneNumber)")
|
|
print(" • Change Detection: rowId in Advoware (wie bei Adressen)")
|
|
|
|
print("\n🔄 Sync-Ablauf:")
|
|
print(" 1. Webhook von Advoware")
|
|
print(" 2. Lade Advoware Kommunikationen")
|
|
print(" 3. Filter: Nur Typen die EspoCRM unterstützt")
|
|
print(" 4. Konvertiere zu emailAddressData/phoneNumberData")
|
|
print(" 5. Setze primary=true für alle")
|
|
print(" 6. Merge mit bestehenden primary=false Einträgen")
|
|
print(" 7. UPDATE CBeteiligte")
|
|
|
|
print("\n⚠️ Einschränkungen akzeptiert:")
|
|
print(" • EspoCRM → Advoware: Nur primary=false Einträge")
|
|
print(" • Keine bidirektionale Sync für Wert-Änderungen")
|
|
print(" • Bei Wert-Änderung: Neuanlage statt Update")
|
|
|
|
except Exception as e:
|
|
print(f"\n❌ Fehler: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|