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,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())