- Fixed initial sync logic to respect actual timestamps, preventing unwanted overwrites. - Introduced exponential backoff for retry logic, with auto-reset for permanently failed entities. - Added validation checks to ensure data consistency during sync processes. - Corrected hash calculation to only include sync-relevant communications. - Resolved issues with empty slots ignoring user inputs and improved conflict handling. - Enhanced handling of Var4 and Var6 entries during sync conflicts. - Documented changes and added new fields required in EspoCRM for improved sync management. Also added a detailed analysis of syncStatus values in EspoCRM CBeteiligte, outlining responsibilities and ensuring robust sync mechanisms.
51 KiB
Adressen-Synchronisation: Analyse EspoCRM ↔ Advoware
Datum: 8. Februar 2026
Status: ✅ Analyse abgeschlossen - Implementierung bereit
Zweck: Evaluierung der Sync-Möglichkeiten für Adressen zwischen EspoCRM und Advoware
⚠️ KRITISCHE ERKENNTNISSE (Executive Summary)
Advoware API-Limitierungen (getestet):
- ❌ DELETE nicht möglich (403 Forbidden)
- ❌ Nur 4 Felder via PUT änderbar:
strasse,plz,ort,anschrift - ❌ READ-ONLY Felder:
land,postfach,postfachPLZ,standardAnschrift,bemerkung,gueltigVon,gueltigBis,reihenfolgeIndex - ✅
bemerkungist stabil (kann für EspoCRM-ID Matching genutzt werden) - ❌
idist immer 0 (unbrauchbar für Mapping) - ⚠️
reihenfolgeIndexwird nach Löschung neu vergeben (NICHT stabil, NICHT als Identifier nutzbar!) - ❌
rowIdändert sich bei jedem PUT (unbrauchbar für Matching)
Konsequenzen:
- ✅ CREATE (POST) funktioniert mit allen Feldern
- ⚠️ UPDATE (PUT) nur für Haupt-Adressfelder
- ❌ DELETE muss manuell in Advoware erfolgenfeat: Update synchronization status options and default values across multiple entity definitions and configuration files
- ❌ Soft-Delete via
gueltigBisnicht möglich (READ-ONLY) - ✅
bemerkungist die EINZIGE stabile Matching-Methode ("EspoCRM-ID: {id}") - ⚠️
reihenfolgeIndexfür PUT-Endpoint muss VOR jedem Update via GET + Match ermittelt werden
Empfohlene Strategie: → Siehe Abschnitt 12 "FINALE SYNC-STRATEGIE"
Quick Summary:
- ✅ CREATE: Vollautomatisch (alle 11 Felder)
- ✅ UPDATE: Nur R/W Felder (strasse, plz, ort, anschrift)
- ❌ DELETE: Notification + manuelle Löschung
- ⚠️ READ-ONLY Änderungen: Notification + manuelle Aktion
1. Advoware Adressen-API (Swagger-basiert)
1.1 Verfügbare Endpoints
Advoware bietet drei REST-Endpoints für Adressen-Management:
GET /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen
- Zweck: Liste aller Adressen eines Beteiligten abrufen
- Auth: AllLoggedIn
- Response: Array von
Adresse-Objekten - Anwendungsfall: Initiale Synchronisation, Polling für Änderungen
POST /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen
- Zweck: Neue Adresse für einen Beteiligten hinzufügen
- Auth: AllLoggedIn
- Request Body:
AdresseParameter(JSON) - Response: Array von
Adresse-Objekten (201 Created) - Anwendungsfall: EspoCRM → Advoware Sync beim Erstellen neuer Adressen
PUT /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen/{adresseId}
- Zweck: Bestehende Adresse ändern
- Auth: Mitarbeiter (⚠️ höhere Berechtigung!)
- Request Body:
AdresseParameter(JSON) - Response: Aktualisierte
Adresse(200 OK) - Anwendungsfall: EspoCRM → Advoware Sync beim Update
Wichtig: Es gibt keinen DELETE-Endpoint in der Swagger-Definition!
2. Advoware Adressen-Schema
2.1 Adresse (Response-Objekt)
Vollständiges Schema mit allen Feldern:
{
"id": 123, // int32 - Adress-ID
"beteiligterId": 104860, // int32 - Referenz zu Beteiligter
"reihenfolgeIndex": 1, // int32 - Sortierung
"rowId": "abc123...", // string - Änderungserkennung
"strasse": "Musterstraße 123", // string (max 50) - Straße + Hausnummer
"plz": "30159", // string (max 20) - Postleitzahl
"ort": "Hannover", // string (max 39) - Ort
"land": "DE", // string (max 3) - Ländercode (ISO 3166)
"postfach": "10 20 30", // string (max 20) - Postfach
"postfachPLZ": "30001", // string (max 6) - Postfach PLZ
"anschrift": "...", // string (max 250) - Formatierte Adresse
"standardAnschrift": true, // boolean - Ist Hauptadresse?
"bemerkung": "Privat", // string (max 250) - Kommentar
"gueltigVon": "2020-01-01T00:00:00", // datetime - Gültigkeit von
"gueltigBis": "2025-12-31T00:00:00" // datetime - Gültigkeit bis
}
2.2 AdresseParameter (Request-Objekt)
Für POST/PUT:
{
"strasse": "Neue Straße 456",
"plz": "10115",
"ort": "Berlin",
"land": "DE",
"postfach": null,
"postfachPLZ": null,
"anschrift": "Neue Straße 456\n10115 Berlin",
"standardAnschrift": false,
"bemerkung": "Geschäftsadresse",
"gueltigVon": "2026-02-08T00:00:00",
"gueltigBis": null
}
Felder-Unterschiede:
id,beteiligterId,reihenfolgeIndex,rowIdwerden nicht im Request mitgeschickt (server-generiert)
3. EspoCRM Adressen-Konzept
3.1 EspoCRM Adressmodell (✅ Verifiziert via API)
EspoCRM hat ein Custom Entity CAdressen mit Beziehung zu CBeteiligte.
Status: ✅ Existiert bereits! (5 Adressen in Datenbank)
CAdressen Entity-Struktur
{
"id": "696f71061b5cbb8ec",
"name": "Wund",
"deleted": false,
// 📍 Adressfelder
"adresseStreet": "Ehlbeek 3",
"adresseCity": "Burgwedel",
"adressePostalCode": "30938",
"adresseCountry": "DE",
"adresseState": null,
// 🔗 Beziehung zu CBeteiligte
"beteiligteId": "68e4aef68d2b4fb98",
"beteiligteName": "ssdasd",
// ⚙️ Business-Felder
"isActive": true,
"syncStatus": "clean",
"description": null,
"autoapplymietverhaltnisportaluser": true,
// 🔄 Advoware-Sync-Felder
"adressid": null, // Advoware Adressen-ID
"advowareindexid": null, // Advoware reihenfolgeIndex?
"advowareLastSync": null,
// 📊 Metadaten
"createdAt": "2026-01-20 12:11:50",
"createdById": "68d65929f18c2afef",
"createdByName": "Admin",
"modifiedAt": "2026-01-20 12:11:59",
"modifiedById": "68d65929f18c2afef",
"modifiedByName": "Admin",
// Teams & Followers
"teamsIds": [],
"teamsNames": {},
"followersIds": [],
"followersNames": {},
"assignedUserId": null,
"assignedUserName": null,
"isFollowed": false,
"streamUpdatedAt": null
}
Vorteile:
- ✅ Beliebig viele Adressen pro Beteiligtem
- ✅ Sync-Felder bereits vorhanden (
adressid,advowareLastSync,syncStatus) - ✅ Standard EspoCRM-Features (Teams, Followers, Assignments)
Beziehung zu CBeteiligte
// CBeteiligte hat Link-Felder:
{
"id": "6987b30a9bbbfefd0",
"name": "OptTest86 Musterfrau3",
"adressensIds": [], // Array von CAdressen-IDs
"adressensNames": {} // Name-Mapping
}
4. Mapping: EspoCRM CAdressen ↔ Advoware Adresse
4.1 Feld-Mapping (✅ Verifiziert)
| EspoCRM Feld | Advoware Feld | Mapping-Typ | Hinweise |
|---|---|---|---|
adresseStreet |
strasse |
✓ Direkt | Straße + Hausnummer |
adresseCity |
ort |
✓ Direkt | Ort |
adressePostalCode |
plz |
✓ Direkt | Postleitzahl |
adresseCountry |
land |
⚠️ Transform | DE ↔ Deutschland |
adresseState |
- | ❌ Nicht gemappt | Bundesland (nur EspoCRM) |
beteiligteId |
beteiligterId |
✓ Direkt | Link zu Beteiligtem |
name |
- | ➜ EspoCRM | Adress-Bezeichnung |
isActive |
- | ➜ EspoCRM | Aktiv-Status |
description |
bemerkung |
⚠️ Möglich | Beschreibung/Bemerkung |
| - | postfach |
← Nur Advoware | Postfach |
| - | postfachPLZ |
← Nur Advoware | Postfach PLZ |
| - | anschrift |
← Nur Advoware | Formatierte Adresse |
| - | standardAnschrift |
← Nur Advoware | Ist Hauptadresse? |
| - | gueltigVon |
← Nur Advoware | Gültigkeit von |
| - | gueltigBis |
← Nur Advoware | Gültigkeit bis |
| - | reihenfolgeIndex |
← Nur Advoware | Sortierung |
Sync-Felder in EspoCRM (bereits vorhanden!)
| EspoCRM Feld | Zweck |
|---|---|
adressid |
Advoware Adressen-ID (zum Mapping) |
advowareindexid |
Advoware reihenfolgeIndex |
advowareLastSync |
Timestamp der letzten Synchronisation |
syncStatus |
Status: clean, dirty, error |
4.2 Herausforderungen
4.2.1 Anzahl Adressen ✅ GELÖST
- EspoCRM: ✅ Beliebig viele Adressen (CAdressen-Entity)
- Advoware: ✅ Beliebig viele Adressen
Status: Kein Problem! Beide Systeme unterstützen mehrere Adressen.
4.2.2 Ländercode-Transformation
- Advoware: Erwartet 2-3 stelligen Code (z.B.
"DE","AT") - EspoCRM: Speichert bereits Kürzel (z.B.
"DE")
Status: ✅ Bereits kompatibel! Ggf. Fallback für alte Daten mit Vollnamen.
Lösung: Transformation-Tabelle als Fallback:
COUNTRY_CODES = {
'Deutschland': 'DE',
'Österreich': 'AT',
'Schweiz': 'CH',
'DE': 'DE', # Already short form
'AT': 'AT',
'CH': 'CH',
# ...
}
4.2.3 Hauptadresse-Flag
- Advoware:
standardAnschrift: truemarkiert Hauptadresse - EspoCRM: Kein dediziertes Feld (nutzt
nameoderisActive)
Strategie-Optionen:
- Erste aktive Adresse = Hauptadresse
- Adresse mit
name = "Hauptadresse" - Neues Feld
isPrimaryin EspoCRM hinzufügen
4.2.4 Formatierte Adresse (anschrift)
Advoware hat ein anschrift-Feld für die vollständig formatierte Adresse:
"anschrift": "Musterstraße 123\n30159 Hannover\nDeutschland"
Lösung: EspoCRM generiert diese aus den Einzelfeldern:
def format_anschrift(addr):
parts = []
if addr.get('adresseStreet'):
parts.append(addr['adresseStreet'])
if addr.get('adressePostalCode') or addr.get('adresseCity'):
parts.append(f"{addr.get('adressePostalCode', '')} {addr.get('adresseCity', '')}".strip())
if addr.get('adresseCountry'):
parts.append(addr['adresseCountry'])
return '\n'.join(parts)
4.2.5 Postfach-Adressen
- Advoware: Separate Felder
postfachundpostfachPLZ - EspoCRM: Kein dediziertes Feld
Strategie:
- Option A: In
descriptionspeichern - Option B: In
adresseStreetmit Prefix "Postfach: ..." - Option C: Neue Custom-Felder hinzufügen
4.2.6 Zeitliche Gültigkeit
- Advoware:
gueltigVonundgueltigBis(datetime) - EspoCRM: Kein dediziertes Feld
Strategie:
- Option A: Ignorieren (nicht kritisch für Start)
- Option B: In
descriptionspeichern - Option C: Neue Custom-Felder hinzufügen
- Option D:
isActive = falsewenngueltigBisin Vergangenheit
5. Sync-Strategien
5.1 ✅ EMPFOHLEN: CAdressen-Entity (Full Sync)
Voraussetzung: ✅ EspoCRM hat bereits CAdressen-Entity mit allen benötigten Feldern
Status: IDEAL - Beide Systeme unterstützen mehrere Adressen
EspoCRM → Advoware
async def sync_addresses_to_advoware(beteiligte_id: str, betnr: int):
"""
Synct alle CAdressen-Entities zu Advoware
"""
espo = EspoCRMAPI()
advo = AdvowareAPI()
# 1. Hole alle Adressen von EspoCRM für diesen Beteiligten
addresses = await espo.list_entities(
'CAdressen',
where=[{
'type': 'equals',
'attribute': 'beteiligteId',
'value': beteiligte_id
}]
)
# 2. Hole existierende Adressen von Advoware
advo_addresses = await advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
# 3. Sync jede Adresse
for addr in addresses['list']:
# Überspringe inaktive Adressen?
if not addr.get('isActive', True):
continue
advo_addr_id = addr.get('adressid') # Gemappte Advoware-ID
advo_data = {
'strasse': addr.get('adresseStreet'),
'plz': addr.get('adressePostalCode'),
'ort': addr.get('adresseCity'),
'land': addr.get('adresseCountry') or 'DE',
'anschrift': format_anschrift(addr),
'bemerkung': addr.get('description'),
# standardAnschrift: Bestimme via Logic
'standardAnschrift': is_primary_address(addr, addresses['list'])
}
if advo_addr_id:
# Update existierende Adresse
result = await advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{advo_addr_id}',
method='PUT',
json_data=advo_data
)
else:
# Create neue Adresse
result = await advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='POST',
json_data=advo_data
)
# POST gibt Array zurück, nimm erste Adresse
if isinstance(result, list) and result:
new_addr = result[0]
else:
new_addr = result
# Update EspoCRM mit Advoware-ID
await espo.update_entity('CAdressen', addr['id'], {
'adressid': new_addr['id'],
'advowareindexid': new_addr.get('reihenfolgeIndex'),
'advowareLastSync': datetime.utcnow().isoformat(),
'syncStatus': 'clean'
})
Advoware → EspoCRM
async def sync_addresses_to_espocrm(betnr: int, beteiligte_id: str):
"""
Synct alle Advoware-Adressen zu EspoCRM CAdressen
"""
advo = AdvowareAPI()
espo = EspoCRMAPI()
# 1. Hole Adressen von Advoware
advo_addresses = await advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
# 2. Hole existierende CAdressen-Entities
espo_addresses = await espo.list_entities(
'CAdressen',
where=[{
'type': 'equals',
'attribute': 'beteiligteId',
'value': beteiligte_id
}]
)
# 3. Mappe via adressid
espo_by_advo_id = {
a['adressid']: a
for a in espo_addresses['list']
if a.get('adressid')
}
for advo_addr in advo_addresses:
espo_data = {
'name': advo_addr.get('bemerkung') or f"{advo_addr.get('ort')} - {advo_addr.get('strasse')}",
'beteiligteId': beteiligte_id,
'adressid': advo_addr['id'],
'advowareindexid': advo_addr.get('reihenfolgeIndex'),
'adresseStreet': advo_addr.get('strasse'),
'adresseCity': advo_addr.get('ort'),
'adressePostalCode': advo_addr.get('plz'),
'adresseCountry': advo_addr.get('land'),
'description': advo_addr.get('bemerkung'),
'isActive': True, # Oder basierend auf gueltigBis
'syncStatus': 'clean',
'advowareLastSync': datetime.utcnow().isoformat()
}
existing_espo = espo_by_advo_id.get(advo_addr['id'])
if existing_espo:
# Update nur wenn rowId sich geändert hat
if advo_addr.get('rowId') != existing_espo.get('advowareRowId'):
await espo.update_entity('CAdressen', existing_espo['id'], espo_data)
else:
# Create
await espo.create_entity('CAdressen', espo_data)
Vorteile:
- ✅ Beliebig viele Adressen
- ✅ Vollständiges Mapping aller Felder
- ✅ Sync-Felder bereits vorhanden
- ✅ Keine Datenverluste
- ✅ Entity existiert bereits!
Nachteile:
- ⚠️ Postfach-Felder fehlen in EspoCRM (optional: Custom Fields)
- ⚠️ Gültigkeitsdaten fehlen in EspoCRM (optional: Custom Fields)
6. Weitere Sync-Aspekte
6.1 Change Detection
Wie erkennt man Änderungen?
Option A: rowId-Vergleich
Advoware gibt rowId zurück:
def has_changed(advo_addr, espo_addr):
"""Vergleich via rowId"""
return advo_addr.get('rowId') != espo_addr.get('advowareRowId')
Option B: Timestamp-basiert
def has_changed(advo_addr, espo_addr):
"""Vergleich via lastSyncedAt"""
last_sync = espo_addr.get('lastSyncedAt')
if not last_sync:
return True
# Advoware hat keine Timestamps, nutze rowId
return advo_addr.get('rowId') != espo_addr.get('advowareRowId')
6.2 Konfliktauflösung
Was passiert bei gleichzeitigen Änderungen?
Master-System definieren
SYNC_MASTER = 'advoware' # oder 'espocrm'
if SYNC_MASTER == 'advoware':
# Advoware-Daten überschreiben EspoCRM
pass
elif SYNC_MASTER == 'espocrm':
# EspoCRM-Daten überschreiben Advoware
pass
else:
# Last-Write-Wins (via Timestamp)
pass
6.3 DELETE-Handling
Problem: Advoware hat keinen DELETE-Endpoint für Adressen!
Lösungen:
-
Soft-Delete in Advoware: Setze
gueltigBisauf Vergangenheit# Statt DELETE: await advo.api_call( f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{addr_id}', method='PUT', json_data={'gueltigBis': '1970-01-01T00:00:00'} ) -
Ignoriere Deletes: EspoCRM-Löschungen werden nicht zu Advoware propagiert
-
Marker-Feld:
bemerkung: 'GELÖSCHT (EspoCRM)'
6.4 Validierung
Welche Felder sind Pflichtfelder?
Laut Schema sind alle Felder nullable: true, aber Best Practices:
def validate_address(addr_data):
"""Validierung vor Sync"""
required = ['strasse', 'plz', 'ort']
for field in required:
if not addr_data.get(field):
raise ValueError(f"Pflichtfeld fehlt: {field}")
# PLZ-Format (Deutschland)
if addr_data.get('land') == 'DE':
plz = addr_data.get('plz', '')
if not re.match(r'^\d{5}$', plz):
raise ValueError(f"Ungültige PLZ: {plz}")
7. Empfehlung
7.1 ✅ EMPFEHLUNG: CAdressen Full-Sync (Strategie 5.1)
Status: OPTIMAL - EspoCRM hat bereits die perfekte Struktur!
Warum:
- ✅ Entity existiert bereits (
CAdressen) - ✅ Sync-Felder vorhanden (
adressid,advowareLastSync,syncStatus) - ✅ Beliebig viele Adressen möglich
- ✅ Minimale Code-Komplexität
- ✅ Keine Datenverluste
- ✅ Bereits 5 Adressen in Produktion
Implementierungs-Reihenfolge:
- ✅ Mapper-Klasse
AdressenMappererstellen (analog zuBeteiligteMapper) - ✅ EspoCRM → Advoware Sync (CREATE + UPDATE)
- ✅ Advoware → EspoCRM Sync (CREATE + UPDATE)
- ✅ Change Detection via
adressid+rowId - ⚠️ DELETE-Handling (via Soft-Delete oder ignorieren)
7.2 Optionale Erweiterungen
Falls benötigt:
- 🔧 Custom-Felder für Postfach hinzufügen (
postfach,postfachPLZ) - 🔧 Custom-Felder für Gültigkeit hinzufügen (
validFrom,validUntil) - 🔧 Feld
isPrimaryfür Hauptadresse-Flag
Aktuell:
descriptionkann für Bemerkungen genutzt werdenisActivekann für abgelaufene Adressen genutzt werden- Hauptadresse via Konvention (erste Adresse oder nach Name)
8. Offene Fragen
✅ An EspoCRM-Team (BEANTWORTET):
- ✅ Gibt es bereits ein Custom Entity für Adressen? → JA:
CAdressen - ✅ Wie viele Adressen haben Beteiligte typischerweise? → Aktuell 5 in DB
- ⚠️ Werden Postfach-Adressen genutzt? → Zu klären
- ⚠️ Ist zeitliche Gültigkeit (
validFrom/validUntil) relevant? → Zu klären
❓ An Advoware-Team (BEANTWORTET via Tests):
- ✅ DELETE nicht verfügbar → 403 Forbidden (bestätigt)
- ✅ Gelöschte Adressen: Nur manuell in Web-Interface möglich
- ❌ Webhook-Support: Nicht verfügbar
- ✅
reihenfolgeIndex: System-managed, automatisch ans Ende gereiht
9. FINALE SYNC-STRATEGIE (Nach umfassenden Tests)
BGESCHLOSSEN)
- Swagger-Dokumentation analysiert
- EspoCRM CAdressen-Entity via API verifiziert
- Sync-Strategien evaluiert
- Empfehlung: Full-Sync mit CAdressen-Entity
Phase 2: Prototyp (Code-Phase) 🚀 NEXT
- Mapper-Klasse
AdressenMapperimplementierenmap_cadressen_to_advoware(espo_addr) -> dictmap_advoware_to_cadressen(advo_addr) -> dictget_changed_fields(espo_addr, advo_addr) -> list
- Ländercode-Transformation
format_anschrift()Helper-Funktion- Change Detection via
adressid+rowId - Unit Tests
Phase 3: Integration
- Sync-Service
AdressenSyncServiceerstellen - Webhook für CAdressen-Changes
- Adressen-Sync nach Beteiligte-Sync
- Error Handling & Retry Logic
- Monitoring & Logging
Phase 4: Testing & Rollout
- Integration Tests mit echten Daten
- Performance-Tests (Batch-Sync)
- Dokumentation
- Staging Deployment
- Production Rollout
- Monitoring & Logging
Erstellt von: GitHub Copilot
Reviewer: [Pending]
Änderungslog:
- 2026-02-08: Initial Analysis
9.1 Test-Erkenntnisse (08.02.2026)
Durchgeführte Tests:
- ✅ Alle Felder POST/PUT Verifikation
- ✅ DELETE-Funktionalität (→ 403 Forbidden)
- ✅
bemerkung-basiertes Matching mit EspoCRM-IDs - ✅
gueltigBisnachträglich setzen (→ READ-ONLY!) - ✅
reihenfolgeIndexVerhalten (automatisch, READ-ONLY) - ✅ Detaillierte Feld-für-Feld PUT Analyse
Kritische Befunde:
ID-Mapping Problem
- ❌
idist immer 0 (unbrauchbar) - ✅
bemerkungist stabil (READ-ONLY bei PUT) → Perfekt für Matching! - ✅
reihenfolgeIndexist stabil (automatisch vergeben, READ-ONLY) - ❌
rowIdändert sich bei PUT → nicht für Matching nutzen
PUT-Limitierungen
✅ ÄNDERBAR (4 Felder):
strasse,plz,ort,anschrift
❌ READ-ONLY (8 Felder):
land,postfach,postfachPLZ,standardAnschriftbemerkung(gut für Matching!)gueltigVon,gueltigBis(keine Soft-Delete möglich)reihenfolgeIndex(system-managed)
DELETE Problem
- ❌ DELETE /Adressen/{id} → 403 Forbidden
- ❌ gueltigBis nachträglich setzen → READ-ONLY
- → Soft-Delete NICHT möglich via API!
9.2 Empfohlene Strategie: "Hybrid mit Notifications"
Prinzip: Automatischer Sync wo möglich + manuelle Eingriffe bei API-Limitierungen
Workflow:
-
CREATE (EspoCRM → Advoware)
POST /Adressen { "bemerkung": f"EspoCRM-ID: {espo_id}", # Matching-Key # ... alle anderen Felder } → Update EspoCRM mit advowareIndexId, advowareRowId -
UPDATE (EspoCRM → Advoware)
GET /Adressen → match via bemerkung PUT /Adressen/{reihenfolgeIndex} { # Nur: strasse, plz, ort, anschrift } → Prüfe READ-ONLY Änderungen → Notification wenn READ-ONLY geändert -
DELETE (EspoCRM → Advoware)
# API unterstützt kein DELETE! → Notification + Task erstellen → EspoCRM.isActive = False → User löscht manuell in Advoware -
SYNC (Advoware → EspoCRM)
GET /Adressen → match via bemerkung → Update/Create in EspoCRM → Advoware = Master für Existenz
9.3 Notification-Management
Zentrale Utility: services/notification_utils.py
Features:
- ✅ Automatische Task-Erstellung in EspoCRM
- ✅ Notification an assigned User
- ✅ Detaillierte Anleitungen für manuelle Eingriffe
- ✅ Verschiedene Action-Types:
address_delete_requiredaddress_reactivate_requiredaddress_field_update_requiredreadonly_field_conflictmissing_in_advoware
Beispiel:
from services.notification_utils import NotificationManager
notif_mgr = NotificationManager(espocrm_api, context)
# DELETE erforderlich
await notif_mgr.notify_manual_action_required(
entity_type='CAdressen',
entity_id=address_id,
action_type='address_delete_required',
details={'betnr': betnr, 'strasse': '...', ...}
)
# → Erstellt Task mit Schritt-für-Schritt Anleitung
9.4 EspoCRM Entity-Anpassungen
Erforderliche Felder in CAdressen:
advowareIndexId(int) - für PUT-EndpointadvowareRowId(varchar) - zur ValidierungadvowareLastSync(datetime) - Sync-ZeitpunktsyncStatus(enum) - synced | partial | manual_action_required | deleted_in_advowareisActive(bool) - Aktiv/Inaktiv FlagmanualActionNote(text) - Bei manuellen Eingriffen
9.5 Offene Punkte & Risiken
Risiken:
| Risiko | Impact | Mitigation |
|---|---|---|
| User ändert bemerkung | Hoch | Regex-Parse, auch in Text suchen |
| DELETE vergessen | Mittel | Task-System mit Due-Date |
| READ-ONLY Konflikte | Niedrig | Notification-System |
| Parallele Syncs | Hoch | Locking-Mechanismus |
Nächste Schritte:
- ✅ Notification-System implementiert
- ⏳ EspoCRM CAdressen Entity validieren
- ⏳ Mapper implementieren (
services/adressen_mapper.py) - ⏳ Sync-Service implementieren
- ⏳ Integration Tests
Test-Scripts:
scripts/test_adressen_api.py- Umfassende API-Testsscripts/test_adressen_delete_matching.py- DELETE + bemerkung-Matchingscripts/test_adressen_deactivate_ordering.py- gueltigBis + reihenfolgeIndexscripts/test_adressen_gueltigbis_modify.py- gueltigBis nachträglich ändernscripts/test_put_response_detail.py- PUT Feld-Analyse
10. Hauptadresse-Logik (standardAnschrift)
10.1 Test-Ergebnisse (08.02.2026)
Durchgeführte Tests:
- ✅ Prüfung ob neue Adressen automatisch zur Hauptadresse werden
- ✅ Explizites Setzen von
standardAnschriftbeim POST - ✅ Verhalten bei mehreren Hauptadressen
Kritische Erkenntnisse:
✅ Was funktioniert:
-
standardAnschriftkann beim POST gesetzt werdenPOST /Adressen { "standardAnschrift": true, # ← Wird akzeptiert! ... } -
Feld wird korrekt gespeichert
- GET zeigt
standardAnschrift: true - Bleibt persistent
- GET zeigt
⚠️ Was NICHT funktioniert:
-
Keine automatische Haupt-Adress-Logik
- Neue Adressen werden NICHT automatisch zur Hauptadresse
standardAnschriftist standardmäßigfalse- Keine "neueste Adresse = Hauptadresse" Regel
-
Mehrere Hauptadressen sind möglich!
- Advoware deaktiviert NICHT alte Hauptadressen automatisch
- Es können mehrere Adressen
standardAnschrift = truehaben - Keine Validierung auf "nur eine Hauptadresse"
-
standardAnschriftist READ-ONLY bei PUT (bereits bekannt)- Kann nur beim POST gesetzt werden
- Kann nicht nachträglich geändert werden
10.2 Konsequenzen für Sync
Probleme:
Szenario 1: User ändert Hauptadresse in EspoCRM
→ PUT zu Advoware → standardAnschrift bleibt unverändert (READ-ONLY)
→ Hauptadresse-Status kann nicht synchronisiert werden!
Szenario 2: Neue Hauptadresse erstellen
→ POST mit standardAnschrift = true
→ Alte Hauptadresse bleibt AUCH true
→ Mehrere Hauptadressen existieren!
Szenario 3: Hauptadresse in Advoware manuell geändert
→ EspoCRM weiß nicht welche die "echte" Hauptadresse ist
→ Wenn mehrere true sind
10.3 Empfohlene Strategie
Option A: EspoCRM als Master (Empfohlen)
# In EspoCRM:
- isPrimary (boolean) - Master-Feld
- advowareStandardAnschrift (boolean) - Read-Only Spiegel
# Sync-Logik:
1. CREATE: Setze standardAnschrift = isPrimary
2. UPDATE:
- Wenn isPrimary geändert → Notification (READ-ONLY in Advoware)
3. SYNC from Advoware:
- Lese alle Adressen
- Wenn mehrere standardAnschrift = true → Warnungslog
- Update advowareStandardAnschrift (Info-Feld)
- isPrimary bleibt EspoCRM-Master
Option B: Advoware als Master
# Sync-Logik:
1. CREATE: Verwende EspoCRM isPrimary initial
2. UPDATE: isPrimary ist READ-ONLY in EspoCRM
3. SYNC from Advoware:
- Lese standardAnschrift
- Update isPrimary in EspoCRM
- Advoware = Master
# Problem: Mehrere Hauptadressen möglich!
→ Welche ist die "echte"?
→ Nehme erste? Letzte? Niedrigster Index?
Option C: Manuelle Verwaltung
# Bei Hauptadresse-Änderung:
1. User ändert in EspoCRM
2. Notification erstellen
3. User ändert manuell in Advoware Web-Interface
4. Sync aktualisiert EspoCRM
# Vorteil: Keine automatischen Konflikte
# Nachteil: Viel manuelle Arbeit
10.4 Implementierungs-Empfehlung
Hybrid-Ansatz:
class HauptadresseSync:
"""
Hauptadresse-Sync mit Notification bei Konflikten
"""
async def sync_hauptadresse_status(self, espo_addresses, advo_addresses):
"""Sync Hauptadresse-Status von Advoware"""
# Finde Hauptadressen in Advoware
advo_haupt = [a for a in advo_addresses if a.get('standardAnschrift')]
if len(advo_haupt) == 0:
# Keine Hauptadresse in Advoware
logger.warning(f"Keine Hauptadresse in Advoware für BetNr {betnr}")
return
if len(advo_haupt) > 1:
# MEHRERE Hauptadressen!
logger.error(f"MEHRERE Hauptadressen in Advoware: {len(advo_haupt)}")
# → Notification erstellen
await notification_manager.notify_manual_action_required(
entity_type='CBeteiligte',
entity_id=espo_beteiligte_id,
action_type='general_manual_action',
details={
'message': f'Mehrere Hauptadressen in Advoware ({len(advo_haupt)})',
'description': (
f'Beteiligter hat {len(advo_haupt)} Adressen mit '
f'standardAnschrift = true. Bitte in Advoware korrigieren, '
f'sodass nur EINE Hauptadresse existiert.'
),
'priority': 'High'
}
)
# Nehme erste als "Haupt" (niedrigster Index)
advo_haupt = [min(advo_haupt, key=lambda a: a.get('reihenfolgeIndex', 999))]
# Update EspoCRM
for espo_addr in espo_addresses:
espo_id = espo_addr['id']
# Finde korrespondierende Advoware-Adresse
bemerkung_match = f"EspoCRM-ID: {espo_id}"
advo_addr = next(
(a for a in advo_addresses
if bemerkung_match in (a.get('bemerkung') or '')),
None
)
if advo_addr:
# Update isPrimary basierend auf Advoware
should_be_primary = advo_addr in advo_haupt
if espo_addr.get('isPrimary') != should_be_primary:
await espocrm_api.update('CAdressen', espo_id, {
'isPrimary': should_be_primary,
'advowareStandardAnschrift': advo_addr.get('standardAnschrift')
})
logger.info(
f"Updated isPrimary: {espo_addr.get('isPrimary')} "
f"→ {should_be_primary} (EspoCRM ID: {espo_id})"
)
async def create_with_hauptadresse_logic(self, espo_addr, betnr):
"""Erstelle Adresse mit Hauptadresse-Logik"""
# Wenn diese Adresse Hauptadresse werden soll
if espo_addr.get('isPrimary'):
# Prüfe ob es schon eine Hauptadresse gibt
existing_addresses = await advoware_api.get(f'/Beteiligte/{betnr}/Adressen')
existing_haupt = [a for a in existing_addresses if a.get('standardAnschrift')]
if len(existing_haupt) > 0:
# → Notification: Alte Hauptadresse manuell deaktivieren
await notification_manager.notify_manual_action_required(
entity_type='CAdressen',
entity_id=espo_addr['id'],
action_type='general_manual_action',
details={
'message': 'Neue Hauptadresse erstellt - alte manuell deaktivieren',
'description': (
f'Neue Hauptadresse wird erstellt:\n'
f'{espo_addr["adresseStreet"]}, {espo_addr["adresseCity"]}\n\n'
f'WICHTIG: Alte Hauptadresse(n) in Advoware manuell '
f'deaktivieren (standardAnschrift = false setzen):\n' +
'\n'.join([f"- {a.get('strasse')}" for a in existing_haupt])
),
'priority': 'High'
}
)
# CREATE normal durchführen
await self.create_address(espo_addr, betnr)
10.5 EspoCRM Entity-Felder
Zusätzlich erforderlich:
{
"isPrimary": "bool", // Master-Feld (EspoCRM managed)
"advowareStandardAnschrift": "bool" // Info-Feld (Advoware Mirror)
}
10.6 Zusammenfassung
Hauptadresse-Sync ist komplex wegen:
- ❌
standardAnschriftist READ-ONLY bei PUT - ❌ Mehrere Hauptadressen in Advoware möglich
- ❌ Keine automatische Deaktivierung alter Hauptadressen
Empfohlener Ansatz:
- ✅ EspoCRM
isPrimaryals Master - ✅ Advoware
standardAnschriftbeim POST setzen - ✅ Notification bei Hauptadresse-Änderung (manuelle Aktion nötig)
- ✅ Sync from Advoware: Prüfe auf mehrere Hauptadressen → Warnung
- ✅ Wähle niedrigsten Index als "echte" Hauptadresse bei Konflikten
Test-Scripts:
scripts/test_hauptadresse_logic.py- Hauptadresse-Logikscripts/test_hauptadresse_explizit.py- Explizites Setzenscripts/test_find_hauptadresse.py- Suche spezifische Adresse
11. reihenfolgeIndex-Instabilität ⚠️
11.1 Test-Ergebnisse (08.02.2026)
Kritische Entdeckung:
reihenfolgeIndex wird nach Löschungen automatisch neu vergeben!
Test:
Vorher (24 Adressen):
Index 1: Test 6667426
Index 2: ...
Index 23: Hauptadresse Explizit Test
Index 24: Zweite Hauptadresse Test
Benutzer löscht 23 Adressen in Advoware Web-Interface
Nachher (1 Adresse):
Index 1: Hauptadresse Explizit Test ← War vorher Index 23!
11.2 Konsequenzen
❌ reihenfolgeIndex ist NICHT stabil!
# FALSCH - Index ändert sich!
espocrm_record = {
'advowareIndexId': 23 # ← Nach Löschung nicht mehr gültig!
}
# RICHTIG - Nur bemerkung ist stabil
espocrm_record = {
'bemerkung': 'EspoCRM-ID: 12345' # ← Bleibt immer gleich
}
Implikationen:
- ❌ Kann NICHT in EspoCRM gespeichert werden als Identifier
- ❌ Kann NICHT für direktes Matching genutzt werden
- ✅ Muss VOR jedem PUT dynamisch ermittelt werden via:
# 1. GET alle Adressen all_addresses = await advo.get(f'/Beteiligte/{betnr}/Adressen') # 2. Match via bemerkung target = next( (a for a in all_addresses if f"EspoCRM-ID: {espo_id}" in (a.get('bemerkung') or '')), None ) # 3. Nutze aktuellen Index für PUT if target: await advo.put( f'/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}', data=updated_data )
11.3 Update der Sync-Strategie
Korrigierte Workflow:
class AdressenSync:
async def update_address(self, espo_addr, betnr):
"""
Update mit dynamischer Index-Ermittlung
"""
espo_id = espo_addr['id']
# 1. GET alle Adressen (immer!)
all_addresses = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
# 2. Match via bemerkung (EINZIGE stabile Methode)
bemerkung_match = f"EspoCRM-ID: {espo_id}"
target = next(
(a for a in all_addresses
if bemerkung_match in (a.get('bemerkung') or '')),
None
)
if not target:
logger.error(f"Adresse nicht gefunden via bemerkung: {bemerkung_match}")
# → Erstelle neu statt Update?
return await self.create_address(espo_addr, betnr)
# 3. Nutze AKTUELLEN reihenfolgeIndex für PUT
current_index = target['reihenfolgeIndex']
# 4. PUT mit aktuellem Index
updated_data = {
'strasse': espo_addr['adresseStreet'],
'plz': espo_addr['adressePostalCode'],
'ort': espo_addr['adresseCity'],
'anschrift': self.format_anschrift(espo_addr)
# Nur 4 Felder erlaubt!
}
result = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{current_index}',
method='PUT',
json_data=updated_data
)
logger.info(
f"Updated Adresse via dynamischen Index {current_index} "
f"(Match: {bemerkung_match})"
)
return result
11.4 EspoCRM Felder - KORREKTUR
Alte Annahme (FALSCH):
{
"advowareIndexId": "int", // ← NICHT speichern!
"advowareRowId": "varchar"
}
Neue Empfehlung (RICHTIG):
{
// KEIN advowareIndexId! (instabil)
"advowareRowId": "varchar", // Nur als Info (ändert sich bei PUT)
"bemerkung": "text" // Muss "EspoCRM-ID: {id}" enthalten!
}
11.5 Zusammenfassung
Identifier-Stabilität:
| Feld | Stabil? | Nutzung |
|---|---|---|
id |
❌ Immer 0 | Unbrauchbar |
rowId |
❌ Ändert sich bei PUT | Nur Change Detection |
reihenfolgeIndex |
❌ Neu vergeben nach Löschung | Nur für PUT-Endpoint (dynamisch) |
bemerkung |
✅ Bleibt konstant (READ-ONLY bei PUT) | EINZIGER stabiler Identifier! |
Empfohlener Workflow:
- CREATE: Setze
bemerkung = "EspoCRM-ID: {espo_id}" - UPDATE: GET → Match via
bemerkung→ Nutze aktuellenreihenfolgeIndexfür PUT - DELETE: Notification (kein API-DELETE)
Test-Scripts:
scripts/test_find_hauptadresse.py- Zeigt Index-Renummerierung nach Löschung
11. reihenfolgeIndex-Instabilität ⚠️
11.1 Test-Ergebnisse (08.02.2026)
Kritische Entdeckung:
reihenfolgeIndex wird nach Löschungen automatisch neu vergeben!
Test:
Vorher (24 Adressen):
Index 1: Test 6667426
Index 2: ...
Index 23: Hauptadresse Explizit Test
Index 24: Zweite Hauptadresse Test
Benutzer löscht 23 Adressen in Advoware Web-Interface
Nachher (1 Adresse):
Index 1: Hauptadresse Explizit Test ← War vorher Index 23!
11.2 Konsequenzen
❌ reihenfolgeIndex ist NICHT stabil!
# FALSCH - Index ändert sich!
espocrm_record = {
'advowareIndexId': 23 # ← Nach Löschung nicht mehr gültig!
}
# RICHTIG - Nur bemerkung ist stabil
espocrm_record = {
'bemerkung': 'EspoCRM-ID: 12345' # ← Bleibt immer gleich
}
Implikationen:
- ❌ Kann NICHT in EspoCRM gespeichert werden als Identifier
- ❌ Kann NICHT für direktes Matching genutzt werden
- ✅ Muss VOR jedem PUT dynamisch ermittelt werden via:
# 1. GET alle Adressen all_addresses = await advo.get(f'/Beteiligte/{betnr}/Adressen') # 2. Match via bemerkung target = next( (a for a in all_addresses if f"EspoCRM-ID: {espo_id}" in (a.get('bemerkung') or '')), None ) # 3. Nutze aktuellen Index für PUT if target: await advo.put( f'/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}', data=updated_data )
11.3 Update der Sync-Strategie
Korrigierter Workflow:
class AdressenSync:
async def update_address(self, espo_addr, betnr):
"""
Update mit dynamischer Index-Ermittlung
"""
espo_id = espo_addr['id']
# 1. GET alle Adressen (immer!)
all_addresses = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
# 2. Match via bemerkung (EINZIGE stabile Methode)
bemerkung_match = f"EspoCRM-ID: {espo_id}"
target = next(
(a for a in all_addresses
if bemerkung_match in (a.get('bemerkung') or '')),
None
)
if not target:
logger.error(f"Adresse nicht gefunden via bemerkung: {bemerkung_match}")
# → Erstelle neu statt Update?
return await self.create_address(espo_addr, betnr)
# 3. Nutze AKTUELLEN reihenfolgeIndex für PUT
current_index = target['reihenfolgeIndex']
# 4. PUT mit aktuellem Index
updated_data = {
'strasse': espo_addr['adresseStreet'],
'plz': espo_addr['adressePostalCode'],
'ort': espo_addr['adresseCity'],
'anschrift': self.format_anschrift(espo_addr)
# Nur 4 Felder erlaubt!
}
result = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{current_index}',
method='PUT',
json_data=updated_data
)
logger.info(
f"Updated Adresse via dynamischen Index {current_index} "
f"(Match: {bemerkung_match})"
)
return result
11.4 EspoCRM Felder - KORREKTUR
Alte Annahme (FALSCH):
{
"advowareIndexId": "int", // ← NICHT speichern!
"advowareRowId": "varchar"
}
Neue Empfehlung (RICHTIG):
{
// KEIN advowareIndexId! (instabil)
"advowareRowId": "varchar", // Nur als Info (ändert sich bei PUT)
"bemerkung": "text" // Muss "EspoCRM-ID: {id}" enthalten!
}
11.5 Zusammenfassung
Identifier-Stabilität:
| Feld | Stabil? | Nutzung |
|---|---|---|
id |
❌ Immer 0 | Unbrauchbar |
rowId |
❌ Ändert sich bei PUT | Nur Change Detection |
reihenfolgeIndex |
❌ Neu vergeben nach Löschung | Nur für PUT-Endpoint (dynamisch) |
bemerkung |
✅ Bleibt konstant (READ-ONLY bei PUT) | EINZIGER stabiler Identifier! |
Empfohlener Workflow:
- CREATE: Setze
bemerkung = "EspoCRM-ID: {espo_id}" - UPDATE: GET → Match via
bemerkung→ Nutze aktuellenreihenfolgeIndexfür PUT - DELETE: Notification (kein API-DELETE)
Test-Scripts:
scripts/test_find_hauptadresse.py- Zeigt Index-Renummerierung nach Löschung
12. FINALE SYNC-STRATEGIE ✅
12.1 Entscheidung (08.02.2026)
Nach umfangreichen Tests wurde folgende finale Strategie festgelegt:
✅ CREATE - Vollständig unterstützt (alle 11 Felder)
✅ UPDATE - Nur für R/W Felder (strasse, plz, ort, anschrift)
❌ DELETE - NICHT automatisch (nur via Notification)
⚠️ READ-ONLY Änderungen - Nur via Notification + manuelle Aktion
12.2 Implementierungs-Regeln
12.2.1 CREATE (EspoCRM → Advoware)
Vollständig automatisiert:
async def create_address(self, espo_addr, betnr):
"""
Erstelle neue Adresse in Advoware
Alle Felder werden synchronisiert
"""
advo_data = {
# R/W Felder (via PUT änderbar)
'strasse': espo_addr.get('adresseStreet'),
'plz': espo_addr.get('adressePostalCode'),
'ort': espo_addr.get('adresseCity'),
'anschrift': self.format_anschrift(espo_addr),
# READ-ONLY Felder (nur bei CREATE!)
'land': espo_addr.get('adresseCountry') or 'DE',
'postfach': espo_addr.get('postfach'),
'postfachPLZ': espo_addr.get('postfachPLZ'),
'standardAnschrift': espo_addr.get('isPrimary', False),
'bemerkung': f"EspoCRM-ID: {espo_addr['id']}", # Matching!
'gueltigVon': espo_addr.get('validFrom'),
'gueltigBis': espo_addr.get('validUntil')
}
result = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='POST',
json_data=advo_data
)
logger.info(f"Created address in Advoware for EspoCRM ID {espo_addr['id']}")
return result
12.2.2 UPDATE (EspoCRM → Advoware)
Nur R/W Felder, REST über Notification:
async def update_address(self, espo_addr, betnr):
"""
Update nur R/W Felder
Alle anderen Änderungen → Notification
"""
espo_id = espo_addr['id']
# 1. Finde Adresse via bemerkung
all_addresses = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
bemerkung_match = f"EspoCRM-ID: {espo_id}"
target = next(
(a for a in all_addresses
if bemerkung_match in (a.get('bemerkung') or '')),
None
)
if not target:
logger.error(f"Address not found: {bemerkung_match}")
return await self.create_address(espo_addr, betnr)
# 2. Update NUR R/W Felder
rw_data = {
'strasse': espo_addr.get('adresseStreet'),
'plz': espo_addr.get('adressePostalCode'),
'ort': espo_addr.get('adresseCity'),
'anschrift': self.format_anschrift(espo_addr)
}
result = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}',
method='PUT',
json_data=rw_data
)
# 3. Prüfe READ-ONLY Felder auf Änderungen
readonly_changes = self.detect_readonly_changes(espo_addr, target)
if readonly_changes:
# → Notification für manuelle Änderung
await self.notify_readonly_changes(espo_addr, readonly_changes)
logger.info(f"Updated address (R/W fields only) for EspoCRM ID {espo_id}")
return result
def detect_readonly_changes(self, espo_addr, advo_addr):
"""
Erkenne Änderungen an READ-ONLY Feldern
"""
changes = []
# Prüfe alle READ-ONLY Felder
readonly_mappings = {
'adresseCountry': ('land', 'Land'),
'postfach': ('postfach', 'Postfach'),
'postfachPLZ': ('postfachPLZ', 'Postfach PLZ'),
'isPrimary': ('standardAnschrift', 'Hauptadresse'),
'validFrom': ('gueltigVon', 'Gültig von'),
'validUntil': ('gueltigBis', 'Gültig bis')
}
for espo_field, (advo_field, label) in readonly_mappings.items():
espo_value = espo_addr.get(espo_field)
advo_value = advo_addr.get(advo_field)
# Normalisiere Werte für Vergleich
if espo_field == 'isPrimary':
espo_value = bool(espo_value)
advo_value = bool(advo_value)
if espo_value != advo_value:
changes.append({
'field': label,
'espoField': espo_field,
'advoField': advo_field,
'espoCRM_value': espo_value,
'advoware_value': advo_value
})
return changes
async def notify_readonly_changes(self, espo_addr, changes):
"""
Erstelle Notification für READ-ONLY Feld-Änderungen
"""
change_details = '\n'.join([
f"- {c['field']}: EspoCRM='{c['espoCRM_value']}' → "
f"Advoware='{c['advoware_value']}'"
for c in changes
])
await self.notification_manager.notify_manual_action_required(
entity_type='CAdressen',
entity_id=espo_addr['id'],
action_type='readonly_field_conflict',
details={
'message': f'{len(changes)} READ-ONLY Feld(er) geändert',
'description': (
f'Folgende Felder wurden in EspoCRM geändert, sind aber '
f'READ-ONLY in Advoware und können nicht automatisch '
f'synchronisiert werden:\n\n{change_details}\n\n'
f'Bitte manuell in Advoware anpassen.'
),
'changes': changes,
'address': f"{espo_addr.get('adresseStreet')}, "
f"{espo_addr.get('adresseCity')}",
'priority': 'High'
}
)
12.2.3 DELETE (EspoCRM → Advoware)
NUR via Notification:
async def handle_address_deletion(self, espo_addr, betnr):
"""
Adresse wurde in EspoCRM gelöscht
→ Nur Notification (kein API-DELETE verfügbar)
"""
# 1. Finde Adresse in Advoware
all_addresses = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
bemerkung_match = f"EspoCRM-ID: {espo_addr['id']}"
target = next(
(a for a in all_addresses
if bemerkung_match in (a.get('bemerkung') or '')),
None
)
if not target:
logger.info(f"Address already deleted: {espo_addr['id']}")
return
# 2. Notification erstellen
await self.notification_manager.notify_manual_action_required(
entity_type='CAdressen',
entity_id=espo_addr['id'],
action_type='address_delete_required',
details={
'message': 'Adresse in Advoware löschen',
'description': (
f'Adresse wurde in EspoCRM gelöscht:\n'
f'{target.get("strasse")}\n'
f'{target.get("plz")} {target.get("ort")}\n\n'
f'Bitte manuell in Advoware löschen:\n'
f'1. Öffne Beteiligten {betnr} in Advoware\n'
f'2. Gehe zu Adressen-Tab\n'
f'3. Lösche Adresse (Index {target.get("reihenfolgeIndex")})\n'
f'4. Speichern'
),
'advowareIndex': target.get('reihenfolgeIndex'),
'betnr': betnr,
'address': f"{target.get('strasse')}, {target.get('ort')}",
'priority': 'Medium'
}
)
logger.info(f"Created delete notification for address {espo_addr['id']}")
12.2.4 SYNC (Advoware → EspoCRM)
Volle Synchronisation (read-only):
async def sync_from_advoware(self, betnr, espo_beteiligte_id):
"""
Synct Adressen von Advoware zu EspoCRM
Alle Felder werden übernommen (Advoware = Master)
"""
# 1. Hole alle Adressen von Advoware
advo_addresses = await self.advo.api_call(
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
method='GET'
)
# 2. Hole existierende EspoCRM Adressen
espo_addresses = await self.espo.list_entities(
'CAdressen',
where=[{
'type': 'equals',
'attribute': 'beteiligteId',
'value': espo_beteiligte_id
}]
)
for advo_addr in advo_addresses:
# Match via bemerkung
bemerkung = advo_addr.get('bemerkung', '')
if 'EspoCRM-ID:' in bemerkung:
# Update existierende
espo_id = bemerkung.split('EspoCRM-ID:')[1].strip().split()[0]
await self.update_espo_address(espo_id, advo_addr)
else:
# Neue Adresse aus Advoware
await self.create_espo_address(advo_addr, espo_beteiligte_id)
logger.info(f"Synced {len(advo_addresses)} addresses from Advoware")
12.3 Zusammenfassung
Automatische Synchronisation:
- ✅ CREATE: Alle Felder (11 Felder)
- ✅ UPDATE: Nur R/W Felder (4 Felder: strasse, plz, ort, anschrift)
- ✅ SYNC from Advoware: Alle Felder (read-only)
Manuelle Aktionen (via Notification):
- ⚠️ DELETE: Notification + manuelle Löschung in Advoware
- ⚠️ READ-ONLY Änderungen: Notification bei:
landgeändertpostfach/postfachPLZgeändertstandardAnschrift(Hauptadresse) geändertgueltigVon/gueltigBisgeändertbemerkunggeändert (würde Matching zerstören!)
Rationale:
- Keine Datenkorruption: Nur sichere Operationen automatisch
- Keine API-Limitierungen umgehen: Respektiere READ-ONLY Status
- Volle Transparenz: User sieht alle manuellen Aktionen
- Matching bleibt stabil:
bemerkungniemals ändern!
Test-Scripts:
scripts/test_adressen_nullen.py- Zeigt dass Nullen nicht empfohlen ist