# 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):** 1. ❌ **DELETE nicht möglich** (403 Forbidden) 2. ❌ **Nur 4 Felder via PUT änderbar**: `strasse`, `plz`, `ort`, `anschrift` 3. ❌ **READ-ONLY Felder**: `land`, `postfach`, `postfachPLZ`, `standardAnschrift`, `bemerkung`, `gueltigVon`, `gueltigBis`, `reihenfolgeIndex` 4. ✅ **`bemerkung` ist stabil** (kann für EspoCRM-ID Matching genutzt werden) 5. ❌ **`id` ist immer 0** (unbrauchbar für Mapping) 6. ⚠️ **`reihenfolgeIndex` wird nach Löschung neu vergeben** (NICHT stabil, NICHT als Identifier nutzbar!) 7. ❌ **`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 `gueltigBis` nicht möglich (READ-ONLY) - ✅ **`bemerkung` ist die EINZIGE stabile Matching-Methode** ("EspoCRM-ID: {id}") - ⚠️ `reihenfolgeIndex` fü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: ```json { "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: ```json { "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`, `rowId` werden **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 ```json { "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 ```json // 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: ```python COUNTRY_CODES = { 'Deutschland': 'DE', 'Österreich': 'AT', 'Schweiz': 'CH', 'DE': 'DE', # Already short form 'AT': 'AT', 'CH': 'CH', # ... } ``` #### 4.2.3 Hauptadresse-Flag - **Advoware**: `standardAnschrift: true` markiert Hauptadresse - **EspoCRM**: Kein dediziertes Feld (nutzt `name` oder `isActive`) **Strategie-Optionen**: 1. Erste aktive Adresse = Hauptadresse 2. Adresse mit `name = "Hauptadresse"` 3. Neues Feld `isPrimary` in 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: ```python 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 `postfach` und `postfachPLZ` - **EspoCRM**: Kein dediziertes Feld **Strategie**: - Option A: In `description` speichern - Option B: In `adresseStreet` mit Prefix "Postfach: ..." - Option C: Neue Custom-Felder hinzufügen #### 4.2.6 Zeitliche Gültigkeit - **Advoware**: `gueltigVon` und `gueltigBis` (datetime) - **EspoCRM**: Kein dediziertes Feld **Strategie**: - Option A: Ignorieren (nicht kritisch für Start) - Option B: In `description` speichern - Option C: Neue Custom-Felder hinzufügen - Option D: `isActive = false` wenn `gueltigBis` in 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 ```python 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 ```python 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: ```python def has_changed(advo_addr, espo_addr): """Vergleich via rowId""" return advo_addr.get('rowId') != espo_addr.get('advowareRowId') ``` #### Option B: Timestamp-basiert ```python 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 ```python 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: 1. **Soft-Delete in Advoware**: Setze `gueltigBis` auf Vergangenheit ```python # 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'} ) ``` 2. **Ignoriere Deletes**: EspoCRM-Löschungen werden nicht zu Advoware propagiert 3. **Marker-Feld**: `bemerkung: 'GELÖSCHT (EspoCRM)'` ### 6.4 Validierung **Welche Felder sind Pflichtfelder?** Laut Schema sind alle Felder `nullable: true`, aber Best Practices: ```python 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:** 1. ✅ Mapper-Klasse `AdressenMapper` erstellen (analog zu `BeteiligteMapper`) 2. ✅ EspoCRM → Advoware Sync (CREATE + UPDATE) 3. ✅ Advoware → EspoCRM Sync (CREATE + UPDATE) 4. ✅ Change Detection via `adressid` + `rowId` 5. ⚠️ 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 `isPrimary` für Hauptadresse-Flag **Aktuell:** - `description` kann für Bemerkungen genutzt werden - `isActive` kann für abgelaufene Adressen genutzt werden - Hauptadresse via Konvention (erste Adresse oder nach Name) --- ## 8. Offene Fragen ### ✅ An EspoCRM-Team (BEANTWORTET): 1. ✅ Gibt es bereits ein Custom Entity für Adressen? → **JA: `CAdressen`** 2. ✅ Wie viele Adressen haben Beteiligte typischerweise? → **Aktuell 5 in DB** 3. ⚠️ Werden Postfach-Adressen genutzt? → **Zu klären** 4. ⚠️ Ist zeitliche Gültigkeit (`validFrom`/`validUntil`) relevant? → **Zu klären** ### ❓ An Advoware-Team (BEANTWORTET via Tests): 1. ✅ DELETE nicht verfügbar → 403 Forbidden (bestätigt) 2. ✅ Gelöschte Adressen: Nur manuell in Web-Interface möglich 3. ❌ Webhook-Support: Nicht verfügbar 4. ✅ `reihenfolgeIndex`: System-managed, automatisch ans Ende gereiht --- ## 9. FINALE SYNC-STRATEGIE (Nach umfassenden Tests) BGESCHLOSSEN) - [x] Swagger-Dokumentation analysiert - [x] **EspoCRM CAdressen-Entity via API verifiziert** - [x] Sync-Strategien evaluiert - [x] **Empfehlung: Full-Sync mit CAdressen-Entity** ### Phase 2: Prototyp (Code-Phase) 🚀 NEXT - [ ] Mapper-Klasse `AdressenMapper` implementieren - `map_cadressen_to_advoware(espo_addr) -> dict` - `map_advoware_to_cadressen(advo_addr) -> dict` - `get_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 `AdressenSyncService` erstellen - [ ] 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:** 1. ✅ Alle Felder POST/PUT Verifikation 2. ✅ DELETE-Funktionalität (→ 403 Forbidden) 3. ✅ `bemerkung`-basiertes Matching mit EspoCRM-IDs 4. ✅ `gueltigBis` nachträglich setzen (→ READ-ONLY!) 5. ✅ `reihenfolgeIndex` Verhalten (automatisch, READ-ONLY) 6. ✅ Detaillierte Feld-für-Feld PUT Analyse **Kritische Befunde:** #### ID-Mapping Problem - ❌ `id` ist immer 0 (unbrauchbar) - ✅ `bemerkung` ist stabil (READ-ONLY bei PUT) → Perfekt für Matching! - ✅ `reihenfolgeIndex` ist 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`, `standardAnschrift` - `bemerkung` (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:** 1. **CREATE (EspoCRM → Advoware)** ```python POST /Adressen { "bemerkung": f"EspoCRM-ID: {espo_id}", # Matching-Key # ... alle anderen Felder } → Update EspoCRM mit advowareIndexId, advowareRowId ``` 2. **UPDATE (EspoCRM → Advoware)** ```python GET /Adressen → match via bemerkung PUT /Adressen/{reihenfolgeIndex} { # Nur: strasse, plz, ort, anschrift } → Prüfe READ-ONLY Änderungen → Notification wenn READ-ONLY geändert ``` 3. **DELETE (EspoCRM → Advoware)** ```python # API unterstützt kein DELETE! → Notification + Task erstellen → EspoCRM.isActive = False → User löscht manuell in Advoware ``` 4. **SYNC (Advoware → EspoCRM)** ```python GET /Adressen → match via bemerkung → Update/Create in EspoCRM → Advoware = Master für Existenz ``` ### 9.3 Notification-Management **Zentrale Utility**: [`services/notification_utils.py`](../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_required` - `address_reactivate_required` - `address_field_update_required` - `readonly_field_conflict` - `missing_in_advoware` **Beispiel:** ```python 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-Endpoint - `advowareRowId` (varchar) - zur Validierung - `advowareLastSync` (datetime) - Sync-Zeitpunkt - `syncStatus` (enum) - synced | partial | manual_action_required | deleted_in_advoware - `isActive` (bool) - Aktiv/Inaktiv Flag - `manualActionNote` (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:** 1. ✅ Notification-System implementiert 2. ⏳ EspoCRM CAdressen Entity validieren 3. ⏳ Mapper implementieren (`services/adressen_mapper.py`) 4. ⏳ Sync-Service implementieren 5. ⏳ Integration Tests --- **Test-Scripts:** - [`scripts/test_adressen_api.py`](../scripts/test_adressen_api.py) - Umfassende API-Tests - [`scripts/test_adressen_delete_matching.py`](../scripts/test_adressen_delete_matching.py) - DELETE + bemerkung-Matching - [`scripts/test_adressen_deactivate_ordering.py`](../scripts/test_adressen_deactivate_ordering.py) - gueltigBis + reihenfolgeIndex - [`scripts/test_adressen_gueltigbis_modify.py`](../scripts/test_adressen_gueltigbis_modify.py) - gueltigBis nachträglich ändern - [`scripts/test_put_response_detail.py`](../scripts/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 `standardAnschrift` beim POST - ✅ Verhalten bei mehreren Hauptadressen **Kritische Erkenntnisse:** #### ✅ Was funktioniert: 1. **`standardAnschrift` kann beim POST gesetzt werden** ```python POST /Adressen { "standardAnschrift": true, # ← Wird akzeptiert! ... } ``` 2. **Feld wird korrekt gespeichert** - GET zeigt `standardAnschrift: true` - Bleibt persistent #### ⚠️ Was NICHT funktioniert: 1. **Keine automatische Haupt-Adress-Logik** - Neue Adressen werden **NICHT automatisch** zur Hauptadresse - `standardAnschrift` ist standardmäßig `false` - Keine "neueste Adresse = Hauptadresse" Regel 2. **Mehrere Hauptadressen sind möglich!** - Advoware deaktiviert **NICHT** alte Hauptadressen automatisch - Es können **mehrere** Adressen `standardAnschrift = true` haben - **Keine Validierung** auf "nur eine Hauptadresse" 3. **`standardAnschrift` ist 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)** ```python # 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** ```python # 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** ```python # 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:** ```python 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:** ```javascript { "isPrimary": "bool", // Master-Feld (EspoCRM managed) "advowareStandardAnschrift": "bool" // Info-Feld (Advoware Mirror) } ``` ### 10.6 Zusammenfassung **Hauptadresse-Sync ist komplex wegen:** 1. ❌ `standardAnschrift` ist READ-ONLY bei PUT 2. ❌ Mehrere Hauptadressen in Advoware möglich 3. ❌ Keine automatische Deaktivierung alter Hauptadressen **Empfohlener Ansatz:** - ✅ EspoCRM `isPrimary` als Master - ✅ Advoware `standardAnschrift` beim 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`](../scripts/test_hauptadresse_logic.py) - Hauptadresse-Logik - [`scripts/test_hauptadresse_explizit.py`](../scripts/test_hauptadresse_explizit.py) - Explizites Setzen - [`scripts/test_find_hauptadresse.py`](../scripts/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!** ```python # 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:** 1. ❌ **Kann NICHT in EspoCRM gespeichert werden** als Identifier 2. ❌ **Kann NICHT für direktes Matching** genutzt werden 3. ✅ **Muss VOR jedem PUT dynamisch ermittelt werden** via: ```python # 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:** ```python 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):** ```javascript { "advowareIndexId": "int", // ← NICHT speichern! "advowareRowId": "varchar" } ``` **Neue Empfehlung (RICHTIG):** ```javascript { // 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:** 1. **CREATE**: Setze `bemerkung = "EspoCRM-ID: {espo_id}"` 2. **UPDATE**: GET → Match via `bemerkung` → Nutze aktuellen `reihenfolgeIndex` für PUT 3. **DELETE**: Notification (kein API-DELETE) **Test-Scripts:** - [`scripts/test_find_hauptadresse.py`](../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!** ```python # 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:** 1. ❌ **Kann NICHT in EspoCRM gespeichert werden** als Identifier 2. ❌ **Kann NICHT für direktes Matching** genutzt werden 3. ✅ **Muss VOR jedem PUT dynamisch ermittelt werden** via: ```python # 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:** ```python 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):** ```javascript { "advowareIndexId": "int", // ← NICHT speichern! "advowareRowId": "varchar" } ``` **Neue Empfehlung (RICHTIG):** ```javascript { // 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:** 1. **CREATE**: Setze `bemerkung = "EspoCRM-ID: {espo_id}"` 2. **UPDATE**: GET → Match via `bemerkung` → Nutze aktuellen `reihenfolgeIndex` für PUT 3. **DELETE**: Notification (kein API-DELETE) **Test-Scripts:** - [`scripts/test_find_hauptadresse.py`](../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:** ```python 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:** ```python 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:** ```python 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):** ```python 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: - `land` geändert - `postfach`/`postfachPLZ` geändert - `standardAnschrift` (Hauptadresse) geändert - `gueltigVon`/`gueltigBis` geändert - `bemerkung` geändert (würde Matching zerstören!) **Rationale:** 1. **Keine Datenkorruption**: Nur sichere Operationen automatisch 2. **Keine API-Limitierungen umgehen**: Respektiere READ-ONLY Status 3. **Volle Transparenz**: User sieht alle manuellen Aktionen 4. **Matching bleibt stabil**: `bemerkung` niemals ändern! **Test-Scripts:** - [`scripts/test_adressen_nullen.py`](../scripts/test_adressen_nullen.py) - Zeigt dass Nullen nicht empfohlen ist