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:
250
bitbylaw/scripts/test_espocrm_id_collections.py
Normal file
250
bitbylaw/scripts/test_espocrm_id_collections.py
Normal 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())
|
||||
Reference in New Issue
Block a user