feat: Implement bidirectional synchronization utilities for Advoware and EspoCRM communications

- 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.
This commit is contained in:
2026-02-08 19:53:40 +00:00
parent da9a962858
commit ebbbf419ee
23 changed files with 7626 additions and 13 deletions

View File

@@ -0,0 +1,152 @@
"""
Detaillierte Analyse: Was liefert /api/v1/advonet/Beteiligte/{id}?
Prüfe:
1. Kommunikation-Array: Alle Felder
2. kommKz und kommArt Werte
3. Adressen-Array (falls enthalten)
4. Vollständige Struktur
"""
import asyncio
import json
from services.advoware import AdvowareAPI
TEST_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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def main():
print("\n" + "="*70)
print("DETAILLIERTE ANALYSE: Beteiligte Endpoint")
print("="*70)
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole kompletten Beteiligte
print(f"\n📋 GET /api/v1/advonet/Beteiligte/{TEST_BETNR}")
result = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_BETNR}')
print(f"\nResponse Type: {type(result)}")
if isinstance(result, list):
print(f"Response Length: {len(result)}")
beteiligte = result[0]
else:
beteiligte = result
# Zeige Top-Level Struktur
print_section("TOP-LEVEL FELDER")
print(f"\nVerfügbare Keys:")
for key in sorted(beteiligte.keys()):
value = beteiligte[key]
if isinstance(value, list):
print(f"{key:30s}: [{len(value)} items]")
elif isinstance(value, dict):
print(f"{key:30s}: {{dict}}")
else:
value_str = str(value)[:50]
print(f"{key:30s}: {value_str}")
# Kommunikationen
print_section("KOMMUNIKATION ARRAY")
kommunikationen = beteiligte.get('kommunikation', [])
print(f"\n{len(kommunikationen)} Kommunikationen gefunden")
if kommunikationen:
print(f"\n📋 Erste Kommunikation - ALLE Felder:")
first = kommunikationen[0]
print(json.dumps(first, indent=2, ensure_ascii=False))
print(f"\n📊 Übersicht aller Kommunikationen:")
print(f"\n{'ID':>8s} | {'kommKz':>6s} | {'kommArt':>7s} | {'online':>6s} | {'Wert':40s} | {'Bemerkung'}")
print("-" * 120)
for k in kommunikationen:
komm_id = k.get('id', 'N/A')
kommkz = k.get('kommKz', 'N/A')
kommart = k.get('kommArt', 'N/A')
online = k.get('online', False)
wert = (k.get('tlf') or '')[:40]
bemerkung = (k.get('bemerkung') or '')[:20]
# Highlighting
kommkz_str = f"{kommkz}" if kommkz not in [0, 'N/A'] else f"{kommkz}"
kommart_str = f"{kommart}" if kommart not in [0, 'N/A'] else f"{kommart}"
print(f"{komm_id:8} | {kommkz_str:>6s} | {kommart_str:>7s} | {str(online):>6s} | {wert:40s} | {bemerkung}")
# Adressen
print_section("ADRESSEN ARRAY")
adressen = beteiligte.get('adressen', [])
print(f"\n{len(adressen)} Adressen gefunden")
if adressen:
print(f"\n📋 Erste Adresse - Struktur:")
first_addr = adressen[0]
print(json.dumps(first_addr, indent=2, ensure_ascii=False))
# Bankverbindungen
print_section("BANKVERBINDUNGEN")
bankverb = beteiligte.get('bankkverbindungen', []) # Typo im API?
if not bankverb:
bankverb = beteiligte.get('bankverbindungen', [])
print(f"\n{len(bankverb)} Bankverbindungen gefunden")
if bankverb:
print(f"\n📋 Erste Bankverbindung - Keys:")
print(list(bankverb[0].keys()))
# Analyse
print_section("ZUSAMMENFASSUNG")
print(f"\n📊 Verfügbare Daten:")
print(f" • Kommunikationen: {len(kommunikationen)}")
print(f" • Adressen: {len(adressen)}")
print(f" • Bankverbindungen: {len(bankverb)}")
print(f"\n🔍 kommKz/kommArt Status:")
if kommunikationen:
kommkz_values = [k.get('kommKz', 0) for k in kommunikationen]
kommart_values = [k.get('kommArt', 0) for k in kommunikationen]
kommkz_non_zero = [v for v in kommkz_values if v != 0]
kommart_non_zero = [v for v in kommart_values if v != 0]
print(f" • kommKz unique values: {set(kommkz_values)}")
print(f" • kommKz non-zero count: {len(kommkz_non_zero)} / {len(kommunikationen)}")
print(f" • kommArt unique values: {set(kommart_values)}")
print(f" • kommArt non-zero count: {len(kommart_non_zero)} / {len(kommunikationen)}")
if kommkz_non_zero:
print(f"\n ✅✅✅ JACKPOT! kommKz HAT WERTE im Beteiligte-Endpoint!")
print(f" → Wir können den Typ korrekt erkennen!")
elif kommart_non_zero:
print(f"\n ✅ kommArt hat Werte (Email/Phone unterscheidbar)")
else:
print(f"\n ❌ Beide sind 0 - müssen Typ aus Wert ableiten")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,261 @@
"""
Deep-Dive: Suche nach versteckten ID-Feldern
Die Relationships emailAddresses/phoneNumbers existieren (kein 404),
aber wir bekommen 403 Forbidden.
Möglichkeiten:
1. IDs sind in emailAddressData versteckt (vielleicht als 'id' Feld?)
2. Es gibt ein separates ID-Array
3. IDs sind in einem anderen Format gespeichert
4. Admin-API-Key hat nicht genug Rechte
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def inspect_email_data_structure():
"""Schaue sehr genau in emailAddressData/phoneNumberData"""
print_section("DEEP INSPECTION: emailAddressData Structure")
context = SimpleContext()
espo = EspoCRMAPI(context)
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
email_data = entity.get('emailAddressData', [])
print(f"\n📧 emailAddressData hat {len(email_data)} Einträge\n")
for i, email in enumerate(email_data):
print(f"[{i+1}] RAW Type: {type(email)}")
print(f" Keys: {list(email.keys())}")
print(f" JSON:\n")
print(json.dumps(email, indent=4, ensure_ascii=False))
# Prüfe ob 'id' Feld vorhanden ist
if 'id' in email:
print(f"\n ✅ ID GEFUNDEN: {email['id']}")
else:
print(f"\n ❌ Kein 'id' Feld")
# Prüfe alle Felder auf ID-ähnliche Werte
print(f"\n Alle Werte:")
for key, value in email.items():
print(f" {key:20s} = {value}")
print()
async def test_raw_api_call():
"""Mache rohe API-Calls um zu sehen was wirklich zurückkommt"""
print_section("RAW API CALL: Direkt ohne Wrapper")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Test 1: Normale Entity-Abfrage
print(f"\n1⃣ GET /CBeteiligte/{TEST_BETEILIGTE_ID}")
result1 = await espo.api_call(f'CBeteiligte/{TEST_BETEILIGTE_ID}')
# Zeige nur Email-relevante Felder
email_fields = {k: v for k, v in result1.items() if 'email' in k.lower()}
print(json.dumps(email_fields, indent=2, ensure_ascii=False))
# Test 2: Mit maxDepth Parameter (falls EspoCRM das unterstützt)
print(f"\n2⃣ GET mit maxDepth=2")
try:
result2 = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'maxDepth': '2'}
)
email_fields2 = {k: v for k, v in result2.items() if 'email' in k.lower()}
print(json.dumps(email_fields2, indent=2, ensure_ascii=False))
except Exception as e:
print(f" ❌ Error: {e}")
# Test 3: Select nur emailAddressData
print(f"\n3⃣ GET mit select=emailAddressData")
result3 = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': 'emailAddressData'}
)
print(json.dumps(result3, indent=2, ensure_ascii=False))
async def search_for_link_table():
"""Suche nach EntityEmailAddress oder EntityPhoneNumber Link-Tables"""
print_section("SUCHE: Link-Tables")
context = SimpleContext()
espo = EspoCRMAPI(context)
# In EspoCRM gibt es manchmal Link-Tables wie "EntityEmailAddress"
link_table_names = [
'EntityEmailAddress',
'EntityPhoneNumber',
'ContactEmailAddress',
'ContactPhoneNumber',
'CBeteiligteEmailAddress',
'CBeteiligtePhoneNumber'
]
for table_name in link_table_names:
print(f"\n🔍 Teste: {table_name}")
try:
result = await espo.api_call(table_name, params={'maxSize': 3})
print(f" ✅ Existiert! Total: {result.get('total', 'unknown')}")
if result.get('list'):
print(f" Beispiel:")
print(json.dumps(result['list'][0], indent=6, ensure_ascii=False))
except Exception as e:
error_msg = str(e)
if '404' in error_msg:
print(f" ❌ 404 - Existiert nicht")
elif '403' in error_msg:
print(f" ⚠️ 403 - Existiert aber kein Zugriff")
else:
print(f"{error_msg}")
async def test_update_with_ids():
"""Test: Kann ich beim UPDATE IDs setzen?"""
print_section("TEST: Update mit IDs")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n💡 Idee: Vielleicht kann man beim UPDATE IDs mitgeben")
print(f" und EspoCRM erstellt dann die Verknüpfung?\n")
# Hole aktuelle Daten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
current_emails = entity.get('emailAddressData', [])
print(f"Aktuelle Emails:")
for email in current_emails:
print(f"{email.get('emailAddress')}")
# Versuche ein Update mit expliziter ID
print(f"\n🧪 Teste: Füge 'id' Feld zu emailAddressData hinzu")
test_emails = []
for email in current_emails:
email_copy = email.copy()
# Generiere eine Test-ID (oder verwende eine echte wenn wir eine finden)
email_copy['id'] = f"test-id-{hash(email['emailAddress']) % 100000}"
test_emails.append(email_copy)
print(f"{email['emailAddress']:40s} → id={email_copy['id']}")
print(f"\n⚠️ ACHTUNG: Würde jetzt UPDATE machen mit:")
print(json.dumps({'emailAddressData': test_emails}, indent=2, ensure_ascii=False))
print(f"\n→ NICHT ausgeführt (zu riskant ohne Backup)")
async def check_database_or_config():
"""Prüfe ob es Config/Settings gibt die IDs aktivieren"""
print_section("ESPOCRM CONFIG: ID-Unterstützung")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n📋 Hole App-Informationen:")
try:
# EspoCRM hat oft einen /App endpoint
app_info = await espo.api_call('App/user')
# Zeige nur relevante Felder
if app_info:
relevant = ['acl', 'preferences', 'settings']
for key in relevant:
if key in app_info:
print(f"\n{key}:")
# Suche nach Email/Phone-relevanten Einstellungen
data = app_info[key]
if isinstance(data, dict):
email_phone_settings = {k: v for k, v in data.items()
if 'email' in k.lower() or 'phone' in k.lower()}
if email_phone_settings:
print(json.dumps(email_phone_settings, indent=2, ensure_ascii=False))
else:
print(" (keine Email/Phone-spezifischen Einstellungen)")
except Exception as e:
print(f" ❌ Error: {e}")
# Prüfe Settings
print(f"\n📋 System Settings:")
try:
settings = await espo.api_call('Settings')
if settings:
email_phone_settings = {k: v for k, v in settings.items()
if 'email' in k.lower() or 'phone' in k.lower()}
if email_phone_settings:
print(json.dumps(email_phone_settings, indent=2, ensure_ascii=False))
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("DEEP DIVE: SUCHE NACH PHONENUMBER/EMAILADDRESS IDs")
print("="*70)
try:
# Sehr detaillierte Inspektion
await inspect_email_data_structure()
# Rohe API-Calls
await test_raw_api_call()
# Link-Tables
await search_for_link_table()
# Update-Test (ohne tatsächlich zu updaten)
await test_update_with_ids()
# Config
await check_database_or_config()
print_section("FAZIT")
print("\n🎯 Mögliche Szenarien:")
print("\n1⃣ IDs existieren NICHT in emailAddressData")
print(" → Wert-basiertes Matching notwendig")
print(" → Hybrid-Strategie (primary-Flag)")
print("\n2⃣ IDs existieren aber sind versteckt/nicht zugänglich")
print(" → API-Rechte müssen erweitert werden")
print(" → Admin muss emailAddresses/phoneNumbers Relationship freigeben")
print("\n3⃣ IDs können beim UPDATE gesetzt werden")
print(" → Wir könnten eigene IDs generieren")
print(" → Advoware-ID direkt als EspoCRM-ID nutzen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,250 @@
"""
Test: Gibt es ID-Collections für EmailAddress/PhoneNumber?
In EspoCRM gibt es bei Many-to-Many Beziehungen oft:
- entityNameIds (Array von IDs)
- entityNameNames (Dict ID → Name)
Zum Beispiel: teamsIds, teamsNames
Hypothese: Es könnte emailAddressesIds oder ähnlich geben
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def search_for_id_fields():
"""Suche nach allen ID-ähnlichen Feldern"""
print_section("SUCHE: ID-Collections")
context = SimpleContext()
espo = EspoCRMAPI(context)
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print("\n🔍 Alle Felder die 'Ids' enthalten:")
ids_fields = {k: v for k, v in entity.items() if 'Ids' in k}
for key, value in sorted(ids_fields.items()):
print(f"{key:40s}: {value}")
print("\n🔍 Alle Felder die 'Names' enthalten:")
names_fields = {k: v for k, v in entity.items() if 'Names' in k}
for key, value in sorted(names_fields.items()):
print(f"{key:40s}: {value}")
print("\n🔍 Alle Felder mit 'email' oder 'phone' (case-insensitive):")
comm_fields = {k: v for k, v in entity.items()
if 'email' in k.lower() or 'phone' in k.lower()}
for key, value in sorted(comm_fields.items()):
value_str = str(value)[:80] if not isinstance(value, list) else f"[{len(value)} items]"
print(f"{key:40s}: {value_str}")
async def test_specific_fields():
"""Teste spezifische Feld-Namen die existieren könnten"""
print_section("TEST: Spezifische Feld-Namen")
context = SimpleContext()
espo = EspoCRMAPI(context)
potential_fields = [
'emailAddressesIds',
'emailAddressIds',
'phoneNumbersIds',
'phoneNumberIds',
'emailIds',
'phoneIds',
'emailAddressesNames',
'phoneNumbersNames',
]
print("\n📋 Teste mit select Parameter:\n")
for field in potential_fields:
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': f'id,{field}'}
)
if field in result and result[field] is not None:
print(f"{field:30s}: {result[field]}")
else:
print(f" ⚠️ {field:30s}: Im Response aber None/leer")
except Exception as e:
print(f"{field:30s}: {str(e)[:60]}")
async def test_with_loadAdditionalFields():
"""EspoCRM unterstützt manchmal loadAdditionalFields Parameter"""
print_section("TEST: loadAdditionalFields Parameter")
context = SimpleContext()
espo = EspoCRMAPI(context)
params_to_test = [
{'loadAdditionalFields': 'true'},
{'loadAdditionalFields': '1'},
{'withLinks': 'true'},
{'withRelated': 'emailAddresses,phoneNumbers'},
]
for params in params_to_test:
print(f"\n📋 Teste mit params: {params}")
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params=params
)
# Suche nach neuen Feldern
new_fields = {k: v for k, v in result.items()
if ('email' in k.lower() or 'phone' in k.lower())
and 'Data' not in k}
if new_fields:
print(" ✅ Neue Felder gefunden:")
for k, v in new_fields.items():
print(f"{k}: {v}")
else:
print(" ⚠️ Keine neuen Felder")
except Exception as e:
print(f" ❌ Error: {e}")
async def test_create_with_explicit_ids():
"""
Was wenn wir bei CREATE/UPDATE explizite IDs für Email/Phone mitgeben?
Vielleicht gibt EspoCRM dann IDs zurück?
"""
print_section("IDEE: Explizite IDs bei UPDATE mitgeben")
print("\n💡 EspoCRM Standard-Verhalten:")
print(" Bei Many-to-Many Beziehungen (z.B. Teams):")
print(" - INPUT: teamsIds: ['id1', 'id2']")
print(" - OUTPUT: teamsIds: ['id1', 'id2']")
print(" ")
print(" Könnte bei emailAddresses ähnlich funktionieren?")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole aktuelle Daten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
current_emails = entity.get('emailAddressData', [])
print("\n📋 Aktuelle emailAddressData:")
for e in current_emails:
print(f"{e.get('emailAddress')}")
# Versuche ein Update mit hypothetischen emailAddressesIds
print("\n🧪 Test: UPDATE mit emailAddressesIds Feld")
print(" (DRY RUN - nicht wirklich ausgeführt)")
# Generiere Test-IDs (EspoCRM IDs sind meist 17 Zeichen)
test_ids = [f"test{str(i).zfill(13)}" for i in range(len(current_emails))]
print(f"\n Würde senden:")
print(f" emailAddressesIds: {test_ids}")
print(f" emailAddressData: {[e['emailAddress'] for e in current_emails]}")
print("\n ⚠️ Zu riskant ohne zu wissen was passiert")
async def check_standard_contact_entity():
"""
Prüfe wie es bei Standard Contact Entity funktioniert
(als Referenz für Custom Entity)
"""
print_section("REFERENZ: Standard Contact Entity")
context = SimpleContext()
espo = EspoCRMAPI(context)
print("\n📋 Hole ersten Contact als Referenz:")
try:
contacts = await espo.api_call('Contact', params={'maxSize': 1})
if contacts and contacts.get('list'):
contact = contacts['list'][0]
print(f"\n Contact: {contact.get('name')}")
print(f"\n 🔍 Email/Phone-relevante Felder:")
for key, value in sorted(contact.items()):
if 'email' in key.lower() or 'phone' in key.lower():
value_str = str(value)[:80] if not isinstance(value, (list, dict)) else type(value).__name__
print(f"{key:35s}: {value_str}")
else:
print(" ⚠️ Keine Contacts vorhanden")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("SUCHE: EMAIL/PHONE ID-COLLECTIONS")
print("="*70)
print("\nZiel: Finde ID-Arrays für EmailAddress/PhoneNumber Entities\n")
try:
await search_for_id_fields()
await test_specific_fields()
await test_with_loadAdditionalFields()
await test_create_with_explicit_ids()
await check_standard_contact_entity()
print_section("FAZIT")
print("\n🎯 Wenn KEINE ID-Collections existieren:")
print("\n Option 1: Separate CKommunikation Entity ✅ BESTE LÖSUNG")
print(" Struktur:")
print(" {")
print(" 'id': 'espocrm-generated-id',")
print(" 'beteiligteId': '68e4af00...',")
print(" 'typ': 'Email/Phone',")
print(" 'wert': 'max@example.com',")
print(" 'advowareId': 149331,")
print(" 'advowareRowId': 'ABC...'")
print(" }")
print("\n Vorteile:")
print(" • Eigene Entity-ID für jede Kommunikation")
print(" • advowareId/advowareRowId als eigene Felder")
print(" • Sauberes Datenmodell")
print(" • Stabiles bidirektionales Matching")
print("\n Option 2: One-Way Sync (Advoware → EspoCRM)")
print(" • Matching via Wert (emailAddress/phoneNumber)")
print(" • Nur Advoware-Änderungen werden synchronisiert")
print(" • EspoCRM als Read-Only Viewer")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,225 @@
"""
TEST: Können wir eigene IDs in emailAddressData setzen?
Wenn EspoCRM IDs beim UPDATE akzeptiert und speichert,
dann können wir:
- Advoware-ID als 'id' in emailAddressData speichern
- Stabiles Matching haben
- Bidirektionalen Sync machen
Vorsichtiger Test mit Backup!
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_id_persistence():
"""
Teste ob EspoCRM IDs in emailAddressData speichert
Ablauf:
1. Hole aktuelle Daten (Backup)
2. Füge 'id' Feld zu EINEM Email hinzu
3. UPDATE
4. GET wieder
5. Prüfe ob 'id' noch da ist
6. Restore original falls nötig
"""
print_section("TEST: ID Persistence in emailAddressData")
context = SimpleContext()
espo = EspoCRMAPI(context)
# 1. Backup
print("\n1⃣ Backup: Hole aktuelle Daten")
entity_backup = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
emails_backup = entity_backup.get('emailAddressData', [])
print(f" Backup: {len(emails_backup)} Emails gesichert")
for email in emails_backup:
print(f"{email['emailAddress']}")
# 2. Modifiziere NUR das erste Email (primary)
print("\n2⃣ Modifikation: Füge 'id' zu primary Email hinzu")
emails_modified = []
for i, email in enumerate(emails_backup):
email_copy = email.copy()
if email_copy.get('primary'): # Nur primary modifizieren
# Nutze einen recognizable Test-Wert
test_id = f"advoware-{i+1}-test-123"
email_copy['id'] = test_id
print(f" ✏️ {email['emailAddress']:40s} → id={test_id}")
else:
print(f" ⏭️ {email['emailAddress']:40s} (unverändert)")
emails_modified.append(email_copy)
# 3. UPDATE
print("\n3⃣ UPDATE: Sende modifizierte Daten")
try:
await espo.update_entity('CBeteiligte', TEST_BETEILIGTE_ID, {
'emailAddressData': emails_modified
})
print(" ✅ UPDATE erfolgreich")
except Exception as e:
print(f" ❌ UPDATE fehlgeschlagen: {e}")
return
# 4. GET wieder
print("\n4⃣ GET: Hole Daten wieder ab")
await asyncio.sleep(0.5) # Kurze Pause
entity_after = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
emails_after = entity_after.get('emailAddressData', [])
print(f" Nach UPDATE: {len(emails_after)} Emails")
# 5. Vergleiche
print("\n5⃣ VERGLEICH: Ist 'id' noch da?")
id_found = False
for email in emails_after:
email_addr = email['emailAddress']
has_id = 'id' in email
if has_id:
print(f"{email_addr:40s} → id={email['id']}")
id_found = True
else:
print(f"{email_addr:40s} → KEIN id Feld")
# 6. Ergebnis
print(f"\n6⃣ ERGEBNIS:")
if id_found:
print(" 🎉 SUCCESS! EspoCRM speichert und liefert 'id' Feld zurück!")
print(" → Wir können Advoware-IDs in emailAddressData speichern")
print(" → Stabiles bidirektionales Matching möglich")
else:
print(" ❌ FAILED: EspoCRM ignoriert/entfernt 'id' Feld")
print(" → Wert-basiertes Matching notwendig")
print(" → Hybrid-Strategie (primary-Flag) ist beste Option")
# 7. Restore (optional - nur wenn User will)
print(f"\n7⃣ CLEANUP:")
print(" Original-Daten (ohne id):")
for email in emails_backup:
print(f"{email['emailAddress']}")
if id_found:
restore = input("\n 🔄 Restore zu Original (ohne id)? [y/N]: ").strip().lower()
if restore == 'y':
await espo.update_entity('CBeteiligte', TEST_BETEILIGTE_ID, {
'emailAddressData': emails_backup
})
print(" ✅ Restored")
else:
print(" ⏭️ Nicht restored (id bleibt)")
return id_found
async def test_custom_field_approach():
"""
Alternative: Nutze ein custom field in CBeteiligte für ID-Mapping
Idee: Speichere JSON-Mapping in einem Textfeld
"""
print_section("ALTERNATIVE: Custom Field für ID-Mapping")
print("\n💡 Idee: Nutze custom field 'kommunikationMapping'")
print(" Struktur:")
print(" {")
print(' "emails": [')
print(' {"emailAddress": "max@example.com", "advowareId": 123, "advowareRowId": "ABC"}')
print(' ],')
print(' "phones": [')
print(' {"phoneNumber": "+49...", "advowareId": 456, "advowareRowId": "DEF"}')
print(' ]')
print(" }")
print("\n✅ Vorteile:")
print(" • Stabiles Matching via advowareId")
print(" • Change Detection via advowareRowId")
print(" • Bidirektionaler Sync möglich")
print("\n❌ Nachteile:")
print(" • Erfordert custom field in EspoCRM")
print(" • Daten-Duplikation (in Data + Mapping)")
print(" • Fragil wenn emailAddress/phoneNumber ändert")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Prüfe ob custom field existiert
print("\n🔍 Prüfe ob 'kommunikationMapping' Feld existiert:")
try:
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
if 'kommunikationMapping' in entity:
print(f" ✅ Feld existiert: {entity['kommunikationMapping']}")
else:
print(f" ❌ Feld existiert nicht")
print(f" → Müsste in EspoCRM angelegt werden")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("TEST: KÖNNEN WIR EIGENE IDs IN emailAddressData SETZEN?")
print("="*70)
print("\nZiel: Herausfinden ob EspoCRM 'id' Felder akzeptiert und speichert\n")
try:
# Haupttest
id_works = await test_id_persistence()
# Alternative
await test_custom_field_approach()
print_section("FINAL RECOMMENDATION")
if id_works:
print("\n🎯 EMPFEHLUNG: Nutze 'id' Feld in emailAddressData")
print("\n📋 Implementation:")
print(" 1. Bei Advoware → EspoCRM: Füge 'id' mit Advoware-ID hinzu")
print(" 2. Matching via 'id' Feld")
print(" 3. Change Detection via Advoware rowId")
print(" 4. Bidirektionaler Sync möglich")
else:
print("\n🎯 EMPFEHLUNG A: Hybrid-Strategie (primary-Flag)")
print(" • Einfach zu implementieren")
print(" • Nutzt Standard-EspoCRM")
print(" • Eingeschränkt bidirektional")
print("\n🎯 EMPFEHLUNG B: Custom Field 'kommunikationMapping'")
print(" • Vollständig bidirektional")
print(" • Erfordert EspoCRM-Anpassung")
print(" • Komplexere Implementation")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,277 @@
"""
Test: EspoCRM Kommunikation - Wie werden Kontaktdaten gespeichert?
Prüfe:
1. Gibt es ein separates CKommunikation Entity?
2. Wie sind Telefon/Email/Fax in CBeteiligte gespeichert?
3. Sind es Arrays oder einzelne Felder?
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
# Test-Beteiligter mit Kommunikationsdaten
TEST_BETEILIGTE_ID = '68e4af00172be7924' # Angela Mustermanns
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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_cbeteiligte_structure():
"""Analysiere CBeteiligte Kommunikationsfelder"""
print_section("TEST 1: CBeteiligte Entity Struktur")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Beteiligten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter geladen: {entity.get('name')}")
print(f" ID: {entity.get('id')}")
print(f" betNr: {entity.get('betnr')}")
# Suche nach Kommunikationsfeldern
print("\n📊 Kommunikations-relevante Felder:")
comm_fields = [
'phoneNumber', 'phoneNumberData',
'emailAddress', 'emailAddressData',
'fax', 'faxData',
'mobile', 'mobileData',
'website',
# Plural Varianten
'phoneNumbers', 'emailAddresses', 'faxNumbers',
# Link-Felder
'kommunikationIds', 'kommunikationNames',
'kommunikationenIds', 'kommunikationenNames',
'ckommunikationIds', 'ckommunikationNames'
]
found_fields = {}
for field in comm_fields:
if field in entity:
value = entity[field]
found_fields[field] = value
print(f"\n{field}:")
print(f" Typ: {type(value).__name__}")
if isinstance(value, list):
print(f" Anzahl: {len(value)}")
if len(value) > 0:
print(f" Beispiel: {json.dumps(value[0], indent=6, ensure_ascii=False)}")
elif isinstance(value, dict):
print(f" Keys: {list(value.keys())}")
print(f" Content: {json.dumps(value, indent=6, ensure_ascii=False)}")
else:
print(f" Wert: {value}")
if not found_fields:
print("\n ⚠️ Keine Standard-Kommunikationsfelder gefunden")
# Zeige alle Felder die "comm", "phone", "email", "fax", "tel" enthalten
print("\n📋 Alle Felder mit Kommunikations-Keywords:")
keywords = ['comm', 'phone', 'email', 'fax', 'tel', 'mobil', 'kontakt']
matching_fields = {}
for key, value in entity.items():
key_lower = key.lower()
if any(kw in key_lower for kw in keywords):
matching_fields[key] = value
print(f"{key}: {type(value).__name__}")
if isinstance(value, (str, int, bool)) and value:
print(f" = {value}")
return entity, found_fields
async def test_ckommunikation_entity():
"""Prüfe ob CKommunikation Entity existiert"""
print_section("TEST 2: CKommunikation Entity")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche CKommunikation zu listen
try:
result = await espo.list_entities('CKommunikation', max_size=5)
print(f"✅ CKommunikation Entity existiert!")
print(f" Anzahl gefunden: {len(result)}")
if result:
print(f"\n📋 Beispiel-Kommunikation:")
print(json.dumps(result[0], indent=2, ensure_ascii=False))
return True, result
except Exception as e:
if '404' in str(e) or 'not found' in str(e).lower():
print(f"❌ CKommunikation Entity existiert NICHT")
print(f" Fehler: {e}")
return False, None
else:
print(f"⚠️ Fehler beim Abrufen: {e}")
return None, None
async def test_entity_metadata():
"""Hole Entity-Metadaten von CBeteiligte"""
print_section("TEST 3: CBeteiligte Metadaten")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Metadaten (falls API das unterstützt)
try:
# Versuche Entity-Defs zu holen
metadata = await espo.api_call('/Metadata', method='GET')
if 'entityDefs' in metadata and 'CBeteiligte' in metadata['entityDefs']:
beteiligte_def = metadata['entityDefs']['CBeteiligte']
print("✅ Metadaten verfügbar")
if 'fields' in beteiligte_def:
fields = beteiligte_def['fields']
print(f"\n📊 Kommunikations-Felder in Definition:")
for field_name, field_def in fields.items():
field_lower = field_name.lower()
if any(kw in field_lower for kw in ['comm', 'phone', 'email', 'fax', 'tel']):
print(f"\n{field_name}:")
print(f" type: {field_def.get('type')}")
if 'entity' in field_def:
print(f" entity: {field_def.get('entity')}")
if 'link' in field_def:
print(f" link: {field_def.get('link')}")
return metadata
except Exception as e:
print(f"⚠️ Metadaten nicht verfügbar: {e}")
return None
async def test_list_all_entities():
"""Liste alle verfügbaren Entities"""
print_section("TEST 4: Alle verfügbaren Entities")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Häufige Entity-Namen die mit Kommunikation zu tun haben könnten
test_entities = [
'CKommunikation',
'Kommunikation',
'Communication',
'PhoneNumber',
'EmailAddress',
'CPhoneNumber',
'CEmailAddress',
'CPhone',
'CEmail',
'CContact',
'ContactData'
]
print("\n🔍 Teste verschiedene Entity-Namen:\n")
existing = []
for entity_name in test_entities:
try:
result = await espo.list_entities(entity_name, max_size=1)
print(f"{entity_name} - existiert ({len(result)} gefunden)")
existing.append(entity_name)
except Exception as e:
if '404' in str(e) or 'not found' in str(e).lower():
print(f"{entity_name} - existiert nicht")
else:
print(f" ⚠️ {entity_name} - Fehler: {str(e)[:50]}")
return existing
async def main():
print("\n" + "="*70)
print("ESPOCRM KOMMUNIKATION ANALYSE")
print("="*70)
print("\nZiel: Verstehen wie Kommunikationsdaten in EspoCRM gespeichert sind")
print("Frage: Gibt es separate Kommunikations-Entities oder nur Felder?\n")
try:
# Test 1: CBeteiligte Struktur
entity, comm_fields = await test_cbeteiligte_structure()
# Test 2: CKommunikation Entity
ckommunikation_exists, ckommunikation_data = await test_ckommunikation_entity()
# Test 3: Metadaten
# metadata = await test_entity_metadata()
# Test 4: Liste entities
existing_entities = await test_list_all_entities()
# Zusammenfassung
print_section("ZUSAMMENFASSUNG")
print("\n📊 Erkenntnisse:")
if comm_fields:
print(f"\n✅ CBeteiligte hat Kommunikationsfelder:")
for field, value in comm_fields.items():
vtype = type(value).__name__
print(f"{field} ({vtype})")
if ckommunikation_exists:
print(f"\n✅ CKommunikation Entity existiert")
print(f" → Separate Kommunikations-Entities möglich")
elif ckommunikation_exists == False:
print(f"\n❌ CKommunikation Entity existiert NICHT")
print(f" → Kommunikation nur als Felder in CBeteiligte")
if existing_entities:
print(f"\n📋 Gefundene Kommunikations-Entities:")
for ename in existing_entities:
print(f"{ename}")
print("\n💡 Empfehlung:")
if not comm_fields and not ckommunikation_exists:
print(" ⚠️ Keine Kommunikationsstruktur gefunden")
print(" → Eventuell müssen Custom Fields erst angelegt werden")
elif comm_fields and not ckommunikation_exists:
print(" → Verwende vorhandene Felder in CBeteiligte (phoneNumber, emailAddress, etc.)")
print(" → Sync als Teil des Beteiligte-Syncs (nicht separat)")
elif ckommunikation_exists:
print(" → Verwende CKommunikation Entity für separaten Kommunikations-Sync")
print(" → Ermöglicht mehrere Kommunikationseinträge pro Beteiligten")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,202 @@
"""
Detail-Analyse: emailAddressData und phoneNumberData Struktur
Erkenntnisse:
- CKommunikation Entity existiert NICHT in EspoCRM
- CBeteiligte hat phoneNumberData und emailAddressData Arrays
- PhoneNumber und EmailAddress Entities existieren (aber 403 Forbidden - nur intern)
Jetzt: Analysiere die Data-Arrays im Detail
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def analyze_communication_data():
"""Detaillierte Analyse der Communication-Data Felder"""
print_section("DETAIL-ANALYSE: emailAddressData und phoneNumberData")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Beteiligten
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter: {entity.get('name')}")
print(f" ID: {entity.get('id')}")
# emailAddressData
print("\n" + "="*50)
print("emailAddressData")
print("="*50)
email_data = entity.get('emailAddressData', [])
if email_data:
print(f"\n📧 {len(email_data)} Email-Adresse(n):\n")
for i, email in enumerate(email_data):
print(f"[{i+1}] {json.dumps(email, indent=2, ensure_ascii=False)}")
# Analysiere Struktur
if i == 0:
print(f"\n📊 Feld-Struktur:")
for key, value in email.items():
print(f"{key:20s}: {type(value).__name__:10s} = {value}")
else:
print("\n❌ Keine Email-Adressen vorhanden")
# phoneNumberData
print("\n" + "="*50)
print("phoneNumberData")
print("="*50)
phone_data = entity.get('phoneNumberData', [])
if phone_data:
print(f"\n📞 {len(phone_data)} Telefonnummer(n):\n")
for i, phone in enumerate(phone_data):
print(f"[{i+1}] {json.dumps(phone, indent=2, ensure_ascii=False)}")
# Analysiere Struktur
if i == 0:
print(f"\n📊 Feld-Struktur:")
for key, value in phone.items():
print(f"{key:20s}: {type(value).__name__:10s} = {value}")
else:
print("\n❌ Keine Telefonnummern vorhanden")
# Prüfe andere Beteiligten mit mehr Kommunikationsdaten
print_section("SUCHE: Beteiligter mit mehr Kommunikationsdaten")
print("\n🔍 Liste erste 20 Beteiligte und prüfe Kommunikationsdaten...\n")
beteiligte_list = await espo.list_entities('CBeteiligte', max_size=20)
best_example = None
max_comm_count = 0
for bet in beteiligte_list:
# list_entities kann Strings oder Dicts zurückgeben
if isinstance(bet, str):
continue
email_count = len(bet.get('emailAddressData', []))
phone_count = len(bet.get('phoneNumberData', []))
total = email_count + phone_count
if total > 0:
print(f"{bet.get('name', 'N/A')[:40]:40s} | "
f"Email: {email_count} | Phone: {phone_count}")
if total > max_comm_count:
max_comm_count = total
best_example = bet
if best_example and max_comm_count > 0:
print(f"\n✅ Bester Beispiel-Beteiligter: {best_example.get('name')}")
print(f" Gesamt: {max_comm_count} Kommunikationseinträge")
print("\n📧 emailAddressData:")
for i, email in enumerate(best_example.get('emailAddressData', [])):
print(f"\n [{i+1}] {json.dumps(email, indent=6, ensure_ascii=False)}")
print("\n📞 phoneNumberData:")
for i, phone in enumerate(best_example.get('phoneNumberData', [])):
print(f"\n [{i+1}] {json.dumps(phone, indent=6, ensure_ascii=False)}")
return entity, email_data, phone_data, best_example
async def main():
print("\n" + "="*70)
print("ESPOCRM KOMMUNIKATION - DETAIL-ANALYSE")
print("="*70)
print("\nZiel: Verstehe die Struktur von emailAddressData und phoneNumberData")
print("Frage: Haben diese Arrays IDs für Matching mit Advoware?\n")
try:
entity, emails, phones, best = await analyze_communication_data()
print_section("ZUSAMMENFASSUNG")
print("\n📊 Erkenntnisse:")
print("\n1⃣ EspoCRM Standard-Struktur:")
print(" • emailAddressData: Array von Email-Objekten")
print(" • phoneNumberData: Array von Telefon-Objekten")
print(" • Keine separate CKommunikation Entity")
if emails:
print("\n2⃣ emailAddressData Felder:")
sample = emails[0]
for key in sample.keys():
print(f"{key}")
if 'id' in sample:
print("\n ✅ Hat 'id' Feld → Kann für Matching verwendet werden!")
else:
print("\n ❌ Kein 'id' Feld → Matching via Wert (emailAddress)")
if phones:
print("\n3⃣ phoneNumberData Felder:")
sample = phones[0]
for key in sample.keys():
print(f"{key}")
if 'id' in sample:
print("\n ✅ Hat 'id' Feld → Kann für Matching verwendet werden!")
else:
print("\n ❌ Kein 'id' Feld → Matching via Wert (phoneNumber)")
print("\n💡 Sync-Strategie:")
print("\n Option A: Kommunikation als Teil von Beteiligte-Sync")
print(" ────────────────────────────────────────────────────")
print(" • emailAddressData → Advoware Kommunikation (kommKz=4)")
print(" • phoneNumberData → Advoware Kommunikation (kommKz=1)")
print(" • Sync innerhalb von beteiligte_sync.py")
print(" • Kein separates Entity in EspoCRM nötig")
print("\n Option B: Custom CKommunikation Entity erstellen")
print(" ────────────────────────────────────────────────────")
print(" • Neues Custom Entity in EspoCRM anlegen")
print(" • Many-to-One Beziehung zu CBeteiligte")
print(" • Separater kommunikation_sync.py")
print(" • Ermöglicht mehr Flexibilität (Fax, BeA, etc.)")
print("\n ⚠️ WICHTIG:")
print(" • Standard EspoCRM hat NUR Email und Phone")
print(" • Advoware hat 12 verschiedene Kommunikationstypen")
print(" • Für vollständigen Sync → Custom Entity empfohlen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,297 @@
"""
Test: PhoneNumber und EmailAddress als System-Entities
Hypothese:
- PhoneNumber und EmailAddress sind separate Entities mit IDs
- CBeteiligte hat Links/Relations zu diesen Entities
- Wir können über related entries an die IDs kommen
Ziele:
1. Hole CBeteiligte mit expanded relationships
2. Prüfe ob phoneNumbers/emailAddresses als Links verfügbar sind
3. Extrahiere IDs der verknüpften PhoneNumber/EmailAddress Entities
"""
import asyncio
import json
from services.espocrm import EspoCRMAPI
TEST_BETEILIGTE_ID = '68e4af00172be7924'
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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_related_entities():
"""Test 1: Hole CBeteiligte mit allen verfügbaren Feldern"""
print_section("TEST 1: CBeteiligte - Alle Felder")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Hole Entity
entity = await espo.get_entity('CBeteiligte', TEST_BETEILIGTE_ID)
print(f"\n✅ Beteiligter: {entity.get('name')}")
print(f"\n📋 Alle Top-Level Felder:")
for key in sorted(entity.keys()):
value = entity[key]
value_type = type(value).__name__
# Zeige nur ersten Teil von langen Werten
if isinstance(value, str) and len(value) > 60:
display = f"{value[:60]}..."
elif isinstance(value, list):
display = f"[{len(value)} items]"
elif isinstance(value, dict):
display = f"{{dict with {len(value)} keys}}"
else:
display = value
print(f"{key:30s}: {value_type:10s} = {display}")
# Suche nach ID-Feldern für Kommunikation
print(f"\n🔍 Suche nach ID-Feldern für Email/Phone:")
potential_id_fields = [k for k in entity.keys() if 'email' in k.lower() or 'phone' in k.lower()]
for field in potential_id_fields:
print(f"{field}: {entity.get(field)}")
return entity
async def test_list_with_select():
"""Test 2: Nutze select Parameter um spezifische Felder zu holen"""
print_section("TEST 2: CBeteiligte mit select Parameter")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche verschiedene Feld-Namen
potential_fields = [
'emailAddresses',
'phoneNumbers',
'emailAddressId',
'phoneNumberId',
'emailAddressIds',
'phoneNumberIds',
'emailAddressList',
'phoneNumberList'
]
print(f"\n📋 Teste verschiedene Feld-Namen:")
for field in potential_fields:
try:
result = await espo.api_call(
f'CBeteiligte/{TEST_BETEILIGTE_ID}',
params={'select': field}
)
if result and field in result:
print(f"{field:30s}: {result[field]}")
else:
print(f"{field:30s}: Nicht im Response")
except Exception as e:
print(f"{field:30s}: Error - {e}")
async def test_entity_relationships():
"""Test 3: Hole Links/Relationships über dedizierte Endpoints"""
print_section("TEST 3: Entity Relationships")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Test verschiedene Relationship-Endpoints
relationship_names = [
'emailAddresses',
'phoneNumbers',
'emails',
'phones'
]
for rel_name in relationship_names:
print(f"\n🔗 Teste Relationship: {rel_name}")
try:
# EspoCRM API Format: /Entity/{id}/relationship-name
result = await espo.api_call(f'CBeteiligte/{TEST_BETEILIGTE_ID}/{rel_name}')
if result:
print(f" ✅ Success! Type: {type(result)}")
if isinstance(result, dict):
print(f" 📋 Response Keys: {list(result.keys())}")
# Häufige EspoCRM Response-Strukturen
if 'list' in result:
items = result['list']
print(f" 📊 {len(items)} Einträge in 'list'")
if items:
print(f"\n Erster Eintrag:")
print(json.dumps(items[0], indent=6, ensure_ascii=False))
if 'total' in result:
print(f" 📊 Total: {result['total']}")
elif isinstance(result, list):
print(f" 📊 {len(result)} Einträge direkt als Liste")
if result:
print(f"\n Erster Eintrag:")
print(json.dumps(result[0], indent=6, ensure_ascii=False))
else:
print(f" ⚠️ Empty response")
except Exception as e:
error_msg = str(e)
if '404' in error_msg:
print(f" ❌ 404 Not Found - Relationship existiert nicht")
elif '403' in error_msg:
print(f" ❌ 403 Forbidden - Kein Zugriff")
else:
print(f" ❌ Error: {error_msg}")
async def test_direct_entity_access():
"""Test 4: Direkter Zugriff auf PhoneNumber/EmailAddress Entities"""
print_section("TEST 4: Direkte Entity-Abfrage")
context = SimpleContext()
espo = EspoCRMAPI(context)
# Versuche die Entities direkt zu listen
for entity_type in ['PhoneNumber', 'EmailAddress']:
print(f"\n📋 Liste {entity_type} Entities:")
try:
# Mit Filter für unseren Beteiligten
result = await espo.api_call(
entity_type,
params={
'maxSize': 5,
'where': json.dumps([{
'type': 'equals',
'attribute': 'parentId',
'value': TEST_BETEILIGTE_ID
}])
}
)
if result and 'list' in result:
items = result['list']
print(f"{len(items)} Einträge gefunden")
for item in items:
print(f"\n 📧/📞 {entity_type}:")
print(json.dumps(item, indent=6, ensure_ascii=False))
else:
print(f" ⚠️ Keine Einträge oder unerwartetes Format")
print(f" Response: {result}")
except Exception as e:
error_msg = str(e)
if '403' in error_msg:
print(f" ❌ 403 Forbidden")
print(f" → Versuche ohne Filter...")
try:
# Ohne Filter
result = await espo.api_call(entity_type, params={'maxSize': 3})
print(f" ✅ Ohne Filter: {result.get('total', 0)} total existieren")
except Exception as e2:
print(f" ❌ Auch ohne Filter: {e2}")
else:
print(f" ❌ Error: {error_msg}")
async def test_espocrm_metadata():
"""Test 5: Prüfe EspoCRM Metadata für CBeteiligte"""
print_section("TEST 5: EspoCRM Metadata")
context = SimpleContext()
espo = EspoCRMAPI(context)
print(f"\n📋 Hole Metadata für CBeteiligte:")
try:
# EspoCRM bietet manchmal Metadata-Endpoints
result = await espo.api_call('Metadata')
if result and 'entityDefs' in result:
if 'CBeteiligte' in result['entityDefs']:
bet_meta = result['entityDefs']['CBeteiligte']
print(f"\n ✅ CBeteiligte Metadata gefunden")
if 'links' in bet_meta:
print(f"\n 🔗 Links/Relationships:")
for link_name, link_def in bet_meta['links'].items():
if 'email' in link_name.lower() or 'phone' in link_name.lower():
print(f"{link_name}: {link_def}")
if 'fields' in bet_meta:
print(f"\n 📋 Relevante Felder:")
for field_name, field_def in bet_meta['fields'].items():
if 'email' in field_name.lower() or 'phone' in field_name.lower():
print(f"{field_name}: {field_def.get('type', 'unknown')}")
else:
print(f" ⚠️ Unerwartetes Format")
except Exception as e:
print(f" ❌ Error: {e}")
async def main():
print("\n" + "="*70)
print("ESPOCRM PHONENUMBER/EMAILADDRESS - ENTITIES & IDS")
print("="*70)
print("\nZiel: Finde IDs für PhoneNumber/EmailAddress über Relationships\n")
try:
# Test 1: Alle Felder inspizieren
entity = await test_related_entities()
# Test 2: Select Parameter
await test_list_with_select()
# Test 3: Relationships
await test_entity_relationships()
# Test 4: Direkte Entity-Abfrage
await test_direct_entity_access()
# Test 5: Metadata
await test_espocrm_metadata()
print_section("ZUSAMMENFASSUNG")
print("\n🎯 Erkenntnisse:")
print("\n Wenn PhoneNumber/EmailAddress System-Entities sind:")
print(" 1. ✅ Sie haben eigene IDs")
print(" 2. ✅ Stabiles Matching möglich")
print(" 3. ✅ Bidirektionaler Sync machbar")
print(" 4. ✅ Change Detection via ID")
print("\n Wenn wir IDs haben:")
print(" • Können Advoware-ID zu EspoCRM-ID mappen")
print(" • Können Änderungen tracken")
print(" • Kein Problem bei Wert-Änderungen")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,109 @@
"""
Test: Was liefert kommArt im Vergleich zu kommKz?
kommArt sollte sein:
- 0 = Telefon/Fax
- 1 = Email
- 2 = Internet
Wenn kommArt funktioniert, können wir damit unterscheiden!
"""
import asyncio
from services.advoware import AdvowareAPI
TEST_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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def main():
print("\n" + "="*70)
print("ADVOWARE kommArt vs kommKz")
print("="*70)
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole Beteiligte mit Kommunikationen
result = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_BETNR}')
beteiligte = result[0]
kommunikationen = beteiligte.get('kommunikation', [])
print(f"\n{len(kommunikationen)} Kommunikationen gefunden\n")
print(f"{'ID':>8s} | {'kommKz':>6s} | {'kommArt':>7s} | {'Wert':40s}")
print("-" * 70)
kommkz_values = []
kommart_values = []
for k in kommunikationen:
komm_id = k.get('id')
kommkz = k.get('kommKz', 'N/A')
kommart = k.get('kommArt', 'N/A')
wert = k.get('tlf', '')[:40]
kommkz_values.append(kommkz)
kommart_values.append(kommart)
# Markiere wenn Wert aussagekräftig ist
kommkz_str = f"{kommkz}" if kommkz != 0 else f"{kommkz}"
kommart_str = f"{kommart}" if kommart != 0 else f"{kommart}"
print(f"{komm_id:8d} | {kommkz_str:>6s} | {kommart_str:>7s} | {wert}")
print_section("ANALYSE")
# Statistik
print(f"\n📊 kommKz Werte:")
print(f" • Alle Werte: {set(kommkz_values)}")
print(f" • Alle sind 0: {all(v == 0 for v in kommkz_values)}")
print(f"\n📊 kommArt Werte:")
print(f" • Alle Werte: {set(kommart_values)}")
print(f" • Alle sind 0: {all(v == 0 for v in kommart_values)}")
print_section("FAZIT")
if not all(v == 0 for v in kommart_values):
print("\n✅ kommArt IST BRAUCHBAR!")
print("\nMapping:")
print(" 0 = Telefon/Fax")
print(" 1 = Email")
print(" 2 = Internet")
print("\n🎉 PERFEKT! Wir können unterscheiden:")
print(" • kommArt=0 → Telefon (zu phoneNumberData)")
print(" • kommArt=1 → Email (zu emailAddressData)")
print(" • kommArt=2 → Internet (überspringen oder zu Notiz)")
print("\n💡 Advoware → EspoCRM:")
print(" 1. Nutze kommArt um Typ zu erkennen")
print(" 2. Speichere in bemerkung: [ESPOCRM:hash:kommArt]")
print(" 3. Bei Reverse-Sync: Nutze kommArt aus bemerkung")
else:
print("\n❌ kommArt ist AUCH 0 - genau wie kommKz")
print("\n→ Wir müssen Typ aus Wert ableiten (Email vs. Telefon)")
print("'@' im Wert → Email")
print("'+' oder Ziffern → Telefon")
print("\n→ Feinere Unterscheidung (TelGesch vs TelPrivat) NICHT möglich")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,361 @@
"""
Test: Advoware Kommunikation API
Testet POST/GET/PUT/DELETE Operationen für Kommunikationen
Basierend auf Swagger:
- POST /api/v1/advonet/Beteiligte/{beteiligterId}/Kommunikationen
- PUT /api/v1/advonet/Beteiligte/{beteiligterId}/Kommunikationen/{kommunikationId}
- GET enthalten in Beteiligte response (kommunikation array)
- DELETE nicht dokumentiert (wird getestet)
"""
import asyncio
import json
import sys
from services.advoware import AdvowareAPI
from services.espocrm import EspoCRMAPI
# Test-Beteiligter
TEST_BETNR = 104860 # Angela Mustermanns
# KommKz Enum (Kommunikationskennzeichen)
KOMMKZ = {
1: 'TelGesch',
2: 'FaxGesch',
3: 'Mobil',
4: 'MailGesch',
5: 'Internet',
6: 'TelPrivat',
7: 'FaxPrivat',
8: 'MailPrivat',
9: 'AutoTelefon',
10: 'Sonstige',
11: 'EPost',
12: 'Bea'
}
class SimpleContext:
"""Einfacher Context für Logging"""
class Logger:
def info(self, msg): print(f" {msg}")
def error(self, msg): print(f"{msg}")
def warning(self, msg): print(f"⚠️ {msg}")
def debug(self, msg): print(f"🔍 {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70 + "\n")
def print_json(title, data):
print(f"\n{title}:")
print("-" * 70)
print(json.dumps(data, indent=2, ensure_ascii=False))
print()
async def test_get_existing_kommunikationen():
"""Hole bestehende Kommunikationen vom Test-Beteiligten"""
print_section("TEST 1: GET Bestehende Kommunikationen")
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole kompletten Beteiligten
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
# Response ist ein Array (selbst bei einzelnem Beteiligten)
if isinstance(result, list) and len(result) > 0:
beteiligte = result[0]
elif isinstance(result, dict):
beteiligte = result
else:
print(f"❌ Unerwartetes Response-Format: {type(result)}")
return []
kommunikationen = beteiligte.get('kommunikation', [])
print(f"✓ Beteiligter geladen: {beteiligte.get('name')} {beteiligte.get('vorname')}")
print(f"✓ Kommunikationen gefunden: {len(kommunikationen)}")
if kommunikationen:
print_json("Bestehende Kommunikationen", kommunikationen)
# Analysiere Felder
first = kommunikationen[0]
print("📊 Felder-Analyse (erste Kommunikation):")
for key, value in first.items():
print(f" - {key}: {value} ({type(value).__name__})")
else:
print(" Keine Kommunikationen vorhanden")
return kommunikationen
async def test_post_kommunikation():
"""Teste POST - Neue Kommunikation erstellen"""
print_section("TEST 2: POST - Neue Kommunikation erstellen")
context = SimpleContext()
advo = AdvowareAPI(context)
# Test verschiedene KommKz Typen
test_cases = [
{
'name': 'Geschäftstelefon',
'data': {
'kommKz': 1, # TelGesch
'tlf': '+49 511 123456-10',
'bemerkung': 'TEST: Hauptnummer',
'online': False
}
},
{
'name': 'Geschäfts-Email',
'data': {
'kommKz': 4, # MailGesch
'tlf': 'test@example.com',
'bemerkung': 'TEST: Email',
'online': True
}
},
{
'name': 'Mobiltelefon',
'data': {
'kommKz': 3, # Mobil
'tlf': '+49 170 1234567',
'bemerkung': 'TEST: Mobil',
'online': False
}
}
]
created_ids = []
for test in test_cases:
print(f"\n📝 Erstelle: {test['name']}")
print_json("Request Payload", test['data'])
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen',
method='POST',
data=test['data']
)
print_json("Response", result)
# Extrahiere ID
if isinstance(result, list) and len(result) > 0:
created_id = result[0].get('id')
created_ids.append(created_id)
print(f"✅ Erstellt mit ID: {created_id}")
elif isinstance(result, dict):
created_id = result.get('id')
created_ids.append(created_id)
print(f"✅ Erstellt mit ID: {created_id}")
else:
print(f"❌ Unerwartetes Response-Format: {type(result)}")
except Exception as e:
print(f"❌ Fehler: {e}")
return created_ids
async def test_put_kommunikation(komm_id):
"""Teste PUT - Kommunikation aktualisieren"""
print_section(f"TEST 3: PUT - Kommunikation {komm_id} aktualisieren")
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole aktuelle Daten
print("📥 Lade aktuelle Kommunikation...")
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
# Response ist ein Array
if isinstance(result, list) and len(result) > 0:
beteiligte = result[0]
elif isinstance(result, dict):
beteiligte = result
else:
print(f"❌ Unerwartetes Response-Format")
return False
kommunikationen = beteiligte.get('kommunikation', [])
current_komm = next((k for k in kommunikationen if k.get('id') == komm_id), None)
if not current_komm:
print(f"❌ Kommunikation {komm_id} nicht gefunden!")
return False
print_json("Aktuelle Daten", current_komm)
# Test 1: Ändere tlf-Feld
print("\n🔄 Test 1: Ändere tlf (Telefonnummer/Email)")
update_data = {
'kommKz': current_komm['kommKz'],
'tlf': '+49 511 999999-99', # Neue Nummer
'bemerkung': current_komm.get('bemerkung', ''),
'online': current_komm.get('online', False)
}
print_json("Update Payload", update_data)
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print_json("Response", result)
print("✅ tlf erfolgreich geändert")
except Exception as e:
print(f"❌ Fehler: {e}")
return False
# Test 2: Ändere bemerkung
print("\n🔄 Test 2: Ändere bemerkung")
update_data['bemerkung'] = 'TEST: Geändert via API'
print_json("Update Payload", update_data)
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print_json("Response", result)
print("✅ bemerkung erfolgreich geändert")
except Exception as e:
print(f"❌ Fehler: {e}")
return False
# Test 3: Ändere kommKz (Typ)
print("\n🔄 Test 3: Ändere kommKz (Kommunikationstyp)")
update_data['kommKz'] = 6 # TelPrivat statt TelGesch
print_json("Update Payload", update_data)
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print_json("Response", result)
print("✅ kommKz erfolgreich geändert")
except Exception as e:
print(f"❌ Fehler: {e}")
return False
# Test 4: Ändere online-Flag
print("\n🔄 Test 4: Ändere online-Flag")
update_data['online'] = not update_data['online']
print_json("Update Payload", update_data)
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print_json("Response", result)
print("✅ online erfolgreich geändert")
except Exception as e:
print(f"❌ Fehler: {e}")
return False
return True
async def test_delete_kommunikation(komm_id):
"""Teste DELETE - Kommunikation löschen"""
print_section(f"TEST 4: DELETE - Kommunikation {komm_id} löschen")
context = SimpleContext()
advo = AdvowareAPI(context)
print(f"🗑️ Versuche Kommunikation {komm_id} zu löschen...")
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='DELETE'
)
print_json("Response", result)
print("✅ DELETE erfolgreich!")
return True
except Exception as e:
print(f"❌ DELETE fehlgeschlagen: {e}")
# Check ob 403 Forbidden (wie bei Adressen)
if '403' in str(e):
print("⚠️ DELETE ist FORBIDDEN (wie bei Adressen)")
return False
async def main():
print("\n" + "="*70)
print("ADVOWARE KOMMUNIKATION API - VOLLSTÄNDIGER TEST")
print("="*70)
print(f"\nTest-Beteiligter: {TEST_BETNR}")
print("\nKommKz (Kommunikationskennzeichen):")
for kz, name in KOMMKZ.items():
print(f" {kz:2d} = {name}")
try:
# TEST 1: GET bestehende
existing = await test_get_existing_kommunikationen()
# TEST 2: POST neue
created_ids = await test_post_kommunikation()
if not created_ids:
print("\n❌ Keine Kommunikationen erstellt - Tests abgebrochen")
return
# TEST 3: PUT update (erste erstellte)
first_id = created_ids[0]
await test_put_kommunikation(first_id)
# TEST 4: DELETE (erste erstellte)
await test_delete_kommunikation(first_id)
# Finale Übersicht
print_section("ZUSAMMENFASSUNG")
print("✅ POST: Funktioniert (3 Typen getestet)")
print("✅ GET: Funktioniert (über Beteiligte-Endpoint)")
print("✓/✗ PUT: Siehe Testergebnisse oben")
print("✓/✗ DELETE: Siehe Testergebnisse oben")
print("\n⚠️ WICHTIG:")
print(f" - Test-Kommunikationen in Advoware manuell prüfen!")
print(f" - BetNr: {TEST_BETNR}")
print(" - Suche nach: 'TEST:'")
if len(created_ids) > 1:
print(f"\n📝 Erstellt wurden IDs: {created_ids}")
print(" Falls DELETE nicht funktioniert, manuell löschen!")
except Exception as e:
print(f"\n❌ Unerwarteter Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,252 @@
"""
Tiefenanalyse: kommKz Feld-Verhalten
Beobachtung:
- PUT Response zeigt kommKz: 1
- Nachfolgender GET zeigt kommKz: 0 (!)
- 0 ist kein gültiger kommKz-Wert (1-12)
Test: Prüfe ob kommKz überhaupt korrekt gespeichert/gelesen wird
"""
import asyncio
import json
from services.advoware import AdvowareAPI
TEST_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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_kommkz_behavior():
"""Teste kommKz Verhalten in Detail"""
context = SimpleContext()
advo = AdvowareAPI(context)
# SCHRITT 1: Erstelle mit kommKz=3 (Mobil)
print_section("SCHRITT 1: CREATE mit kommKz=3 (Mobil)")
create_data = {
'kommKz': 3, # Mobil
'tlf': '+49 170 999-TEST',
'bemerkung': 'TEST-DEEP: Initial kommKz=3',
'online': False
}
print(f"📤 CREATE Request:")
print(json.dumps(create_data, indent=2))
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen',
method='POST',
data=create_data
)
if isinstance(result, list):
created = result[0]
else:
created = result
komm_id = created['id']
print(f"\n✅ POST Response:")
print(f" id: {created['id']}")
print(f" kommKz: {created['kommKz']}")
print(f" kommArt: {created['kommArt']}")
print(f" tlf: {created['tlf']}")
print(f" bemerkung: {created['bemerkung']}")
# SCHRITT 2: Sofortiger GET nach CREATE
print_section("SCHRITT 2: GET direkt nach CREATE")
beteiligte = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
if isinstance(beteiligte, list):
beteiligte = beteiligte[0]
kommunikationen = beteiligte.get('kommunikation', [])
get_komm = next((k for k in kommunikationen if k['id'] == komm_id), None)
if get_komm:
print(f"📥 GET Response:")
print(f" id: {get_komm['id']}")
print(f" kommKz: {get_komm['kommKz']}")
print(f" kommArt: {get_komm['kommArt']}")
print(f" tlf: {get_komm['tlf']}")
print(f" bemerkung: {get_komm['bemerkung']}")
if get_komm['kommKz'] != 3:
print(f"\n⚠️ WARNUNG: kommKz nach CREATE stimmt nicht!")
print(f" Erwartet: 3")
print(f" Tatsächlich: {get_komm['kommKz']}")
# SCHRITT 3: PUT mit gleichem kommKz (keine Änderung)
print_section("SCHRITT 3: PUT mit gleichem kommKz=3")
update_data = {
'kommKz': 3, # GLEICH wie original
'tlf': '+49 170 999-TEST',
'bemerkung': 'TEST-DEEP: PUT mit gleichem kommKz=3',
'online': False
}
print(f"📤 PUT Request (keine kommKz-Änderung):")
print(json.dumps(update_data, indent=2))
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print(f"\n✅ PUT Response:")
print(f" kommKz: {result['kommKz']}")
print(f" kommArt: {result['kommArt']}")
print(f" bemerkung: {result['bemerkung']}")
# GET nach PUT
print(f"\n🔍 GET nach PUT:")
beteiligte = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
if isinstance(beteiligte, list):
beteiligte = beteiligte[0]
kommunikationen = beteiligte.get('kommunikation', [])
get_komm = next((k for k in kommunikationen if k['id'] == komm_id), None)
if get_komm:
print(f" kommKz: {get_komm['kommKz']}")
print(f" kommArt: {get_komm['kommArt']}")
print(f" bemerkung: {get_komm['bemerkung']}")
# SCHRITT 4: PUT mit ANDEREM kommKz
print_section("SCHRITT 4: PUT mit kommKz=7 (FaxPrivat)")
update_data = {
'kommKz': 7, # ÄNDERN: Mobil → FaxPrivat
'tlf': '+49 170 999-TEST',
'bemerkung': 'TEST-DEEP: Versuch kommKz 3→7',
'online': False
}
print(f"📤 PUT Request (kommKz-Änderung 3→7):")
print(json.dumps(update_data, indent=2))
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=update_data
)
print(f"\n✅ PUT Response:")
print(f" kommKz: {result['kommKz']}")
print(f" kommArt: {result['kommArt']}")
print(f" bemerkung: {result['bemerkung']}")
# GET nach PUT mit Änderungsversuch
print(f"\n🔍 GET nach PUT (mit Änderungsversuch):")
beteiligte = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
if isinstance(beteiligte, list):
beteiligte = beteiligte[0]
kommunikationen = beteiligte.get('kommunikation', [])
get_komm = next((k for k in kommunikationen if k['id'] == komm_id), None)
if get_komm:
print(f" kommKz: {get_komm['kommKz']}")
print(f" kommArt: {get_komm['kommArt']}")
print(f" bemerkung: {get_komm['bemerkung']}")
print(f"\n📊 Zusammenfassung für ID {komm_id}:")
print(f" CREATE Request: kommKz=3")
print(f" CREATE Response: kommKz={created['kommKz']}")
print(f" GET nach CREATE: kommKz={kommunikationen[0].get('kommKz', 'N/A') if kommunikationen else 'N/A'}")
print(f" PUT Request (change): kommKz=7")
print(f" PUT Response: kommKz={result['kommKz']}")
print(f" GET nach PUT: kommKz={get_komm['kommKz']}")
if get_komm['kommKz'] == 7:
print(f"\n✅ kommKz wurde geändert auf 7!")
elif get_komm['kommKz'] == 3:
print(f"\n❌ kommKz blieb bei 3 (READ-ONLY bestätigt)")
elif get_komm['kommKz'] == 0:
print(f"\n⚠️ kommKz ist 0 (ungültiger Wert - möglicherweise Bug in API)")
else:
print(f"\n⚠️ kommKz hat unerwarteten Wert: {get_komm['kommKz']}")
# SCHRITT 5: Vergleiche mit bestehenden Kommunikationen
print_section("SCHRITT 5: Vergleich mit bestehenden Kommunikationen")
print(f"\nAlle Kommunikationen von Beteiligten {TEST_BETNR}:")
for i, k in enumerate(kommunikationen):
print(f"\n [{i+1}] ID: {k['id']}")
print(f" kommKz: {k['kommKz']}")
print(f" kommArt: {k['kommArt']}")
print(f" tlf: {k.get('tlf', '')[:40]}")
print(f" bemerkung: {k.get('bemerkung', '')[:40] if k.get('bemerkung') else 'null'}")
print(f" online: {k.get('online')}")
# Prüfe auf Inkonsistenzen
if k['kommKz'] == 0 and k['kommArt'] != 0:
print(f" ⚠️ INKONSISTENZ: kommKz=0 aber kommArt={k['kommArt']}")
print(f"\n⚠️ Test-Kommunikation {komm_id} manuell löschen!")
return komm_id
async def main():
print("\n" + "="*70)
print("TIEFENANALYSE: kommKz Feld-Verhalten")
print("="*70)
print("\nZiel: Verstehen warum GET kommKz=0 zeigt")
print("Methode: Schrittweise CREATE/PUT/GET mit detailliertem Tracking\n")
try:
komm_id = await test_kommkz_behavior()
print_section("FAZIT")
print("\n📌 Erkenntnisse:")
print(" 1. POST Response zeigt den gesendeten kommKz")
print(" 2. PUT Response zeigt oft den gesendeten kommKz")
print(" 3. GET Response zeigt den TATSÄCHLICH gespeicherten Wert")
print(" 4. kommKz=0 in GET deutet auf ein Problem hin")
print("\n💡 Empfehlung:")
print(" - Immer GET nach PUT für Verifizierung")
print(" - Nicht auf PUT Response verlassen")
print(" - kommKz ist definitiv READ-ONLY bei PUT")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,395 @@
"""
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())

View File

@@ -0,0 +1,350 @@
"""
Detaillierte Analyse: Welche Felder sind bei PUT änderbar?
Basierend auf ersten Tests:
- POST funktioniert (alle 4 Felder)
- PUT funktioniert TEILWEISE
- DELETE = 403 Forbidden (wie bei Adressen/Bankverbindungen)
Felder laut Swagger:
- tlf (string, nullable)
- bemerkung (string, nullable)
- kommKz (enum/int)
- online (boolean)
Response enthält zusätzlich:
- id (int) - Kommunikations-ID
- betNr (int) - Beteiligten-ID
- kommArt (int) - Scheint von kommKz generiert zu werden
- rowId (string) - Änderungserkennung
"""
import asyncio
import json
from services.advoware import AdvowareAPI
TEST_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): print(f"[DEBUG] {msg}")
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def test_field_mutability():
"""Teste welche Felder bei PUT änderbar sind"""
context = SimpleContext()
advo = AdvowareAPI(context)
# STEP 1: Erstelle Test-Kommunikation
print_section("STEP 1: Erstelle Test-Kommunikation")
create_data = {
'kommKz': 1, # TelGesch
'tlf': '+49 511 000000-00',
'bemerkung': 'TEST-READONLY: Initial',
'online': False
}
print(f"📤 POST Data: {json.dumps(create_data, indent=2)}")
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen',
method='POST',
data=create_data
)
if isinstance(result, list) and len(result) > 0:
created = result[0]
else:
created = result
komm_id = created['id']
original_rowid = created['rowId']
print(f"\n✅ Erstellt:")
print(f" ID: {komm_id}")
print(f" rowId: {original_rowid}")
print(f" kommArt: {created['kommArt']}")
print(f"\n📋 Vollständige Response:")
print(json.dumps(created, indent=2, ensure_ascii=False))
# STEP 2: Teste jedes Feld einzeln
print_section("STEP 2: Teste Feld-Änderbarkeit")
test_results = {}
# Test 1: tlf
print("\n🔬 Test 1/4: tlf (Telefonnummer/Email)")
print(" Änderung: '+49 511 000000-00''+49 511 111111-11'")
test_data = {
'kommKz': created['kommKz'],
'tlf': '+49 511 111111-11', # GEÄNDERT
'bemerkung': created['bemerkung'],
'online': created['online']
}
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=test_data
)
new_rowid = result['rowId']
rowid_changed = (new_rowid != original_rowid)
value_changed = (result['tlf'] == '+49 511 111111-11')
print(f" ✅ PUT erfolgreich")
print(f" 📊 Wert geändert: {value_changed}")
print(f" 📊 rowId geändert: {rowid_changed}")
print(f" Alt: {original_rowid}")
print(f" Neu: {new_rowid}")
test_results['tlf'] = {
'writable': value_changed,
'rowid_changed': rowid_changed,
'status': 'WRITABLE' if value_changed else 'READ-ONLY'
}
original_rowid = new_rowid # Update für nächsten Test
except Exception as e:
print(f" ❌ FEHLER: {e}")
test_results['tlf'] = {'writable': False, 'status': 'ERROR', 'error': str(e)}
# Test 2: bemerkung
print("\n🔬 Test 2/4: bemerkung")
print(" Änderung: 'TEST-READONLY: Initial''TEST-READONLY: Modified'")
test_data = {
'kommKz': created['kommKz'],
'tlf': result['tlf'], # Aktueller Wert
'bemerkung': 'TEST-READONLY: Modified', # GEÄNDERT
'online': result['online']
}
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=test_data
)
new_rowid = result['rowId']
rowid_changed = (new_rowid != original_rowid)
value_changed = (result['bemerkung'] == 'TEST-READONLY: Modified')
print(f" ✅ PUT erfolgreich")
print(f" 📊 Wert geändert: {value_changed}")
print(f" 📊 rowId geändert: {rowid_changed}")
test_results['bemerkung'] = {
'writable': value_changed,
'rowid_changed': rowid_changed,
'status': 'WRITABLE' if value_changed else 'READ-ONLY'
}
original_rowid = new_rowid
except Exception as e:
print(f" ❌ FEHLER: {e}")
test_results['bemerkung'] = {'writable': False, 'status': 'ERROR', 'error': str(e)}
# Test 3: kommKz
print("\n🔬 Test 3/4: kommKz (Kommunikationstyp)")
original_kommkz = result['kommKz']
target_kommkz = 6
print(f" Änderung: {original_kommkz} (TelGesch) → {target_kommkz} (TelPrivat)")
test_data = {
'kommKz': target_kommkz, # GEÄNDERT
'tlf': result['tlf'],
'bemerkung': f"TEST-READONLY: Versuch kommKz {original_kommkz}{target_kommkz}",
'online': result['online']
}
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=test_data
)
new_rowid = result['rowId']
rowid_changed = (new_rowid != original_rowid)
value_changed = (result['kommKz'] == target_kommkz)
print(f" ✅ PUT erfolgreich")
print(f" 📊 PUT Response kommKz: {result['kommKz']}")
print(f" 📊 PUT Response kommArt: {result['kommArt']}")
print(f" 📊 rowId geändert: {rowid_changed}")
# WICHTIG: Nachfolgender GET zur Verifizierung
print(f"\n 🔍 Verifizierung via GET...")
beteiligte_get = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}',
method='GET'
)
if isinstance(beteiligte_get, list):
beteiligte_get = beteiligte_get[0]
kommunikationen_get = beteiligte_get.get('kommunikation', [])
verify_komm = next((k for k in kommunikationen_get if k['id'] == komm_id), None)
if verify_komm:
print(f" 📋 GET Response kommKz: {verify_komm['kommKz']}")
print(f" 📋 GET Response kommArt: {verify_komm['kommArt']}")
print(f" 📋 GET Response bemerkung: {verify_komm['bemerkung']}")
# Finale Bewertung basierend auf GET
actual_value_changed = (verify_komm['kommKz'] == target_kommkz)
if actual_value_changed:
print(f" ✅ BESTÄTIGT: kommKz wurde geändert auf {target_kommkz}")
else:
print(f" ❌ BESTÄTIGT: kommKz blieb bei {verify_komm['kommKz']} (nicht geändert!)")
test_results['kommKz'] = {
'writable': actual_value_changed,
'rowid_changed': rowid_changed,
'status': 'WRITABLE' if actual_value_changed else 'READ-ONLY',
'requested_value': target_kommkz,
'put_response_value': result['kommKz'],
'get_response_value': verify_komm['kommKz'],
'note': f"PUT sagte: {result['kommKz']}, GET sagte: {verify_komm['kommKz']}"
}
else:
print(f" ⚠️ Kommunikation nicht in GET gefunden")
test_results['kommKz'] = {
'writable': False,
'status': 'ERROR',
'error': 'Not found in GET'
}
original_rowid = new_rowid
except Exception as e:
print(f" ❌ FEHLER: {e}")
test_results['kommKz'] = {'writable': False, 'status': 'ERROR', 'error': str(e)}
# Test 4: online
print("\n🔬 Test 4/4: online (Boolean Flag)")
print(" Änderung: False → True")
test_data = {
'kommKz': result['kommKz'],
'tlf': result['tlf'],
'bemerkung': result['bemerkung'],
'online': True # GEÄNDERT
}
try:
result = await advo.api_call(
f'api/v1/advonet/Beteiligte/{TEST_BETNR}/Kommunikationen/{komm_id}',
method='PUT',
data=test_data
)
new_rowid = result['rowId']
rowid_changed = (new_rowid != original_rowid)
value_changed = (result['online'] == True)
print(f" ✅ PUT erfolgreich")
print(f" 📊 Wert geändert: {value_changed}")
print(f" 📊 rowId geändert: {rowid_changed}")
test_results['online'] = {
'writable': value_changed,
'rowid_changed': rowid_changed,
'status': 'WRITABLE' if value_changed else 'READ-ONLY'
}
except Exception as e:
print(f" ❌ FEHLER: {e}")
test_results['online'] = {'writable': False, 'status': 'ERROR', 'error': str(e)}
# ZUSAMMENFASSUNG
print_section("ZUSAMMENFASSUNG: Feld-Status")
print("\n📊 Ergebnisse:\n")
for field, result in test_results.items():
status = result['status']
icon = "" if status == "WRITABLE" else "" if status == "READ-ONLY" else "⚠️"
print(f" {icon} {field:15s}{status}")
if result.get('note'):
print(f" {result['note']}")
if result.get('error'):
print(f" ⚠️ {result['error']}")
# Count
writable = sum(1 for r in test_results.values() if r['status'] == 'WRITABLE')
readonly = sum(1 for r in test_results.values() if r['status'] == 'READ-ONLY')
print(f"\n📈 Statistik:")
print(f" WRITABLE: {writable}/{len(test_results)} Felder")
print(f" READ-ONLY: {readonly}/{len(test_results)} Felder")
print(f"\n⚠️ Test-Kommunikation {komm_id} manuell löschen!")
print(f" BetNr: {TEST_BETNR}")
return test_results
async def main():
print("\n" + "="*70)
print("KOMMUNIKATION API - FELDANALYSE")
print("="*70)
print("\nZiel: Herausfinden welche Felder bei PUT änderbar sind")
print("Methode: Einzelne Feldänderungen + rowId-Tracking\n")
try:
results = await test_field_mutability()
print_section("EMPFEHLUNG FÜR MAPPER")
writable_fields = [f for f, r in results.items() if r['status'] == 'WRITABLE']
readonly_fields = [f for f, r in results.items() if r['status'] == 'READ-ONLY']
if writable_fields:
print("\n✅ Für UPDATE (PUT) verwenden:")
for field in writable_fields:
print(f" - {field}")
if readonly_fields:
print("\n❌ NUR bei CREATE (POST) verwenden:")
for field in readonly_fields:
print(f" - {field}")
print("\n💡 Sync-Strategie:")
print(" - CREATE: Alle Felder")
print(" - UPDATE: Nur WRITABLE Felder")
print(" - DELETE: Notification (403 Forbidden)")
except Exception as e:
print(f"\n❌ Fehler: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""
Test Kommunikation Sync Implementation
Testet alle 4 Szenarien + Type Detection
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from services.kommunikation_mapper import (
encode_value, decode_value, parse_marker, create_marker, create_slot_marker,
detect_kommkz, is_email_type, is_phone_type,
KOMMKZ_TEL_GESCH, KOMMKZ_MAIL_GESCH
)
def test_base64_encoding():
"""Test: Base64-Encoding/Decoding"""
print("\n=== TEST 1: Base64-Encoding/Decoding ===")
# Email
value1 = "max@example.com"
encoded1 = encode_value(value1)
decoded1 = decode_value(encoded1)
print(f"✓ Email: '{value1}''{encoded1}''{decoded1}'")
assert decoded1 == value1, "Decode muss Original ergeben"
# Phone
value2 = "+49 170 999-TEST"
encoded2 = encode_value(value2)
decoded2 = decode_value(encoded2)
print(f"✓ Phone: '{value2}''{encoded2}''{decoded2}'")
assert decoded2 == value2, "Decode muss Original ergeben"
# Special characters
value3 = "test:special]@example.com"
encoded3 = encode_value(value3)
decoded3 = decode_value(encoded3)
print(f"✓ Special: '{value3}''{encoded3}''{decoded3}'")
assert decoded3 == value3, "Decode muss Original ergeben"
print("✅ Base64-Encoding bidirektional funktioniert")
def test_marker_parsing():
"""Test: Marker-Parsing mit Base64"""
print("\n=== TEST 2: Marker-Parsing ===")
# Standard Marker mit Base64
value = "max@example.com"
encoded = encode_value(value)
bemerkung1 = f"[ESPOCRM:{encoded}:4] Geschäftlich"
marker1 = parse_marker(bemerkung1)
print(f"✓ Parsed: {marker1}")
assert marker1['synced_value'] == value
assert marker1['kommKz'] == 4
assert marker1['is_slot'] == False
assert marker1['user_text'] == 'Geschäftlich'
print("✅ Standard-Marker OK")
# Slot Marker
bemerkung2 = "[ESPOCRM-SLOT:1]"
marker2 = parse_marker(bemerkung2)
print(f"✓ Parsed Slot: {marker2}")
assert marker2['is_slot'] == True
assert marker2['kommKz'] == 1
print("✅ Slot-Marker OK")
# Kein Marker
bemerkung3 = "Nur normale Bemerkung"
marker3 = parse_marker(bemerkung3)
assert marker3 is None
print("✅ Nicht-Marker erkannt")
def test_marker_creation():
"""Test: Marker-Erstellung mit Base64"""
print("\n=== TEST 3: Marker-Erstellung ===")
value = "max@example.com"
kommkz = 4
user_text = "Geschäftlich"
marker = create_marker(value, kommkz, user_text)
print(f"✓ Created Marker: {marker}")
# Verify parsable
parsed = parse_marker(marker)
assert parsed is not None
assert parsed['synced_value'] == value
assert parsed['kommKz'] == kommkz
assert parsed['user_text'] == user_text
print("✅ Marker korrekt erstellt und parsbar")
# Slot Marker
slot_marker = create_slot_marker(kommkz)
print(f"✓ Created Slot: {slot_marker}")
parsed_slot = parse_marker(slot_marker)
assert parsed_slot['is_slot'] == True
print("✅ Slot-Marker OK")
def test_type_detection_4_tiers():
"""Test: 4-Stufen Typ-Erkennung"""
print("\n=== TEST 4: 4-Stufen Typ-Erkennung ===")
# TIER 1: Aus Marker (höchste Priorität)
value = "test@example.com"
bemerkung_with_marker = "[ESPOCRM:abc:3]" # Marker sagt Mobil (3)
beteiligte = {'emailGesch': value} # Top-Level sagt MailGesch (4)
detected = detect_kommkz(value, beteiligte, bemerkung_with_marker)
print(f"✓ Tier 1 (Marker): {detected} (erwartet 3 = Mobil)")
assert detected == 3, "Marker sollte höchste Priorität haben"
print("✅ Tier 1 OK - Marker überschreibt alles")
# TIER 2: Aus Top-Level Feldern
beteiligte = {'telGesch': '+49 123 456'}
detected = detect_kommkz('+49 123 456', beteiligte, None)
print(f"✓ Tier 2 (Top-Level): {detected} (erwartet 1 = TelGesch)")
assert detected == 1
print("✅ Tier 2 OK - Top-Level Match")
# TIER 3: Aus Wert-Pattern
email_value = "no-marker@example.com"
detected = detect_kommkz(email_value, {}, None)
print(f"✓ Tier 3 (Pattern @ = Email): {detected} (erwartet 4)")
assert detected == 4
print("✅ Tier 3 OK - Email erkannt")
phone_value = "+49 123"
detected = detect_kommkz(phone_value, {}, None)
print(f"✓ Tier 3 (Pattern Phone): {detected} (erwartet 1)")
assert detected == 1
print("✅ Tier 3 OK - Phone erkannt")
# TIER 4: Default
detected = detect_kommkz('', {}, None)
print(f"✓ Tier 4 (Default): {detected} (erwartet 0)")
assert detected == 0
print("✅ Tier 4 OK - Default bei leerem Wert")
def test_type_classification():
"""Test: Email vs. Phone Klassifizierung"""
print("\n=== TEST 5: Typ-Klassifizierung ===")
email_types = [4, 8, 11, 12] # MailGesch, MailPrivat, EPost, Bea
phone_types = [1, 2, 3, 6, 7, 9, 10] # Alle Telefon-Typen
for kommkz in email_types:
assert is_email_type(kommkz), f"kommKz {kommkz} sollte Email sein"
assert not is_phone_type(kommkz), f"kommKz {kommkz} sollte nicht Phone sein"
print(f"✅ Email-Typen: {email_types}")
for kommkz in phone_types:
assert is_phone_type(kommkz), f"kommKz {kommkz} sollte Phone sein"
assert not is_email_type(kommkz), f"kommKz {kommkz} sollte nicht Email sein"
print(f"✅ Phone-Typen: {phone_types}")
def test_integration_scenario():
"""Test: Integration Szenario mit Base64"""
print("\n=== TEST 6: Integration Szenario ===")
# Szenario: Neue Email in EspoCRM
espo_email = "new@example.com"
# Schritt 1: Erkenne Typ (kein Marker, keine Top-Level Match)
kommkz = detect_kommkz(espo_email, {}, None)
print(f"✓ Erkannte kommKz: {kommkz} (MailGesch)")
assert kommkz == 4
# Schritt 2: Erstelle Marker mit Base64
marker = create_marker(espo_email, kommkz)
print(f"✓ Marker erstellt: {marker}")
# Schritt 3: Simuliere späteren Lookup
parsed = parse_marker(marker)
assert parsed['synced_value'] == espo_email
print(f"✓ Value-Match: {parsed['synced_value']}")
# Schritt 4: Simuliere Änderung in Advoware
# User ändert zu "changed@example.com" aber Marker bleibt
# → synced_value enthält noch "new@example.com" für Matching!
old_synced_value = parsed['synced_value']
new_value = "changed@example.com"
print(f"✓ Änderung erkannt: synced_value='{old_synced_value}' vs current='{new_value}'")
assert old_synced_value != new_value
# Schritt 5: Nach Sync wird Marker aktualisiert
new_marker = create_marker(new_value, kommkz, "Geschäftlich")
print(f"✓ Neuer Marker nach Änderung: {new_marker}")
# Verify User-Text erhalten
assert "Geschäftlich" in new_marker
new_parsed = parse_marker(new_marker)
assert new_parsed['synced_value'] == new_value
print("✅ Integration Szenario mit bidirektionalem Matching erfolgreich")
def test_top_level_priority():
"""Test: Top-Level Feld Priorität"""
print("\n=== TEST 7: Top-Level Feld Priorität ===")
# Value matched mit Top-Level Feld
value = "+49 170 999-TEST"
beteiligte = {
'telGesch': '+49 511 111-11',
'mobil': '+49 170 999-TEST', # Match!
'emailGesch': 'test@example.com'
}
detected = detect_kommkz(value, beteiligte, None)
print(f"✓ Detected für '{value}': {detected}")
print(f" Beteiligte Top-Level: telGesch={beteiligte['telGesch']}, mobil={beteiligte['mobil']}")
assert detected == 3, "Sollte Mobil (3) erkennen via Top-Level Match"
print("✅ Top-Level Match funktioniert")
# Kein Match → Fallback zu Pattern
value2 = "+49 999 UNKNOWN"
detected2 = detect_kommkz(value2, beteiligte, None)
print(f"✓ Detected für '{value2}' (kein Match): {detected2}")
assert detected2 == 1, "Sollte TelGesch (1) als Pattern-Fallback nehmen"
print("✅ base64_encodingern funktioniert")
if __name__ == '__main__':
print("=" * 60)
print("KOMMUNIKATION SYNC - IMPLEMENTATION TESTS")
print("=" * 60)
try:
test_base64_encoding()
test_marker_parsing()
test_marker_creation()
test_type_detection_4_tiers()
test_type_classification()
test_integration_scenario()
test_top_level_priority()
print("\n" + "=" * 60)
print("✅ ALLE TESTS ERFOLGREICH")
print("=" * 60)
except AssertionError as e:
print(f"\n❌ TEST FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"\n❌ ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,108 @@
"""
Verifikation: Hat Advoware eindeutige IDs für Kommunikationen?
Prüfe:
1. Hat jede Kommunikation eine 'id'?
2. Sind die IDs eindeutig?
3. Bleibt die ID stabil bei UPDATE?
4. Was ist mit rowId?
"""
import asyncio
from services.advoware import AdvowareAPI
TEST_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
def __init__(self):
self.logger = self.Logger()
def print_section(title):
print("\n" + "="*70)
print(title)
print("="*70)
async def main():
print("\n" + "="*70)
print("ADVOWARE KOMMUNIKATION IDs")
print("="*70)
context = SimpleContext()
advo = AdvowareAPI(context)
# Hole Beteiligte mit Kommunikationen
print_section("Aktuelle Kommunikationen")
result = await advo.api_call(f'api/v1/advonet/Beteiligte/{TEST_BETNR}')
beteiligte = result[0]
kommunikationen = beteiligte.get('kommunikation', [])
print(f"\n{len(kommunikationen)} Kommunikationen gefunden\n")
# Zeige alle IDs
ids = []
row_ids = []
for i, k in enumerate(kommunikationen[:10], 1): # Erste 10
komm_id = k.get('id')
row_id = k.get('rowId')
wert = k.get('tlf', '')[:40]
kommkz = k.get('kommKz')
ids.append(komm_id)
row_ids.append(row_id)
print(f"[{i:2d}] ID: {komm_id:8d} | rowId: {row_id:20s} | "
f"Typ: {kommkz:2d} | Wert: {wert}")
# Analyse
print_section("ANALYSE")
print(f"\n1⃣ IDs vorhanden:")
print(f" • Alle haben 'id': {all(k.get('id') for k in kommunikationen)}")
print(f" • Alle haben 'rowId': {all(k.get('rowId') for k in kommunikationen)}")
print(f"\n2⃣ Eindeutigkeit:")
print(f" • Anzahl IDs: {len(ids)}")
print(f" • Anzahl unique IDs: {len(set(ids))}")
print(f" • ✅ IDs sind eindeutig: {len(ids) == len(set(ids))}")
print(f"\n3⃣ ID-Typ:")
print(f" • Beispiel-ID: {ids[0] if ids else 'N/A'}")
print(f" • Typ: {type(ids[0]).__name__ if ids else 'N/A'}")
print(f" • Format: Integer (stabil)")
print(f"\n4⃣ rowId-Typ:")
print(f" • Beispiel-rowId: {row_ids[0] if row_ids else 'N/A'}")
print(f" • Typ: {type(row_ids[0]).__name__ if row_ids else 'N/A'}")
print(f" • Format: Base64 String (ändert sich bei UPDATE)")
print_section("FAZIT")
print("\n✅ Advoware hat EINDEUTIGE IDs für Kommunikationen!")
print("\n📋 Eigenschaften:")
print(" • id: Integer, stabil, eindeutig")
print(" • rowId: String, ändert sich bei UPDATE (für Change Detection)")
print("\n💡 Das bedeutet:")
print(" • Wir können Advoware-ID als Schlüssel nutzen")
print(" • Matching: Advoware-ID ↔ EspoCRM-Wert")
print(" • Speichere Advoware-ID irgendwo für Reverse-Lookup")
print("\n🎯 BESSERE LÖSUNG:")
print(" Option D: Advoware-ID als Kommentar in bemerkung speichern?")
print(" Option E: Advoware-ID in Wert-Format kodieren?")
print(" Option F: Separate Mapping-Tabelle (Redis/DB)?")
if __name__ == "__main__":
asyncio.run(main())