feat: Implement address synchronization between EspoCRM and Advoware
- Add AdressenMapper for transforming addresses between EspoCRM and Advoware formats. - Create AdressenSync class to handle address creation, update, and deletion synchronization. - Introduce NotificationManager for managing manual intervention notifications in case of sync issues. - Implement detailed logging for address sync operations and error handling. - Ensure READ-ONLY field changes are detected and notified for manual resolution.
This commit is contained in:
1646
bitbylaw/docs/ADRESSEN_SYNC_ANALYSE.md
Normal file
1646
bitbylaw/docs/ADRESSEN_SYNC_ANALYSE.md
Normal file
File diff suppressed because it is too large
Load Diff
254
bitbylaw/docs/ADRESSEN_SYNC_SUMMARY.md
Normal file
254
bitbylaw/docs/ADRESSEN_SYNC_SUMMARY.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# Adressen-Sync: Zusammenfassung & Implementierungsplan
|
||||||
|
|
||||||
|
**Datum**: 8. Februar 2026
|
||||||
|
**Status**: ✅ Analyse abgeschlossen, bereit für Implementierung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Executive Summary
|
||||||
|
|
||||||
|
### ✅ Was funktioniert:
|
||||||
|
- **CREATE** (POST): Alle Felder können gesetzt werden
|
||||||
|
- **UPDATE** (PUT): 4 Haupt-Adressfelder (`strasse`, `plz`, `ort`, `anschrift`)
|
||||||
|
- **MATCHING**: Via `bemerkung`-Feld mit EspoCRM-ID (stabil, READ-ONLY)
|
||||||
|
- **SYNC from Advoware**: Vollständig möglich
|
||||||
|
|
||||||
|
### ❌ Was nicht funktioniert:
|
||||||
|
- **DELETE**: 403 Forbidden (nicht verfügbar)
|
||||||
|
- **Soft-Delete**: `gueltigBis` ist READ-ONLY (kann nicht nachträglich gesetzt werden)
|
||||||
|
- **8 Felder READ-ONLY bei PUT**: `land`, `postfach`, `postfachPLZ`, `standardAnschrift`, `bemerkung`, `gueltigVon`, `gueltigBis`, `reihenfolgeIndex`
|
||||||
|
|
||||||
|
### 💡 Lösung: Hybrid-Ansatz
|
||||||
|
**Automatischer Sync + Notification-System für manuelle Eingriffe**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Implementierte Komponenten
|
||||||
|
|
||||||
|
### 1. Notification-System ✅
|
||||||
|
**Datei**: [`services/notification_utils.py`](../services/notification_utils.py)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Zentrale `NotificationManager` Klasse
|
||||||
|
- Task-Erstellung in EspoCRM mit Schritt-für-Schritt Anleitung
|
||||||
|
- In-App Notifications an assigned Users
|
||||||
|
- 6 vordefinierte Action-Types:
|
||||||
|
- `address_delete_required` - DELETE manuell nötig
|
||||||
|
- `address_reactivate_required` - Neue Adresse erstellen
|
||||||
|
- `address_field_update_required` - READ-ONLY Felder ändern
|
||||||
|
- `readonly_field_conflict` - Sync-Konflikt
|
||||||
|
- `missing_in_advoware` - Element fehlt
|
||||||
|
- `general_manual_action` - Allgemein
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
```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='65abc123',
|
||||||
|
action_type='address_delete_required',
|
||||||
|
details={
|
||||||
|
'betnr': '104860',
|
||||||
|
'strasse': 'Teststraße 123',
|
||||||
|
'plz': '30159',
|
||||||
|
'ort': 'Hannover'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# → Erstellt Task + Notification mit detaillierter Anleitung
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Umfassende Test-Suite ✅
|
||||||
|
**Test-Scripts** (alle in [`scripts/`](../scripts/)):
|
||||||
|
|
||||||
|
1. **`test_adressen_api.py`** - Haupttest (7 Tests)
|
||||||
|
- POST/PUT mit allen Feldern
|
||||||
|
- Feld-für-Feld Verifikation
|
||||||
|
- Response-Analyse
|
||||||
|
|
||||||
|
2. **`test_adressen_delete_matching.py`** - DELETE + Matching
|
||||||
|
- DELETE-Funktionalität (→ 403)
|
||||||
|
- `bemerkung`-basiertes Matching
|
||||||
|
- Stabilität von `bemerkung` bei PUT
|
||||||
|
|
||||||
|
3. **`test_adressen_deactivate_ordering.py`** - Deaktivierung
|
||||||
|
- `gueltigBis` nachträglich setzen (→ READ-ONLY)
|
||||||
|
- `reihenfolgeIndex` Verhalten
|
||||||
|
- Automatisches Ans-Ende-Reihen
|
||||||
|
|
||||||
|
4. **`test_adressen_gueltigbis_modify.py`** - Soft-Delete
|
||||||
|
- `gueltigBis` ändern (→ nicht möglich)
|
||||||
|
- Verschiedene Methoden getestet
|
||||||
|
|
||||||
|
5. **`test_put_response_detail.py`** - PUT-Analyse
|
||||||
|
- Welche Felder werden wirklich geändert
|
||||||
|
- Response vs. GET Vergleich
|
||||||
|
|
||||||
|
### 3. Dokumentation ✅
|
||||||
|
**Datei**: [`docs/ADRESSEN_SYNC_ANALYSE.md`](ADRESSEN_SYNC_ANALYSE.md)
|
||||||
|
|
||||||
|
**Inhalte:**
|
||||||
|
- Swagger API-Dokumentation
|
||||||
|
- EspoCRM Entity-Struktur
|
||||||
|
- Detaillierte Test-Ergebnisse
|
||||||
|
- Sync-Strategien (3 Optionen evaluiert)
|
||||||
|
- Finale Empfehlung: Hybrid-Ansatz
|
||||||
|
- Feld-Mappings
|
||||||
|
- Risiko-Analyse
|
||||||
|
- Implementierungsplan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Kritische Erkenntnisse
|
||||||
|
|
||||||
|
### ID-Mapping
|
||||||
|
```
|
||||||
|
❌ id = 0 → Immer 0, unbrauchbar
|
||||||
|
✅ bemerkung → Stabil (READ-ONLY), perfekt für Matching
|
||||||
|
✅ reihenfolgeIndex → Stabil, automatisch vergeben, für PUT-Endpoint
|
||||||
|
❌ rowId → Ändert sich bei PUT, nicht für Matching!
|
||||||
|
```
|
||||||
|
|
||||||
|
### PUT-Feldübersicht
|
||||||
|
| Feld | POST | PUT | Matching |
|
||||||
|
|------|------|-----|----------|
|
||||||
|
| `strasse` | ✅ | ✅ | - |
|
||||||
|
| `plz` | ✅ | ✅ | - |
|
||||||
|
| `ort` | ✅ | ✅ | - |
|
||||||
|
| `land` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `postfach` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `postfachPLZ` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `anschrift` | ✅ | ✅ | - |
|
||||||
|
| `standardAnschrift` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `bemerkung` | ✅ | ❌ READ-ONLY | ✅ Perfekt! |
|
||||||
|
| `gueltigVon` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `gueltigBis` | ✅ | ❌ READ-ONLY | - |
|
||||||
|
| `reihenfolgeIndex` | - | ❌ System | ✅ Für PUT |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Nächste Schritte
|
||||||
|
|
||||||
|
### Phase 1: Validierung ⏳
|
||||||
|
- [ ] EspoCRM CAdressen Entity prüfen
|
||||||
|
- [ ] Felder vorhanden: `advowareIndexId`, `advowareRowId`, `syncStatus`, `isActive`, `manualActionNote`
|
||||||
|
- [ ] Relation zu CBeteiligte korrekt
|
||||||
|
- [ ] Notification-System testen
|
||||||
|
- [ ] Task-Erstellung funktioniert
|
||||||
|
- [ ] Assigned Users werden benachrichtigt
|
||||||
|
|
||||||
|
### Phase 2: Mapper ⏳
|
||||||
|
- [ ] `services/adressen_mapper.py` erstellen
|
||||||
|
```python
|
||||||
|
class AdressenMapper:
|
||||||
|
def map_espocrm_to_advoware(espo_addr) -> dict
|
||||||
|
def map_advoware_to_espocrm(advo_addr) -> dict
|
||||||
|
def find_by_bemerkung(addresses, espo_id) -> dict
|
||||||
|
def detect_readonly_changes(espo, advo) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Sync-Service ⏳
|
||||||
|
- [ ] `services/adressen_sync.py` erstellen
|
||||||
|
```python
|
||||||
|
class AdressenSyncService:
|
||||||
|
async def create_address(espo_addr)
|
||||||
|
async def update_address(espo_addr)
|
||||||
|
async def delete_address(espo_addr) # → Notification
|
||||||
|
async def sync_from_advoware(betnr, espo_beteiligte_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Integration ⏳
|
||||||
|
- [ ] In bestehenden Beteiligte-Sync integrieren oder
|
||||||
|
- [ ] Eigener Adressen-Sync Step
|
||||||
|
|
||||||
|
### Phase 5: Testing ⏳
|
||||||
|
- [ ] Unit Tests für Mapper
|
||||||
|
- [ ] Integration Tests mit Test-Daten
|
||||||
|
- [ ] End-to-End Test: CREATE → UPDATE → DELETE
|
||||||
|
- [ ] Notification-Flow testen
|
||||||
|
|
||||||
|
### Phase 6: Deployment ⏳
|
||||||
|
- [ ] Staging-Test mit echten Daten
|
||||||
|
- [ ] User-Schulung: Manuelle Eingriffe
|
||||||
|
- [ ] Monitoring einrichten
|
||||||
|
- [ ] Production Rollout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Wichtige Hinweise für Entwickler
|
||||||
|
|
||||||
|
### Matching-Strategie
|
||||||
|
**IMMER via `bemerkung`-Feld:**
|
||||||
|
```python
|
||||||
|
# Beim CREATE:
|
||||||
|
bemerkung = f"EspoCRM-ID: {espocrm_address_id}"
|
||||||
|
|
||||||
|
# Beim Sync:
|
||||||
|
espocrm_id = parse_espocrm_id_from_bemerkung(advo_addr['bemerkung'])
|
||||||
|
# Robust gegen User-Änderungen:
|
||||||
|
import re
|
||||||
|
match = re.search(r'EspoCRM-ID:\s*([a-f0-9-]+)', bemerkung)
|
||||||
|
espocrm_id = match.group(1) if match else None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification Trigger
|
||||||
|
**Immer Notifications erstellen bei:**
|
||||||
|
- DELETE-Request (API nicht verfügbar)
|
||||||
|
- PUT mit READ-ONLY Feldern (land, postfach, etc.)
|
||||||
|
- Reaktivierung (neue Adresse erstellen)
|
||||||
|
- Adresse direkt in Advoware erstellt (fehlende bemerkung)
|
||||||
|
|
||||||
|
### Sync-Richtung
|
||||||
|
- **EspoCRM → Advoware**: Für CREATE/UPDATE
|
||||||
|
- **Advoware → EspoCRM**: Master für "Existenz"
|
||||||
|
- **Konflikt-Resolution**: Siehe Dokumentation
|
||||||
|
|
||||||
|
### Aktuelle Adresse-Matching
|
||||||
|
**Wichtig**: Die "aktuelle" Adresse muss in beiden Systemen gleich sein!
|
||||||
|
|
||||||
|
**Strategie:**
|
||||||
|
```python
|
||||||
|
# In Advoware: standardAnschrift = true (READ-ONLY!)
|
||||||
|
# In EspoCRM: isPrimary = true (eigenes Feld)
|
||||||
|
|
||||||
|
# Sync-Logik:
|
||||||
|
if espo_addr['isPrimary']:
|
||||||
|
# Prüfe ob Advoware-Adresse standardAnschrift = true hat
|
||||||
|
if not advo_addr['standardAnschrift']:
|
||||||
|
# → Notification: Hauptadresse manuell in Advoware setzen
|
||||||
|
await notify_main_address_mismatch(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Metriken & Monitoring
|
||||||
|
|
||||||
|
**Zu überwachende KPIs:**
|
||||||
|
- Anzahl erstellter Notifications pro Tag
|
||||||
|
- Durchschnittliche Zeit bis Task-Completion
|
||||||
|
- Anzahl gescheiterter Syncs
|
||||||
|
- READ-ONLY Feld-Konflikte (Häufigkeit)
|
||||||
|
- DELETE-Requests (manuell nötig)
|
||||||
|
|
||||||
|
**Alerts einrichten für:**
|
||||||
|
- Mehr als 5 unerledigte DELETE-Tasks pro User
|
||||||
|
- Sync-Fehlerrate > 10%
|
||||||
|
- Tasks älter als 7 Tage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Referenzen
|
||||||
|
|
||||||
|
- **Hauptdokumentation**: [`docs/ADRESSEN_SYNC_ANALYSE.md`](ADRESSEN_SYNC_ANALYSE.md)
|
||||||
|
- **Notification-Utility**: [`services/notification_utils.py`](../services/notification_utils.py)
|
||||||
|
- **Test-Scripts**: [`scripts/test_adressen_*.py`](../scripts/)
|
||||||
|
- **Swagger-Doku**: Advoware API v1 - Adressen Endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Erstellt**: 8. Februar 2026
|
||||||
|
**Autor**: GitHub Copilot
|
||||||
|
**Review**: Pending
|
||||||
696
bitbylaw/scripts/test_adressen_api.py
Normal file
696
bitbylaw/scripts/test_adressen_api.py
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
"""
|
||||||
|
Advoware Adressen-API Tester
|
||||||
|
|
||||||
|
Testet die Advoware Adressen-API umfassend, um herauszufinden:
|
||||||
|
1. Welche IDs für Mapping nutzbar sind
|
||||||
|
2. Welche Felder wirklich beschreibbar/änderbar sind
|
||||||
|
3. Wie sich die API bei mehreren Adressen verhält
|
||||||
|
|
||||||
|
Basierend auf Erfahrungen mit Beteiligte-API, wo nur 8 von vielen Feldern funktionierten.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/test_adressen_api.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
sys.path.insert(0, '/opt/motia-app/bitbylaw')
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
# Test-Konfiguration
|
||||||
|
TEST_BETNR = 104860 # Beteiligten-Nr für Tests
|
||||||
|
|
||||||
|
# ANSI Color Codes
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
CYAN = '\033[96m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
"""Mock context for logging"""
|
||||||
|
class Logger:
|
||||||
|
def info(self, msg): print(f"[INFO] {msg}")
|
||||||
|
def error(self, msg): print(f"[ERROR] {msg}")
|
||||||
|
def debug(self, msg): print(f"[DEBUG] {msg}")
|
||||||
|
def warning(self, msg): print(f"[WARNING] {msg}")
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = self.Logger()
|
||||||
|
|
||||||
|
|
||||||
|
def print_header(title: str):
|
||||||
|
"""Print formatted section header"""
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"{BOLD}{CYAN}{title}{RESET}")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def print_success(msg: str):
|
||||||
|
"""Print success message"""
|
||||||
|
print(f"{GREEN}✓ {msg}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_error(msg: str):
|
||||||
|
"""Print error message"""
|
||||||
|
print(f"{RED}✗ {msg}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_warning(msg: str):
|
||||||
|
"""Print warning message"""
|
||||||
|
print(f"{YELLOW}⚠ {msg}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_info(msg: str):
|
||||||
|
"""Print info message"""
|
||||||
|
print(f"{BLUE}ℹ {msg}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_1_get_existing_addresses():
|
||||||
|
"""Test 1: Hole bestehende Adressen und analysiere Struktur"""
|
||||||
|
print_header("TEST 1: GET Adressen - Struktur analysieren")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
endpoint = f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen'
|
||||||
|
print_info(f"GET {endpoint}")
|
||||||
|
|
||||||
|
addresses = await advo.api_call(endpoint, method='GET')
|
||||||
|
|
||||||
|
if not addresses:
|
||||||
|
print_warning("Keine Adressen gefunden - wird in Test 2 erstellen")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print_success(f"Erfolgreich {len(addresses)} Adressen abgerufen")
|
||||||
|
|
||||||
|
# Analysiere Struktur
|
||||||
|
print(f"\n{BOLD}Anzahl Adressen:{RESET} {len(addresses)}")
|
||||||
|
|
||||||
|
for i, addr in enumerate(addresses, 1):
|
||||||
|
print(f"\n{BOLD}--- Adresse {i} ---{RESET}")
|
||||||
|
print(f" id: {addr.get('id')}")
|
||||||
|
print(f" beteiligterId: {addr.get('beteiligterId')}")
|
||||||
|
print(f" reihenfolgeIndex: {addr.get('reihenfolgeIndex')}")
|
||||||
|
print(f" rowId: {addr.get('rowId')}")
|
||||||
|
print(f" strasse: {addr.get('strasse')}")
|
||||||
|
print(f" plz: {addr.get('plz')}")
|
||||||
|
print(f" ort: {addr.get('ort')}")
|
||||||
|
print(f" land: {addr.get('land')}")
|
||||||
|
print(f" postfach: {addr.get('postfach')}")
|
||||||
|
print(f" postfachPLZ: {addr.get('postfachPLZ')}")
|
||||||
|
print(f" anschrift: {addr.get('anschrift')}")
|
||||||
|
print(f" standardAnschrift: {addr.get('standardAnschrift')}")
|
||||||
|
print(f" bemerkung: {addr.get('bemerkung')}")
|
||||||
|
print(f" gueltigVon: {addr.get('gueltigVon')}")
|
||||||
|
print(f" gueltigBis: {addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
# ID-Analyse
|
||||||
|
print(f"\n{BOLD}ID-Analyse für Mapping:{RESET}")
|
||||||
|
print(f" - 'id' vorhanden: {all('id' in a for a in addresses)}")
|
||||||
|
print(f" - 'id' Typ: {type(addresses[0].get('id')) if addresses else 'N/A'}")
|
||||||
|
print(f" - 'id' eindeutig: {len(set(a.get('id') for a in addresses)) == len(addresses)}")
|
||||||
|
print(f" - 'rowId' vorhanden: {all('rowId' in a for a in addresses)}")
|
||||||
|
print(f" - 'rowId' eindeutig: {len(set(a.get('rowId') for a in addresses)) == len(addresses)}")
|
||||||
|
|
||||||
|
print_success("✓ ID-Felder 'id' und 'rowId' sind nutzbar für Mapping")
|
||||||
|
|
||||||
|
return addresses
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"GET fehlgeschlagen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_2_create_test_address():
|
||||||
|
"""Test 2: Erstelle Test-Adresse mit allen Feldern"""
|
||||||
|
print_header("TEST 2: POST - Neue Adresse erstellen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Vollständige Test-Daten mit allen Feldern
|
||||||
|
test_address = {
|
||||||
|
'strasse': 'Teststraße 123',
|
||||||
|
'plz': '30159',
|
||||||
|
'ort': 'Hannover',
|
||||||
|
'land': 'DE',
|
||||||
|
'postfach': 'PF 10 20 30',
|
||||||
|
'postfachPLZ': '30001',
|
||||||
|
'anschrift': 'Teststraße 123\n30159 Hannover\nDeutschland',
|
||||||
|
'standardAnschrift': False,
|
||||||
|
'bemerkung': f'TEST-Adresse erstellt am {datetime.now().isoformat()}',
|
||||||
|
'gueltigVon': '2026-02-08T00:00:00',
|
||||||
|
'gueltigBis': '2027-12-31T23:59:59'
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Erstelle Adresse mit allen Feldern:")
|
||||||
|
print(json.dumps(test_address, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
endpoint = f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen'
|
||||||
|
print_info(f"\nPOST {endpoint}")
|
||||||
|
|
||||||
|
result = await advo.api_call(endpoint, method='POST', json_data=test_address)
|
||||||
|
|
||||||
|
print_success("POST erfolgreich!")
|
||||||
|
print(f"\n{BOLD}Response:{RESET}")
|
||||||
|
|
||||||
|
# Advoware gibt Array zurück
|
||||||
|
if isinstance(result, list):
|
||||||
|
print_info(f"Response ist Array mit {len(result)} Elementen")
|
||||||
|
if result:
|
||||||
|
created_addr = result[0]
|
||||||
|
print(json.dumps(created_addr, indent=2, ensure_ascii=False))
|
||||||
|
return created_addr
|
||||||
|
else:
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"POST fehlgeschlagen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_3_verify_created_fields(created_addr: Dict):
|
||||||
|
"""Test 3: Vergleiche gesendete vs. zurückgegebene Daten"""
|
||||||
|
print_header("TEST 3: Feld-Verifikation - Was wurde wirklich gespeichert?")
|
||||||
|
|
||||||
|
if not created_addr:
|
||||||
|
print_error("Keine Adresse zum Verifizieren")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Erwartete vs. tatsächliche Werte
|
||||||
|
expected = {
|
||||||
|
'strasse': 'Teststraße 123',
|
||||||
|
'plz': '30159',
|
||||||
|
'ort': 'Hannover',
|
||||||
|
'land': 'DE',
|
||||||
|
'postfach': 'PF 10 20 30',
|
||||||
|
'postfachPLZ': '30001',
|
||||||
|
'anschrift': 'Teststraße 123\n30159 Hannover\nDeutschland',
|
||||||
|
'standardAnschrift': False,
|
||||||
|
'bemerkung': 'TEST-Adresse', # Partial match
|
||||||
|
'gueltigVon': '2026-02-08', # Nur Datum-Teil
|
||||||
|
'gueltigBis': '2027-12-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
working_fields = []
|
||||||
|
broken_fields = []
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Feld-für-Feld-Vergleich:{RESET}\n")
|
||||||
|
|
||||||
|
for field, expected_val in expected.items():
|
||||||
|
actual_val = created_addr.get(field)
|
||||||
|
|
||||||
|
# Vergleich
|
||||||
|
if field in ['bemerkung']:
|
||||||
|
# Partial match für Felder mit Timestamps
|
||||||
|
matches = expected_val in str(actual_val) if actual_val else False
|
||||||
|
elif field in ['gueltigVon', 'gueltigBis']:
|
||||||
|
# Datum-Vergleich (nur YYYY-MM-DD Teil)
|
||||||
|
actual_date = str(actual_val).split('T')[0] if actual_val else None
|
||||||
|
matches = actual_date == expected_val
|
||||||
|
else:
|
||||||
|
matches = actual_val == expected_val
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
print_success(f"{field:20} : {actual_val}")
|
||||||
|
working_fields.append(field)
|
||||||
|
else:
|
||||||
|
print_error(f"{field:20} : Expected '{expected_val}', Got '{actual_val}'")
|
||||||
|
broken_fields.append(field)
|
||||||
|
|
||||||
|
# Zusätzliche Felder prüfen
|
||||||
|
print(f"\n{BOLD}Zusätzliche Felder:{RESET}")
|
||||||
|
extra_fields = ['id', 'beteiligterId', 'reihenfolgeIndex', 'rowId']
|
||||||
|
for field in extra_fields:
|
||||||
|
val = created_addr.get(field)
|
||||||
|
if val is not None:
|
||||||
|
print_success(f"{field:20} : {val}")
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
print(f"\n{BOLD}{'='*60}{RESET}")
|
||||||
|
print(f"{GREEN}✓ Funktionierende Felder ({len(working_fields)}):{RESET}")
|
||||||
|
for field in working_fields:
|
||||||
|
print(f" - {field}")
|
||||||
|
|
||||||
|
if broken_fields:
|
||||||
|
print(f"\n{RED}✗ Nicht funktionierende Felder ({len(broken_fields)}):{RESET}")
|
||||||
|
for field in broken_fields:
|
||||||
|
print(f" - {field}")
|
||||||
|
|
||||||
|
return created_addr
|
||||||
|
|
||||||
|
|
||||||
|
async def test_4_update_address_full(row_id: str):
|
||||||
|
"""Test 4: Update mit allen Feldern (Read-Modify-Write Pattern)"""
|
||||||
|
print_header("TEST 4: PUT - Adresse mit allen Feldern ändern")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Lese aktuelle Adresse
|
||||||
|
print_info("Schritt 1: Lese aktuelle Adresse...")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finde via rowId
|
||||||
|
current_addr = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
if not current_addr:
|
||||||
|
print_error(f"Adresse mit rowId {row_id} nicht gefunden")
|
||||||
|
return None
|
||||||
|
|
||||||
|
addr_id = current_addr.get('reihenfolgeIndex')
|
||||||
|
print_success(f"Aktuelle Adresse geladen: {current_addr.get('strasse')} (Index: {addr_id})")
|
||||||
|
|
||||||
|
# 2. Ändere ALLE Felder
|
||||||
|
print_info("\nSchritt 2: Ändere alle Felder...")
|
||||||
|
modified_addr = {
|
||||||
|
'strasse': 'GEÄNDERT Neue Straße 999',
|
||||||
|
'plz': '10115',
|
||||||
|
'ort': 'Berlin',
|
||||||
|
'land': 'DE',
|
||||||
|
'postfach': 'PF 99 88 77',
|
||||||
|
'postfachPLZ': '10001',
|
||||||
|
'anschrift': 'GEÄNDERT Neue Straße 999\n10115 Berlin\nDeutschland',
|
||||||
|
'standardAnschrift': True, # Toggle
|
||||||
|
'bemerkung': f'GEÄNDERT am {datetime.now().isoformat()}',
|
||||||
|
'gueltigVon': '2026-03-01T00:00:00',
|
||||||
|
'gueltigBis': '2028-12-31T23:59:59'
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(modified_addr, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
# 3. Update
|
||||||
|
print_info(f"\nSchritt 3: PUT zu Advoware (Index: {addr_id})...")
|
||||||
|
endpoint = f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{addr_id}'
|
||||||
|
result = await advo.api_call(endpoint, method='PUT', json_data=modified_addr)
|
||||||
|
|
||||||
|
print_success("PUT erfolgreich!")
|
||||||
|
print(f"\n{BOLD}Response:{RESET}")
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"PUT fehlgeschlagen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_5_verify_update(row_id: str):
|
||||||
|
"""Test 5: Hole Adresse erneut und prüfe was wirklich geändert wurde"""
|
||||||
|
print_header("TEST 5: Update-Verifikation - Was wurde wirklich geändert?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finde via rowId
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
if not updated_addr:
|
||||||
|
print_error(f"Adresse mit rowId {row_id} nicht gefunden")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print_success("Adresse neu geladen")
|
||||||
|
|
||||||
|
# Erwartete geänderte Werte
|
||||||
|
expected_changes = {
|
||||||
|
'strasse': 'GEÄNDERT Neue Straße 999',
|
||||||
|
'plz': '10115',
|
||||||
|
'ort': 'Berlin',
|
||||||
|
'land': 'DE',
|
||||||
|
'postfach': 'PF 99 88 77',
|
||||||
|
'postfachPLZ': '10001',
|
||||||
|
'standardAnschrift': True,
|
||||||
|
'bemerkung': 'GEÄNDERT am',
|
||||||
|
'gueltigVon': '2026-03-01',
|
||||||
|
'gueltigBis': '2028-12-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
updatable_fields = []
|
||||||
|
readonly_fields = []
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Änderungs-Verifikation:{RESET}\n")
|
||||||
|
|
||||||
|
for field, expected_val in expected_changes.items():
|
||||||
|
actual_val = updated_addr.get(field)
|
||||||
|
|
||||||
|
# Vergleich
|
||||||
|
if field == 'bemerkung':
|
||||||
|
changed = expected_val in str(actual_val) if actual_val else False
|
||||||
|
elif field in ['gueltigVon', 'gueltigBis']:
|
||||||
|
actual_date = str(actual_val).split('T')[0] if actual_val else None
|
||||||
|
changed = actual_date == expected_val
|
||||||
|
else:
|
||||||
|
changed = actual_val == expected_val
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
print_success(f"{field:20} : ✓ GEÄNDERT → {actual_val}")
|
||||||
|
updatable_fields.append(field)
|
||||||
|
else:
|
||||||
|
print_error(f"{field:20} : ✗ NICHT GEÄNDERT (ist: {actual_val})")
|
||||||
|
readonly_fields.append(field)
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
print(f"\n{BOLD}{'='*60}{RESET}")
|
||||||
|
print(f"{GREEN}✓ Änderbare Felder ({len(updatable_fields)}):{RESET}")
|
||||||
|
for field in updatable_fields:
|
||||||
|
print(f" - {field}")
|
||||||
|
|
||||||
|
if readonly_fields:
|
||||||
|
print(f"\n{RED}✗ Nicht änderbare Felder ({len(readonly_fields)}):{RESET}")
|
||||||
|
for field in readonly_fields:
|
||||||
|
print(f" - {field}")
|
||||||
|
|
||||||
|
return updated_addr
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Verifikation fehlgeschlagen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_6_multiple_addresses_behavior():
|
||||||
|
"""Test 6: Verhalten bei mehreren Adressen"""
|
||||||
|
print_header("TEST 6: Mehrere Adressen - Verhalten testen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole alle Adressen
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Aktuelle Anzahl Adressen: {len(all_addresses)}")
|
||||||
|
|
||||||
|
# Erstelle 2. Test-Adresse
|
||||||
|
print_info("\nErstelle 2. Test-Adresse...")
|
||||||
|
test_addr_2 = {
|
||||||
|
'strasse': 'Zweite Straße 456',
|
||||||
|
'plz': '20095',
|
||||||
|
'ort': 'Hamburg',
|
||||||
|
'land': 'DE',
|
||||||
|
'standardAnschrift': False,
|
||||||
|
'bemerkung': 'TEST-Adresse 2'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=test_addr_2
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, list) and result:
|
||||||
|
addr_2 = result[0]
|
||||||
|
print_success(f"2. Adresse erstellt: ID {addr_2.get('id')}")
|
||||||
|
|
||||||
|
# Hole erneut alle Adressen
|
||||||
|
all_addresses_after = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success(f"Neue Anzahl Adressen: {len(all_addresses_after)}")
|
||||||
|
|
||||||
|
# Analysiere reihenfolgeIndex
|
||||||
|
print(f"\n{BOLD}Reihenfolge-Analyse:{RESET}")
|
||||||
|
for addr in all_addresses_after:
|
||||||
|
print(f" ID {addr.get('id'):5} | Index: {addr.get('reihenfolgeIndex'):3} | "
|
||||||
|
f"Standard: {addr.get('standardAnschrift')} | {addr.get('ort')}")
|
||||||
|
|
||||||
|
# Prüfe standardAnschrift Logik
|
||||||
|
standard_addrs = [a for a in all_addresses_after if a.get('standardAnschrift')]
|
||||||
|
print(f"\n{BOLD}standardAnschrift-Logik:{RESET}")
|
||||||
|
if len(standard_addrs) == 0:
|
||||||
|
print_warning("Keine Adresse als Standard markiert")
|
||||||
|
elif len(standard_addrs) == 1:
|
||||||
|
print_success(f"Genau 1 Standard-Adresse (ID: {standard_addrs[0].get('id')})")
|
||||||
|
else:
|
||||||
|
print_error(f"MEHRERE Standard-Adressen: {len(standard_addrs)}")
|
||||||
|
|
||||||
|
return all_addresses_after
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Test fehlgeschlagen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_7_field_by_field_update(row_id: str):
|
||||||
|
"""Test 7: Teste jedes Feld einzeln (einzelne Updates)"""
|
||||||
|
print_header("TEST 7: Feld-für-Feld Update-Test")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Hole Index für PUT
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
test_addr = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
if not test_addr:
|
||||||
|
print_error("Test-Adresse nicht gefunden")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
addr_index = test_addr.get('reihenfolgeIndex')
|
||||||
|
print_info(f"Verwende Adresse mit Index: {addr_index}")
|
||||||
|
|
||||||
|
# Test-Felder mit Werten
|
||||||
|
test_fields = {
|
||||||
|
'strasse': 'Einzeltest Straße',
|
||||||
|
'plz': '80331',
|
||||||
|
'ort': 'München',
|
||||||
|
'land': 'AT',
|
||||||
|
'postfach': 'PF 11 22',
|
||||||
|
'postfachPLZ': '80001',
|
||||||
|
'anschrift': 'Formatierte Anschrift\nTest',
|
||||||
|
'standardAnschrift': True,
|
||||||
|
'bemerkung': 'Einzelfeld-Test',
|
||||||
|
'gueltigVon': '2026-04-01T00:00:00',
|
||||||
|
'gueltigBis': '2026-12-31T23:59:59'
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for field_name, test_value in test_fields.items():
|
||||||
|
print(f"\n{BOLD}Test Feld: {field_name}{RESET}")
|
||||||
|
print_info(f"Setze auf: {test_value}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Lese aktuelle Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
current = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
|
||||||
|
if not current:
|
||||||
|
print_error(f"Adresse nicht gefunden")
|
||||||
|
results[field_name] = 'FAILED'
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Update nur dieses eine Feld
|
||||||
|
update_data = {
|
||||||
|
'strasse': current.get('strasse'),
|
||||||
|
'plz': current.get('plz'),
|
||||||
|
'ort': current.get('ort'),
|
||||||
|
'land': current.get('land'),
|
||||||
|
'standardAnschrift': current.get('standardAnschrift', False)
|
||||||
|
}
|
||||||
|
update_data[field_name] = test_value
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{addr_index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Verifiziere
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
updated = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
|
||||||
|
actual_value = updated.get(field_name)
|
||||||
|
|
||||||
|
# Vergleich (mit Toleranz für Datumsfelder)
|
||||||
|
if field_name in ['gueltigVon', 'gueltigBis']:
|
||||||
|
expected_date = test_value.split('T')[0]
|
||||||
|
actual_date = str(actual_value).split('T')[0] if actual_value else None
|
||||||
|
success = actual_date == expected_date
|
||||||
|
else:
|
||||||
|
success = actual_value == test_value
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print_success(f"✓ FUNKTIONIERT: {actual_value}")
|
||||||
|
results[field_name] = 'WORKING'
|
||||||
|
else:
|
||||||
|
print_error(f"✗ FUNKTIONIERT NICHT: Expected '{test_value}', Got '{actual_value}'")
|
||||||
|
results[field_name] = 'BROKEN'
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
results[field_name] = 'ERROR'
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5) # Rate limiting
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
print(f"\n{BOLD}{'='*60}{RESET}")
|
||||||
|
print(f"{BOLD}FINAL RESULTS - Feld-für-Feld Test:{RESET}\n")
|
||||||
|
|
||||||
|
working = [f for f, r in results.items() if r == 'WORKING']
|
||||||
|
broken = [f for f, r in results.items() if r == 'BROKEN']
|
||||||
|
errors = [f for f, r in results.items() if r == 'ERROR']
|
||||||
|
|
||||||
|
print(f"{GREEN}✓ WORKING ({len(working)}):{RESET}")
|
||||||
|
for f in working:
|
||||||
|
print(f" - {f}")
|
||||||
|
|
||||||
|
if broken:
|
||||||
|
print(f"\n{RED}✗ BROKEN ({len(broken)}):{RESET}")
|
||||||
|
for f in broken:
|
||||||
|
print(f" - {f}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"\n{YELLOW}⚠ ERRORS ({len(errors)}):{RESET}")
|
||||||
|
for f in errors:
|
||||||
|
print(f" - {f}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Haupt-Test-Ablauf"""
|
||||||
|
print(f"\n{BOLD}{CYAN}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}║ ADVOWARE ADRESSEN-API - UMFASSENDER FUNKTIONS-TEST ║{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}╚══════════════════════════════════════════════════════════════╝{RESET}")
|
||||||
|
print(f"\n{BOLD}Test-Konfiguration:{RESET}")
|
||||||
|
print(f" BetNr: {TEST_BETNR}")
|
||||||
|
print(f" Datum: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Test 1: GET existing
|
||||||
|
existing_addresses = await test_1_get_existing_addresses()
|
||||||
|
|
||||||
|
# Test 2: POST new
|
||||||
|
created_addr = await test_2_create_test_address()
|
||||||
|
|
||||||
|
if not created_addr:
|
||||||
|
print_error("\nTest abgebrochen: Konnte keine Adresse erstellen")
|
||||||
|
return
|
||||||
|
|
||||||
|
row_id = created_addr.get('rowId')
|
||||||
|
initial_id = created_addr.get('id')
|
||||||
|
|
||||||
|
if not row_id:
|
||||||
|
print_error("\nTest abgebrochen: Keine rowId zurückgegeben")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_warning(f"\n⚠️ KRITISCH: POST gibt id={initial_id} zurück")
|
||||||
|
print_info(f"rowId: {row_id}")
|
||||||
|
|
||||||
|
# Hole Adressen erneut, um echte ID zu finden
|
||||||
|
print_info("\nHole Adressen erneut, um zu prüfen...")
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finde via rowId
|
||||||
|
found_addr = next((a for a in all_addresses if a.get('rowId') == row_id), None)
|
||||||
|
if found_addr:
|
||||||
|
actual_id = found_addr.get('id')
|
||||||
|
actual_index = found_addr.get('reihenfolgeIndex')
|
||||||
|
print_success(f"✓ Adresse via rowId gefunden:")
|
||||||
|
print(f" - id: {actual_id}")
|
||||||
|
print(f" - reihenfolgeIndex: {actual_index}")
|
||||||
|
print(f" - rowId: {row_id}")
|
||||||
|
|
||||||
|
# KRITISCHE ERKENNTNIS
|
||||||
|
if actual_id == 0:
|
||||||
|
print_error("\n❌ KRITISCH: 'id' ist immer 0 - NICHT NUTZBAR für Mapping!")
|
||||||
|
print_success(f"✓ Nur 'rowId' ist eindeutig → MUSS für Mapping verwendet werden")
|
||||||
|
print_warning(f"⚠️ 'reihenfolgeIndex' könnte als Alternative dienen: {actual_index}")
|
||||||
|
|
||||||
|
# Verwende reihenfolgeIndex als "ID"
|
||||||
|
addr_id = actual_index
|
||||||
|
print_info(f"\n>>> Verwende reihenfolgeIndex={addr_id} für weitere Tests")
|
||||||
|
else:
|
||||||
|
addr_id = actual_id
|
||||||
|
print_info(f"\n>>> Test-Adressen-ID: {addr_id}")
|
||||||
|
else:
|
||||||
|
print_error("Konnte Adresse nicht via rowId finden")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler beim Abrufen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 3: Verify created fields
|
||||||
|
await test_3_verify_created_fields(created_addr)
|
||||||
|
|
||||||
|
# Test 4: Update full
|
||||||
|
await test_4_update_address_full(row_id)
|
||||||
|
|
||||||
|
# Test 5: Verify update
|
||||||
|
await test_5_verify_update(row_id)
|
||||||
|
|
||||||
|
# Test 6: Multiple addresses
|
||||||
|
await test_6_multiple_addresses_behavior()
|
||||||
|
|
||||||
|
# Test 7: Field-by-field (most important!)
|
||||||
|
await test_7_field_by_field_update(row_id)
|
||||||
|
|
||||||
|
# Final Summary
|
||||||
|
print(f"\n{BOLD}{CYAN}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}║ TEST ABGESCHLOSSEN ║{RESET}")
|
||||||
|
print(f"{BOLD}{CYAN}╚══════════════════════════════════════════════════════════════╝{RESET}")
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Wichtigste Erkenntnisse:{RESET}")
|
||||||
|
print(f" - Test-Adresse rowId: {row_id}")
|
||||||
|
print(f" - ❌ KRITISCH: 'id' ist immer 0 - nicht nutzbar!")
|
||||||
|
print(f" - ✓ 'rowId' ist eindeutig → MUSS für Mapping verwendet werden")
|
||||||
|
print(f" - Siehe Feld-für-Feld Ergebnisse oben")
|
||||||
|
print(f" - Dokumentation wird in ADRESSEN_SYNC_ANALYSE.md aktualisiert")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ ACHTUNG:{RESET} Test-Adressen wurden in Advoware erstellt!")
|
||||||
|
print(f" Diese sollten manuell gelöscht oder via Support entfernt werden.")
|
||||||
|
print(f" Test-Adressen enthalten 'TEST' oder 'GEÄNDERT' im Text.\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
466
bitbylaw/scripts/test_adressen_deactivate_ordering.py
Normal file
466
bitbylaw/scripts/test_adressen_deactivate_ordering.py
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Deaktivierung via gueltigBis + reihenfolgeIndex-Verhalten
|
||||||
|
================================================================
|
||||||
|
|
||||||
|
Ziele:
|
||||||
|
1. Teste ob abgelaufene Adressen (gueltigBis < heute) ausgeblendet werden
|
||||||
|
2. Teste ob man reihenfolgeIndex beim POST setzen kann
|
||||||
|
3. Teste ob neue Adressen automatisch ans Ende rutschen
|
||||||
|
4. Teste ob man reihenfolgeIndex via PUT ändern kann (Sortierung)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
# Test-Konfiguration
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
# ANSI Color codes
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_header(text):
|
||||||
|
print(f"\n{BOLD}{'='*80}{RESET}")
|
||||||
|
print(f"{BOLD}{text}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*80}{RESET}\n")
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_warning(text):
|
||||||
|
print(f"{YELLOW}⚠ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
"""Minimal logger für AdvowareAPI"""
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): print_error(msg)
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
"""Minimal context für AdvowareAPI"""
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
def log_info(self, msg): pass
|
||||||
|
def log_error(self, msg): print_error(msg)
|
||||||
|
def log_debug(self, msg): pass
|
||||||
|
|
||||||
|
|
||||||
|
async def test_1_create_expired_address():
|
||||||
|
"""Test 1: Erstelle Adresse mit gueltigBis in der Vergangenheit"""
|
||||||
|
print_header("TEST 1: Adresse mit gueltigBis in Vergangenheit (abgelaufen)")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Datum in der Vergangenheit
|
||||||
|
expired_date = "2023-12-31T23:59:59"
|
||||||
|
|
||||||
|
address_data = {
|
||||||
|
"strasse": "Abgelaufene Straße 99",
|
||||||
|
"plz": "99999",
|
||||||
|
"ort": "Vergangenheit",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": "TEST-ABGELAUFEN: Diese Adresse ist seit 2023 ungültig",
|
||||||
|
"gueltigVon": "2020-01-01T00:00:00",
|
||||||
|
"gueltigBis": expired_date # ← In der Vergangenheit!
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info(f"Erstelle Adresse mit gueltigBis: {expired_date} (vor 2+ Jahren)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
addr = result[0]
|
||||||
|
print_success(f"✓ Adresse erstellt: rowId={addr.get('rowId')}")
|
||||||
|
print_info(f" gueltigBis: {addr.get('gueltigBis')}")
|
||||||
|
print_info(f" reihenfolgeIndex: {addr.get('reihenfolgeIndex')}")
|
||||||
|
return addr.get('bemerkung')
|
||||||
|
else:
|
||||||
|
print_error("POST lieferte keine Response")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_2_check_if_expired_address_visible():
|
||||||
|
"""Test 2: Prüfe ob abgelaufene Adresse in GET sichtbar ist"""
|
||||||
|
print_header("TEST 2: Ist abgelaufene Adresse in GET sichtbar?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Gesamtanzahl Adressen: {len(all_addresses)}")
|
||||||
|
|
||||||
|
# Suche abgelaufene Adresse
|
||||||
|
expired_found = None
|
||||||
|
active_count = 0
|
||||||
|
expired_count = 0
|
||||||
|
|
||||||
|
today = datetime.now()
|
||||||
|
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
gueltig_bis = addr.get('gueltigBis')
|
||||||
|
|
||||||
|
if 'TEST-ABGELAUFEN' in bemerkung:
|
||||||
|
expired_found = addr
|
||||||
|
print_success(f"\n✓ Abgelaufene Test-Adresse gefunden!")
|
||||||
|
print_info(f" Index: {addr.get('reihenfolgeIndex')}")
|
||||||
|
print_info(f" gueltigBis: {gueltig_bis}")
|
||||||
|
print_info(f" Straße: {addr.get('strasse')}")
|
||||||
|
|
||||||
|
# Zähle aktive vs. abgelaufene
|
||||||
|
if gueltig_bis:
|
||||||
|
try:
|
||||||
|
bis_date = datetime.fromisoformat(gueltig_bis.replace('Z', '+00:00'))
|
||||||
|
if bis_date < today:
|
||||||
|
expired_count += 1
|
||||||
|
else:
|
||||||
|
active_count += 1
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Statistik:{RESET}")
|
||||||
|
print(f" Aktive Adressen (gueltigBis > heute): {active_count}")
|
||||||
|
print(f" Abgelaufene Adressen (gueltigBis < heute): {expired_count}")
|
||||||
|
print(f" Ohne gueltigBis: {len(all_addresses) - active_count - expired_count}")
|
||||||
|
|
||||||
|
if expired_found:
|
||||||
|
print_error("\n❌ WICHTIG: Abgelaufene Adressen werden NICHT gefiltert!")
|
||||||
|
print_warning("⚠ GET /Adressen zeigt ALLE Adressen, auch abgelaufene")
|
||||||
|
print_info("💡 Filtern nach gueltigBis muss CLIENT-seitig erfolgen")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_success("\n✓ Abgelaufene Adresse nicht sichtbar (wird gefiltert)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_3_create_with_explicit_reihenfolgeIndex():
|
||||||
|
"""Test 3: Versuche reihenfolgeIndex beim POST zu setzen"""
|
||||||
|
print_header("TEST 3: Kann man reihenfolgeIndex beim POST setzen?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Versuche mit explizitem Index
|
||||||
|
address_data = {
|
||||||
|
"reihenfolgeIndex": 999, # ← Versuche expliziten Index
|
||||||
|
"strasse": "Test Index 999",
|
||||||
|
"plz": "88888",
|
||||||
|
"ort": "Indextest",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": "TEST-INDEX: Versuch mit explizitem reihenfolgeIndex=999"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Versuche POST mit reihenfolgeIndex=999...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
addr = result[0]
|
||||||
|
actual_index = addr.get('reihenfolgeIndex')
|
||||||
|
print_info(f"Response reihenfolgeIndex: {actual_index}")
|
||||||
|
|
||||||
|
# Hole alle Adressen und prüfe wo sie gelandet ist
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
found = None
|
||||||
|
for a in all_addresses:
|
||||||
|
if (a.get('bemerkung') or '').startswith('TEST-INDEX'):
|
||||||
|
found = a
|
||||||
|
break
|
||||||
|
|
||||||
|
if found:
|
||||||
|
real_index = found.get('reihenfolgeIndex')
|
||||||
|
print_info(f"GET zeigt reihenfolgeIndex: {real_index}")
|
||||||
|
|
||||||
|
if real_index == 999:
|
||||||
|
print_success("\n✓ reihenfolgeIndex kann explizit gesetzt werden!")
|
||||||
|
print_warning("⚠ ABER: Das könnte bestehende Adressen verschieben!")
|
||||||
|
elif real_index == 0:
|
||||||
|
print_warning("\n⚠ POST gibt reihenfolgeIndex=0 zurück")
|
||||||
|
print_info("→ Echter Index wird erst nach GET sichtbar")
|
||||||
|
else:
|
||||||
|
print_error(f"\n❌ reihenfolgeIndex={real_index} ignoriert Vorgabe (999)")
|
||||||
|
print_success("✓ Index wird automatisch vergeben (ans Ende)")
|
||||||
|
|
||||||
|
return real_index
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_4_create_multiple_check_ordering():
|
||||||
|
"""Test 4: Erstelle mehrere Adressen und prüfe Reihenfolge"""
|
||||||
|
print_header("TEST 4: Mehrere neue Adressen - werden sie ans Ende gereiht?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
print_info("Hole aktuelle Adressen...")
|
||||||
|
all_before = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
max_index_before = max([a.get('reihenfolgeIndex', 0) for a in all_before])
|
||||||
|
count_before = len(all_before)
|
||||||
|
print_info(f" Anzahl vorher: {count_before}")
|
||||||
|
print_info(f" Höchster Index: {max_index_before}")
|
||||||
|
|
||||||
|
# Erstelle 3 neue Adressen
|
||||||
|
print_info("\nErstelle 3 neue Adressen...")
|
||||||
|
created_ids = []
|
||||||
|
|
||||||
|
for i in range(1, 4):
|
||||||
|
address_data = {
|
||||||
|
"strasse": f"Reihenfolge-Test {i}",
|
||||||
|
"plz": f"7777{i}",
|
||||||
|
"ort": f"Stadt-{i}",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": f"TEST-REIHENFOLGE-{i}"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
if result and len(result) > 0:
|
||||||
|
created_ids.append(f"TEST-REIHENFOLGE-{i}")
|
||||||
|
print_success(f" ✓ Adresse {i} erstellt")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f" ✗ Fehler bei Adresse {i}: {e}")
|
||||||
|
|
||||||
|
# Hole alle Adressen erneut
|
||||||
|
print_info("\nHole Adressen erneut...")
|
||||||
|
all_after = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
count_after = len(all_after)
|
||||||
|
print_info(f" Anzahl nachher: {count_after}")
|
||||||
|
print_info(f" Neue Adressen: {count_after - count_before}")
|
||||||
|
|
||||||
|
# Finde unsere Test-Adressen
|
||||||
|
print(f"\n{BOLD}Reihenfolge der neuen Test-Adressen:{RESET}")
|
||||||
|
test_addresses = []
|
||||||
|
for addr in all_after:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if 'TEST-REIHENFOLGE-' in bemerkung:
|
||||||
|
test_addresses.append({
|
||||||
|
'bemerkung': bemerkung,
|
||||||
|
'index': addr.get('reihenfolgeIndex'),
|
||||||
|
'strasse': addr.get('strasse')
|
||||||
|
})
|
||||||
|
|
||||||
|
test_addresses.sort(key=lambda x: x['index'])
|
||||||
|
|
||||||
|
for t in test_addresses:
|
||||||
|
print(f" Index {t['index']:2d}: {t['bemerkung']} ({t['strasse']})")
|
||||||
|
|
||||||
|
# Analyse
|
||||||
|
if len(test_addresses) >= 3:
|
||||||
|
indices = [t['index'] for t in test_addresses[-3:]] # Letzten 3
|
||||||
|
if indices == sorted(indices) and indices[-1] > max_index_before:
|
||||||
|
print_success("\n✓✓✓ Neue Adressen werden automatisch ANS ENDE gereiht!")
|
||||||
|
print_success("✓ Indices sind aufsteigend und fortlaufend")
|
||||||
|
print_info(f" Neue Indices: {indices}")
|
||||||
|
else:
|
||||||
|
print_warning(f"\n⚠ Unerwartete Reihenfolge: {indices}")
|
||||||
|
|
||||||
|
return test_addresses
|
||||||
|
|
||||||
|
|
||||||
|
async def test_5_try_change_reihenfolgeIndex_via_put():
|
||||||
|
"""Test 5: Versuche reihenfolgeIndex via PUT zu ändern"""
|
||||||
|
print_header("TEST 5: Kann man reihenfolgeIndex via PUT ändern?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Finde Test-Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addr = None
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if 'TEST-REIHENFOLGE-1' in bemerkung:
|
||||||
|
test_addr = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if not test_addr:
|
||||||
|
print_error("Test-Adresse nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_index = test_addr.get('reihenfolgeIndex')
|
||||||
|
new_index = 1 # Versuche an erste Position zu setzen
|
||||||
|
|
||||||
|
print_info(f"Aktueller Index: {current_index}")
|
||||||
|
print_info(f"Versuche Index zu ändern auf: {new_index}")
|
||||||
|
|
||||||
|
# PUT mit neuem reihenfolgeIndex
|
||||||
|
update_data = {
|
||||||
|
"reihenfolgeIndex": new_index,
|
||||||
|
"strasse": test_addr.get('strasse'),
|
||||||
|
"plz": test_addr.get('plz'),
|
||||||
|
"ort": test_addr.get('ort'),
|
||||||
|
"land": test_addr.get('land')
|
||||||
|
}
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{current_index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("✓ PUT erfolgreich")
|
||||||
|
|
||||||
|
# Prüfe Ergebnis
|
||||||
|
print_info("\nPrüfe neuen Index...")
|
||||||
|
all_after = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
for addr in all_after:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if 'TEST-REIHENFOLGE-1' in bemerkung:
|
||||||
|
result_index = addr.get('reihenfolgeIndex')
|
||||||
|
print_info(f"Index nach PUT: {result_index}")
|
||||||
|
|
||||||
|
if result_index == new_index:
|
||||||
|
print_success("\n✓✓✓ reihenfolgeIndex KANN via PUT geändert werden!")
|
||||||
|
print_warning("⚠ Das könnte andere Adressen verschieben!")
|
||||||
|
else:
|
||||||
|
print_error(f"\n❌ reihenfolgeIndex NICHT änderbar (bleibt {result_index})")
|
||||||
|
print_success("✓ Index ist READ-ONLY bei PUT")
|
||||||
|
|
||||||
|
return result_index == new_index
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ Deaktivierung + reihenfolgeIndex Tests für Adressen ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"Test-Konfiguration:")
|
||||||
|
print(f" BetNr: {TEST_BETNR}")
|
||||||
|
print(f" Datum: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Test 1: Abgelaufene Adresse erstellen
|
||||||
|
await test_1_create_expired_address()
|
||||||
|
|
||||||
|
# Test 2: Ist abgelaufene Adresse sichtbar?
|
||||||
|
visible = await test_2_check_if_expired_address_visible()
|
||||||
|
|
||||||
|
# Test 3: Expliziter reihenfolgeIndex
|
||||||
|
await test_3_create_with_explicit_reihenfolgeIndex()
|
||||||
|
|
||||||
|
# Test 4: Mehrere Adressen - Reihenfolge
|
||||||
|
await test_4_create_multiple_check_ordering()
|
||||||
|
|
||||||
|
# Test 5: reihenfolgeIndex ändern via PUT
|
||||||
|
changeable = await test_5_try_change_reihenfolgeIndex_via_put()
|
||||||
|
|
||||||
|
# Finale Zusammenfassung
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ FINALE ERKENNTNISSE ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"{BOLD}1. Deaktivierung via gueltigBis:{RESET}")
|
||||||
|
if visible:
|
||||||
|
print_error(" ❌ Abgelaufene Adressen werden NICHT automatisch gefiltert")
|
||||||
|
print_warning(" ⚠ GET /Adressen zeigt alle Adressen (auch abgelaufen)")
|
||||||
|
print_info(" 💡 Soft-Delete via gueltigBis ist möglich")
|
||||||
|
print_info(" 💡 Aber: Filtern muss CLIENT-seitig erfolgen")
|
||||||
|
print_info(" 💡 Strategie: In EspoCRM als 'inactive' markieren wenn gueltigBis < heute")
|
||||||
|
else:
|
||||||
|
print_success(" ✓ Abgelaufene Adressen werden automatisch ausgeblendet")
|
||||||
|
print_success(" ✓ gueltigBis eignet sich perfekt für Soft-Delete")
|
||||||
|
|
||||||
|
print(f"\n{BOLD}2. reihenfolgeIndex Verhalten:{RESET}")
|
||||||
|
print_info(" • Neue Adressen werden automatisch ans Ende gereiht")
|
||||||
|
print_info(" • Index wird vom System vergeben (fortlaufend)")
|
||||||
|
if changeable:
|
||||||
|
print_warning(" ⚠ reihenfolgeIndex kann via PUT geändert werden")
|
||||||
|
print_warning(" ⚠ Vorsicht: Könnte andere Adressen verschieben")
|
||||||
|
else:
|
||||||
|
print_success(" ✓ reihenfolgeIndex ist READ-ONLY bei PUT (stabil)")
|
||||||
|
|
||||||
|
print(f"\n{BOLD}3. Sync-Empfehlungen:{RESET}")
|
||||||
|
print_success(" ✓ Nutze 'bemerkung' für EspoCRM-ID Matching (stabil)")
|
||||||
|
print_success(" ✓ Nutze 'gueltigBis' für Soft-Delete (setze auf gestern)")
|
||||||
|
print_success(" ✓ Nutze 'reihenfolgeIndex' nur für PUT (nicht für Matching)")
|
||||||
|
print_info(" 💡 Workflow: GET → parse bemerkung → match → PUT via Index")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ ACHTUNG: Test-Adressen mit 'TEST-' im bemerkung-Feld{RESET}")
|
||||||
|
print(f"{YELLOW} sollten manuell bereinigt werden.{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
457
bitbylaw/scripts/test_adressen_delete_matching.py
Normal file
457
bitbylaw/scripts/test_adressen_delete_matching.py
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: DELETE + bemerkung-basiertes Matching für Adressen
|
||||||
|
==========================================================
|
||||||
|
|
||||||
|
Ziele:
|
||||||
|
1. Teste ob DELETE funktioniert
|
||||||
|
2. Teste ob reihenfolgeIndex nach DELETE neu sortiert wird
|
||||||
|
3. Teste bemerkung als Matching-Field mit EspoCRM-ID
|
||||||
|
4. Validiere ob bemerkung stabil bleibt bei PUT
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
# Test-Konfiguration
|
||||||
|
TEST_BETNR = 104860 # Test Beteiligte
|
||||||
|
ESPOCRM_TEST_IDS = ["espo-001", "espo-002", "espo-003"]
|
||||||
|
|
||||||
|
# ANSI Color codes
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_header(text):
|
||||||
|
print(f"\n{BOLD}{'='*80}{RESET}")
|
||||||
|
print(f"{BOLD}{text}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*80}{RESET}\n")
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_warning(text):
|
||||||
|
print(f"{YELLOW}⚠ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
"""Minimal logger für AdvowareAPI"""
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): print_error(msg)
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
"""Minimal context für AdvowareAPI"""
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
def log_info(self, msg): pass
|
||||||
|
def log_error(self, msg): print_error(msg)
|
||||||
|
def log_debug(self, msg): pass
|
||||||
|
|
||||||
|
|
||||||
|
async def test_1_create_addresses_with_espocrm_ids():
|
||||||
|
"""Test 1: Erstelle 3 Adressen mit EspoCRM-IDs im bemerkung-Feld"""
|
||||||
|
print_header("TEST 1: Erstelle Adressen mit EspoCRM-IDs im bemerkung-Feld")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
created_addresses = []
|
||||||
|
|
||||||
|
for i, espo_id in enumerate(ESPOCRM_TEST_IDS, 1):
|
||||||
|
print_info(f"\nErstelle Adresse {i} mit EspoCRM-ID: {espo_id}")
|
||||||
|
|
||||||
|
address_data = {
|
||||||
|
"strasse": f"Teststraße {i*10}",
|
||||||
|
"plz": f"3015{i}",
|
||||||
|
"ort": f"Testort-{i}",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": f"EspoCRM-ID: {espo_id}", # ← Unsere Sync-ID!
|
||||||
|
"gueltigVon": f"2026-02-0{i}T00:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
addr = result[0]
|
||||||
|
created_addresses.append({
|
||||||
|
'espo_id': espo_id,
|
||||||
|
'rowId': addr.get('rowId'),
|
||||||
|
'reihenfolgeIndex': addr.get('reihenfolgeIndex'),
|
||||||
|
'bemerkung': addr.get('bemerkung')
|
||||||
|
})
|
||||||
|
print_success(f"✓ Erstellt: rowId={addr.get('rowId')}, Index={addr.get('reihenfolgeIndex')}")
|
||||||
|
print_info(f" bemerkung: {addr.get('bemerkung')}")
|
||||||
|
else:
|
||||||
|
print_error("POST lieferte leere Response")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler beim Erstellen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
print_success(f"\n✓ {len(created_addresses)} Adressen erfolgreich erstellt")
|
||||||
|
return created_addresses
|
||||||
|
|
||||||
|
|
||||||
|
async def test_2_find_addresses_by_espocrm_id():
|
||||||
|
"""Test 2: Finde Adressen via EspoCRM-ID im bemerkung-Feld"""
|
||||||
|
print_header("TEST 2: Finde Adressen via EspoCRM-ID (bemerkung-Matching)")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Gesamtanzahl Adressen: {len(all_addresses)}")
|
||||||
|
|
||||||
|
# Parse bemerkung und finde unsere IDs
|
||||||
|
found_mapping = {}
|
||||||
|
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung', '')
|
||||||
|
if bemerkung and 'EspoCRM-ID:' in bemerkung:
|
||||||
|
# Parse: "EspoCRM-ID: espo-001" → "espo-001"
|
||||||
|
espo_id = bemerkung.split('EspoCRM-ID:')[1].strip()
|
||||||
|
found_mapping[espo_id] = {
|
||||||
|
'reihenfolgeIndex': addr.get('reihenfolgeIndex'),
|
||||||
|
'rowId': addr.get('rowId'),
|
||||||
|
'strasse': addr.get('strasse'),
|
||||||
|
'bemerkung': bemerkung
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success(f"\n✓ {len(found_mapping)} Adressen mit EspoCRM-ID gefunden:")
|
||||||
|
for espo_id, data in found_mapping.items():
|
||||||
|
print(f" {espo_id}:")
|
||||||
|
print(f" - Index: {data['reihenfolgeIndex']}")
|
||||||
|
print(f" - Straße: {data['strasse']}")
|
||||||
|
print(f" - rowId: {data['rowId']}")
|
||||||
|
|
||||||
|
# Validierung
|
||||||
|
for test_id in ESPOCRM_TEST_IDS:
|
||||||
|
if test_id in found_mapping:
|
||||||
|
print_success(f"✓ {test_id} gefunden!")
|
||||||
|
else:
|
||||||
|
print_error(f"✗ {test_id} NICHT gefunden!")
|
||||||
|
|
||||||
|
return found_mapping
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler beim Abrufen: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_3_update_address_check_bemerkung_stability():
|
||||||
|
"""Test 3: Versuche bemerkung zu ändern und prüfe Stabilität"""
|
||||||
|
print_header("TEST 3: Teste ob bemerkung bei PUT stabil bleibt")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole Adressen
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finde erste Test-Adresse
|
||||||
|
test_addr = None
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if bemerkung and 'EspoCRM-ID: espo-001' in bemerkung:
|
||||||
|
test_addr = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if not test_addr:
|
||||||
|
print_error("Test-Adresse mit espo-001 nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
original_bemerkung = test_addr.get('bemerkung')
|
||||||
|
reihenfolge_index = test_addr.get('reihenfolgeIndex')
|
||||||
|
|
||||||
|
print_info(f"Test-Adresse Index: {reihenfolge_index}")
|
||||||
|
print_info(f"Original bemerkung: {original_bemerkung}")
|
||||||
|
|
||||||
|
# Versuche Update mit ANDERER bemerkung
|
||||||
|
print_info("\nVersuche bemerkung zu ändern via PUT...")
|
||||||
|
update_data = {
|
||||||
|
"strasse": test_addr.get('strasse'),
|
||||||
|
"plz": test_addr.get('plz'),
|
||||||
|
"ort": "GEÄNDERT-ORT", # Ändere ort
|
||||||
|
"land": test_addr.get('land'),
|
||||||
|
"bemerkung": "GEÄNDERT: Diese bemerkung sollte NICHT überschrieben werden!"
|
||||||
|
}
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{reihenfolge_index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hole erneut und prüfe
|
||||||
|
print_info("\nHole Adresse erneut und prüfe bemerkung...")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == reihenfolge_index), None)
|
||||||
|
if updated_addr:
|
||||||
|
updated_bemerkung = updated_addr.get('bemerkung')
|
||||||
|
updated_ort = updated_addr.get('ort')
|
||||||
|
|
||||||
|
print_info(f"Nach PUT bemerkung: {updated_bemerkung}")
|
||||||
|
print_info(f"Nach PUT ort: {updated_ort}")
|
||||||
|
|
||||||
|
if updated_bemerkung == original_bemerkung:
|
||||||
|
print_success("\n✓✓✓ PERFEKT: bemerkung ist READ-ONLY bei PUT!")
|
||||||
|
print_success("✓ EspoCRM-ID bleibt stabil → Perfekt für Matching!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_warning("\n⚠ bemerkung wurde geändert - nicht stabil!")
|
||||||
|
print_error(f" Original: {original_bemerkung}")
|
||||||
|
print_error(f" Neu: {updated_bemerkung}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print_error("Adresse nach PUT nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_4_delete_middle_address_check_reindex():
|
||||||
|
"""Test 4: Lösche mittlere Adresse und prüfe ob Indices neu sortiert werden"""
|
||||||
|
print_header("TEST 4: DELETE - Werden reihenfolgeIndex neu sortiert?")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole aktuelle Adressen
|
||||||
|
print_info("VOR DELETE:")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Zeige nur unsere Test-Adressen
|
||||||
|
test_addresses_before = []
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if bemerkung and 'EspoCRM-ID:' in bemerkung:
|
||||||
|
test_addresses_before.append({
|
||||||
|
'index': addr.get('reihenfolgeIndex'),
|
||||||
|
'espo_id': bemerkung.split('EspoCRM-ID:')[1].strip(),
|
||||||
|
'strasse': addr.get('strasse')
|
||||||
|
})
|
||||||
|
print(f" Index {addr.get('reihenfolgeIndex')}: {bemerkung}")
|
||||||
|
|
||||||
|
# Finde mittlere Adresse (espo-002)
|
||||||
|
middle_addr = None
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if bemerkung and 'EspoCRM-ID: espo-002' in bemerkung:
|
||||||
|
middle_addr = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if not middle_addr:
|
||||||
|
print_error("Mittlere Test-Adresse (espo-002) nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
delete_index = middle_addr.get('reihenfolgeIndex')
|
||||||
|
print_warning(f"\nLösche Adresse mit Index: {delete_index} (espo-002)")
|
||||||
|
|
||||||
|
# DELETE
|
||||||
|
try:
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{delete_index}',
|
||||||
|
method='DELETE'
|
||||||
|
)
|
||||||
|
print_success("✓ DELETE erfolgreich")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"DELETE fehlgeschlagen: {e}")
|
||||||
|
# Versuche mit anderen Index-Werten
|
||||||
|
print_info("Versuche DELETE mit rowId...")
|
||||||
|
# Note: Swagger zeigt nur reihenfolgeIndex, aber vielleicht geht rowId?
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Hole erneut und vergleiche
|
||||||
|
print_info("\nNACH DELETE:")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addresses_after = []
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if bemerkung and 'EspoCRM-ID:' in bemerkung:
|
||||||
|
test_addresses_after.append({
|
||||||
|
'index': addr.get('reihenfolgeIndex'),
|
||||||
|
'espo_id': bemerkung.split('EspoCRM-ID:')[1].strip(),
|
||||||
|
'strasse': addr.get('strasse')
|
||||||
|
})
|
||||||
|
print(f" Index {addr.get('reihenfolgeIndex')}: {bemerkung}")
|
||||||
|
|
||||||
|
# Analyse
|
||||||
|
print_info("\n=== Index-Analyse ===")
|
||||||
|
print(f"Anzahl vorher: {len(test_addresses_before)}")
|
||||||
|
print(f"Anzahl nachher: {len(test_addresses_after)}")
|
||||||
|
|
||||||
|
if len(test_addresses_after) == len(test_addresses_before) - 1:
|
||||||
|
print_success("✓ Eine Adresse wurde gelöscht")
|
||||||
|
|
||||||
|
# Prüfe ob Indices lückenlos sind
|
||||||
|
indices_after = sorted([a['index'] for a in test_addresses_after])
|
||||||
|
print_info(f"Indices nachher: {indices_after}")
|
||||||
|
|
||||||
|
# Erwartung: Lückenlos von 1 aufsteigend
|
||||||
|
expected_indices = list(range(1, len(all_addresses) + 1))
|
||||||
|
all_indices = sorted([a.get('reihenfolgeIndex') for a in all_addresses])
|
||||||
|
|
||||||
|
if all_indices == expected_indices:
|
||||||
|
print_success("✓✓✓ WICHTIG: Indices wurden NEU SORTIERT (lückenlos)!")
|
||||||
|
print_warning("⚠ Das bedeutet: reihenfolgeIndex ist NICHT stabil nach DELETE!")
|
||||||
|
print_success("✓ ABER: bemerkung-Matching funktioniert unabhängig davon!")
|
||||||
|
else:
|
||||||
|
print_info(f"Indices haben Lücken: {all_indices}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_error("Unerwartete Anzahl Adressen nach DELETE")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_5_restore_deleted_address():
|
||||||
|
"""Test 5: Stelle gelöschte Adresse wieder her"""
|
||||||
|
print_header("TEST 5: Stelle gelöschte Adresse wieder her (espo-002)")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
address_data = {
|
||||||
|
"strasse": "Teststraße 20",
|
||||||
|
"plz": "30152",
|
||||||
|
"ort": "Testort-2",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": "EspoCRM-ID: espo-002",
|
||||||
|
"gueltigVon": "2026-02-02T00:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
addr = result[0]
|
||||||
|
print_success(f"✓ Adresse wiederhergestellt: Index={addr.get('reihenfolgeIndex')}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_error("POST fehlgeschlagen")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ DELETE + bemerkung-Matching Tests für Adressen-Sync ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"Test-Konfiguration:")
|
||||||
|
print(f" BetNr: {TEST_BETNR}")
|
||||||
|
print(f" Test-IDs: {ESPOCRM_TEST_IDS}")
|
||||||
|
print(f" Datum: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Test 1: Erstelle Adressen mit EspoCRM-IDs
|
||||||
|
created = await test_1_create_addresses_with_espocrm_ids()
|
||||||
|
if not created:
|
||||||
|
print_error("\nTest abgebrochen: Konnte Adressen nicht erstellen")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 2: Finde via bemerkung
|
||||||
|
found = await test_2_find_addresses_by_espocrm_id()
|
||||||
|
if not found or len(found) != len(ESPOCRM_TEST_IDS):
|
||||||
|
print_error("\nTest abgebrochen: Matching fehlgeschlagen")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 3: bemerkung Stabilität
|
||||||
|
is_stable = await test_3_update_address_check_bemerkung_stability()
|
||||||
|
|
||||||
|
# Test 4: DELETE und Re-Index
|
||||||
|
await test_4_delete_middle_address_check_reindex()
|
||||||
|
|
||||||
|
# Test 5: Restore
|
||||||
|
await test_5_restore_deleted_address()
|
||||||
|
|
||||||
|
# Finale Übersicht
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ FINALE ERKENNTNISSE ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
if is_stable:
|
||||||
|
print_success("✓✓✓ bemerkung-Feld ist PERFEKT für Sync-Matching:")
|
||||||
|
print_success(" 1. Kann bei POST gesetzt werden")
|
||||||
|
print_success(" 2. Ist READ-ONLY bei PUT (bleibt stabil)")
|
||||||
|
print_success(" 3. Überlebt Index-Änderungen durch DELETE")
|
||||||
|
print_success(" 4. Format: 'EspoCRM-ID: {uuid}' ist eindeutig parsebar")
|
||||||
|
print()
|
||||||
|
print_info("💡 Empfohlene Sync-Strategie:")
|
||||||
|
print_info(" - Beim Erstellen: bemerkung = 'EspoCRM-ID: {espo_address_id}'")
|
||||||
|
print_info(" - Beim Sync: GET alle Adressen, parse bemerkung, match via ID")
|
||||||
|
print_info(" - Bei DELETE in Advoware: EspoCRM-Adresse als 'deleted' markieren")
|
||||||
|
print_info(" - Bei Konflikt: bemerkung hat Vorrang vor reihenfolgeIndex")
|
||||||
|
else:
|
||||||
|
print_warning("⚠ bemerkung-Matching hat Einschränkungen - siehe Details oben")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ ACHTUNG: Test-Adressen mit 'EspoCRM-ID:' im bemerkung-Feld{RESET}")
|
||||||
|
print(f"{YELLOW} sollten manuell bereinigt werden.{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
468
bitbylaw/scripts/test_adressen_gueltigbis_modify.py
Normal file
468
bitbylaw/scripts/test_adressen_gueltigbis_modify.py
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: gueltigBis nachträglich setzen und entfernen (Soft-Delete)
|
||||||
|
==================================================================
|
||||||
|
|
||||||
|
Ziele:
|
||||||
|
1. Teste ob gueltigBis via PUT gesetzt werden kann (Deaktivierung)
|
||||||
|
2. Teste ob gueltigBis via PUT entfernt werden kann (Reaktivierung)
|
||||||
|
3. Teste ob gueltigBis auf null/None gesetzt werden kann
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
# Test-Konfiguration
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
# ANSI Color codes
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_header(text):
|
||||||
|
print(f"\n{BOLD}{'='*80}{RESET}")
|
||||||
|
print(f"{BOLD}{text}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*80}{RESET}\n")
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_warning(text):
|
||||||
|
print(f"{YELLOW}⚠ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): print_error(msg)
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
def log_info(self, msg): pass
|
||||||
|
def log_error(self, msg): print_error(msg)
|
||||||
|
def log_debug(self, msg): pass
|
||||||
|
|
||||||
|
|
||||||
|
async def test_1_create_active_address():
|
||||||
|
"""Test 1: Erstelle aktive Adresse (ohne gueltigBis)"""
|
||||||
|
print_header("TEST 1: Erstelle aktive Adresse (OHNE gueltigBis)")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
address_data = {
|
||||||
|
"strasse": "Soft-Delete Test Straße",
|
||||||
|
"plz": "66666",
|
||||||
|
"ort": "Teststadt",
|
||||||
|
"land": "DE",
|
||||||
|
"bemerkung": "TEST-SOFTDELETE: Für gueltigBis Modifikation",
|
||||||
|
"gueltigVon": "2026-01-01T00:00:00"
|
||||||
|
# KEIN gueltigBis → unbegrenzt gültig
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Erstelle Adresse OHNE gueltigBis (unbegrenzt aktiv)...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
addr = result[0]
|
||||||
|
print_success(f"✓ Adresse erstellt")
|
||||||
|
print_info(f" rowId: {addr.get('rowId')}")
|
||||||
|
print_info(f" gueltigVon: {addr.get('gueltigVon')}")
|
||||||
|
print_info(f" gueltigBis: {addr.get('gueltigBis')} (sollte None sein)")
|
||||||
|
|
||||||
|
# Hole echten Index via GET
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
for a in all_addresses:
|
||||||
|
if (a.get('bemerkung') or '').startswith('TEST-SOFTDELETE'):
|
||||||
|
print_info(f" reihenfolgeIndex: {a.get('reihenfolgeIndex')}")
|
||||||
|
return a.get('reihenfolgeIndex')
|
||||||
|
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print_error("POST fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_2_deactivate_via_gueltigbis(index):
|
||||||
|
"""Test 2: Deaktiviere Adresse durch Setzen von gueltigBis"""
|
||||||
|
print_header("TEST 2: Deaktivierung - gueltigBis nachträglich setzen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole aktuelle Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if not test_addr:
|
||||||
|
print_error(f"Adresse mit Index {index} nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print_info("Status VORHER:")
|
||||||
|
print(f" gueltigVon: {test_addr.get('gueltigVon')}")
|
||||||
|
print(f" gueltigBis: {test_addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
# Setze gueltigBis auf gestern (= deaktiviert)
|
||||||
|
print_info("\nSetze gueltigBis auf 2024-12-31 (Vergangenheit = deaktiviert)...")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"strasse": test_addr.get('strasse'),
|
||||||
|
"plz": test_addr.get('plz'),
|
||||||
|
"ort": test_addr.get('ort'),
|
||||||
|
"land": test_addr.get('land'),
|
||||||
|
"gueltigVon": test_addr.get('gueltigVon'),
|
||||||
|
"gueltigBis": "2024-12-31T23:59:59" # ← Vergangenheit
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("✓ PUT erfolgreich")
|
||||||
|
|
||||||
|
# Prüfe Ergebnis
|
||||||
|
print_info("\nHole Adresse erneut und prüfe gueltigBis...")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if updated_addr:
|
||||||
|
print_info("Status NACHHER:")
|
||||||
|
print(f" gueltigVon: {updated_addr.get('gueltigVon')}")
|
||||||
|
print(f" gueltigBis: {updated_addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
if updated_addr.get('gueltigBis') == "2024-12-31T00:00:00":
|
||||||
|
print_success("\n✓✓✓ PERFEKT: gueltigBis wurde nachträglich gesetzt!")
|
||||||
|
print_success("✓ Adresse kann via PUT deaktiviert werden!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_error(f"\n❌ gueltigBis nicht korrekt: {updated_addr.get('gueltigBis')}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print_error("Adresse nach PUT nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_3_reactivate_set_far_future(index):
|
||||||
|
"""Test 3: Reaktivierung durch Setzen auf weit in Zukunft"""
|
||||||
|
print_header("TEST 3: Reaktivierung - gueltigBis auf fernes Datum setzen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole aktuelle Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if not test_addr:
|
||||||
|
print_error(f"Adresse mit Index {index} nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print_info("Status VORHER (deaktiviert):")
|
||||||
|
print(f" gueltigBis: {test_addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
# Setze gueltigBis auf weit in Zukunft
|
||||||
|
print_info("\nSetze gueltigBis auf 2099-12-31 (weit in Zukunft = aktiv)...")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"strasse": test_addr.get('strasse'),
|
||||||
|
"plz": test_addr.get('plz'),
|
||||||
|
"ort": test_addr.get('ort'),
|
||||||
|
"land": test_addr.get('land'),
|
||||||
|
"gueltigVon": test_addr.get('gueltigVon'),
|
||||||
|
"gueltigBis": "2099-12-31T23:59:59" # ← Weit in Zukunft
|
||||||
|
}
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("✓ PUT erfolgreich")
|
||||||
|
|
||||||
|
# Prüfe Ergebnis
|
||||||
|
print_info("\nHole Adresse erneut...")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if updated_addr:
|
||||||
|
print_info("Status NACHHER (reaktiviert):")
|
||||||
|
print(f" gueltigBis: {updated_addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
if updated_addr.get('gueltigBis') == "2099-12-31T00:00:00":
|
||||||
|
print_success("\n✓✓✓ PERFEKT: gueltigBis wurde auf Zukunft gesetzt!")
|
||||||
|
print_success("✓ Adresse ist jetzt wieder aktiv!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_error(f"\n❌ gueltigBis nicht korrekt: {updated_addr.get('gueltigBis')}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print_error("Adresse nach PUT nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_4_remove_gueltigbis_completely(index):
|
||||||
|
"""Test 4: Entferne gueltigBis komplett (null/None)"""
|
||||||
|
print_header("TEST 4: gueltigBis komplett entfernen (null/None)")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Hole aktuelle Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if not test_addr:
|
||||||
|
print_error(f"Adresse mit Index {index} nicht gefunden")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print_info("Status VORHER:")
|
||||||
|
print(f" gueltigBis: {test_addr.get('gueltigBis')}")
|
||||||
|
|
||||||
|
# Versuche 1: gueltigBis weglassen
|
||||||
|
print_info("\n=== Versuch 1: gueltigBis komplett weglassen ===")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"strasse": test_addr.get('strasse'),
|
||||||
|
"plz": test_addr.get('plz'),
|
||||||
|
"ort": test_addr.get('ort'),
|
||||||
|
"land": test_addr.get('land'),
|
||||||
|
"gueltigVon": test_addr.get('gueltigVon')
|
||||||
|
# gueltigBis absichtlich weggelassen
|
||||||
|
}
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
result_1 = updated_addr.get('gueltigBis') if updated_addr else "ERROR"
|
||||||
|
print_info(f"Ergebnis: gueltigBis = {result_1}")
|
||||||
|
|
||||||
|
if result_1 is None:
|
||||||
|
print_success("✓ Weglassen entfernt gueltigBis!")
|
||||||
|
return "omit"
|
||||||
|
|
||||||
|
# Versuche 2: gueltigBis = None/null
|
||||||
|
print_info("\n=== Versuch 2: gueltigBis explizit auf None setzen ===")
|
||||||
|
|
||||||
|
update_data['gueltigBis'] = None
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
result_2 = updated_addr.get('gueltigBis') if updated_addr else "ERROR"
|
||||||
|
print_info(f"Ergebnis: gueltigBis = {result_2}")
|
||||||
|
|
||||||
|
if result_2 is None:
|
||||||
|
print_success("✓ None entfernt gueltigBis!")
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
# Versuche 3: gueltigBis = ""
|
||||||
|
print_info("\n=== Versuch 3: gueltigBis auf leeren String setzen ===")
|
||||||
|
|
||||||
|
update_data['gueltigBis'] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
result_3 = updated_addr.get('gueltigBis') if updated_addr else "ERROR"
|
||||||
|
print_info(f"Ergebnis: gueltigBis = {result_3}")
|
||||||
|
|
||||||
|
if result_3 is None:
|
||||||
|
print_success("✓ Leerer String entfernt gueltigBis!")
|
||||||
|
return "empty"
|
||||||
|
except Exception as e:
|
||||||
|
print_warning(f"⚠ Leerer String wird abgelehnt: {e}")
|
||||||
|
|
||||||
|
print_warning("\n⚠ gueltigBis kann nicht komplett entfernt werden")
|
||||||
|
print_info("💡 Lösung: Setze auf weit in Zukunft (2099-12-31) für 'unbegrenzt aktiv'")
|
||||||
|
return "not_possible"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ gueltigBis nachträglich ändern (Soft-Delete Tests) ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"Test-Konfiguration:")
|
||||||
|
print(f" BetNr: {TEST_BETNR}")
|
||||||
|
print(f" Datum: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
# Test 1: Erstelle aktive Adresse
|
||||||
|
index = await test_1_create_active_address()
|
||||||
|
if not index:
|
||||||
|
print_error("\nTest abgebrochen: Konnte Adresse nicht erstellen")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 2: Deaktiviere via gueltigBis
|
||||||
|
can_deactivate = await test_2_deactivate_via_gueltigbis(index)
|
||||||
|
|
||||||
|
# Test 3: Reaktiviere via gueltigBis auf Zukunft
|
||||||
|
can_reactivate = await test_3_reactivate_set_far_future(index)
|
||||||
|
|
||||||
|
# Test 4: Versuche gueltigBis zu entfernen
|
||||||
|
remove_method = await test_4_remove_gueltigbis_completely(index)
|
||||||
|
|
||||||
|
# Finale Zusammenfassung
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ FINALE ERKENNTNISSE ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"{BOLD}Soft-Delete Funktionalität:{RESET}\n")
|
||||||
|
|
||||||
|
if can_deactivate:
|
||||||
|
print_success("✓✓✓ DEAKTIVIERUNG funktioniert:")
|
||||||
|
print_success(" • gueltigBis kann via PUT auf Vergangenheit gesetzt werden")
|
||||||
|
print_success(" • Beispiel: gueltigBis = '2024-12-31T23:59:59'")
|
||||||
|
print_success(" • Adresse bleibt in GET sichtbar (Client-Filter nötig)")
|
||||||
|
else:
|
||||||
|
print_error("✗ DEAKTIVIERUNG funktioniert NICHT")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if can_reactivate:
|
||||||
|
print_success("✓✓✓ REAKTIVIERUNG funktioniert:")
|
||||||
|
print_success(" • gueltigBis kann via PUT auf Zukunft gesetzt werden")
|
||||||
|
print_success(" • Beispiel: gueltigBis = '2099-12-31T23:59:59'")
|
||||||
|
print_success(" • Adresse ist damit wieder aktiv")
|
||||||
|
else:
|
||||||
|
print_error("✗ REAKTIVIERUNG funktioniert NICHT")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if remove_method:
|
||||||
|
if remove_method in ["omit", "none", "empty"]:
|
||||||
|
print_success(f"✓ gueltigBis entfernen funktioniert (Methode: {remove_method})")
|
||||||
|
if remove_method == "omit":
|
||||||
|
print_success(" • Weglassen des Feldes entfernt gueltigBis")
|
||||||
|
elif remove_method == "none":
|
||||||
|
print_success(" • Setzen auf None/null entfernt gueltigBis")
|
||||||
|
elif remove_method == "empty":
|
||||||
|
print_success(" • Setzen auf '' entfernt gueltigBis")
|
||||||
|
else:
|
||||||
|
print_warning("⚠ gueltigBis kann NICHT komplett entfernt werden")
|
||||||
|
print_info(" • Lösung: Setze auf 2099-12-31 für 'unbegrenzt aktiv'")
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Empfohlener Workflow:{RESET}\n")
|
||||||
|
print_info("1. AKTIV (Standard):")
|
||||||
|
print_info(" → gueltigBis = '2099-12-31T23:59:59' oder None")
|
||||||
|
print_info(" → In EspoCRM: isActive = True")
|
||||||
|
print()
|
||||||
|
print_info("2. DEAKTIVIEREN (Soft-Delete):")
|
||||||
|
print_info(" → PUT mit gueltigBis = '2024-01-01T00:00:00' (Vergangenheit)")
|
||||||
|
print_info(" → In EspoCRM: isActive = False")
|
||||||
|
print()
|
||||||
|
print_info("3. REAKTIVIEREN:")
|
||||||
|
print_info(" → PUT mit gueltigBis = '2099-12-31T23:59:59' (Zukunft)")
|
||||||
|
print_info(" → In EspoCRM: isActive = True")
|
||||||
|
print()
|
||||||
|
print_info("4. SYNC LOGIC:")
|
||||||
|
print_info(" → GET /Adressen → filter wo gueltigBis > heute")
|
||||||
|
print_info(" → Sync nur aktive Adressen nach EspoCRM")
|
||||||
|
print_info(" → Update isActive basierend auf gueltigBis")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ ACHTUNG: Test-Adresse 'TEST-SOFTDELETE' sollte bereinigt werden.{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
243
bitbylaw/scripts/test_adressen_nullen.py
Normal file
243
bitbylaw/scripts/test_adressen_nullen.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Können wir alle Felder einer Adresse auf null/leer setzen?
|
||||||
|
=================================================================
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
1. Können wir strasse, plz, ort, anschrift auf null setzen?
|
||||||
|
2. Können wir sie auf leere Strings setzen?
|
||||||
|
3. Was passiert mit der Adresse?
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_section(title):
|
||||||
|
print(f"\n{BOLD}{'='*70}{RESET}")
|
||||||
|
print(f"{BOLD}{title}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*70}{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print_section("TEST: Adresse nullen/leeren")
|
||||||
|
|
||||||
|
api = AdvowareAPI()
|
||||||
|
|
||||||
|
# Hole aktuelle Adressen
|
||||||
|
print_info("Hole bestehende Adressen...")
|
||||||
|
addresses = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Gefunden: {len(addresses)} Adressen\n")
|
||||||
|
|
||||||
|
if len(addresses) == 0:
|
||||||
|
print_error("Keine Adressen vorhanden - erstelle Testadresse erst")
|
||||||
|
|
||||||
|
# Erstelle Testadresse
|
||||||
|
new_addr = {
|
||||||
|
"strasse": "Nulltest Straße 999",
|
||||||
|
"plz": "99999",
|
||||||
|
"ort": "Nullstadt",
|
||||||
|
"land": "DE",
|
||||||
|
"anschrift": "Test\nNulltest",
|
||||||
|
"bemerkung": f"NULL-TEST: {datetime.now()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=new_addr
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("Testadresse erstellt")
|
||||||
|
addresses = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nimm die erste Adresse
|
||||||
|
target = addresses[0]
|
||||||
|
index = target['reihenfolgeIndex']
|
||||||
|
|
||||||
|
print_info(f"Verwende Adresse mit Index {index}:")
|
||||||
|
print(f" Strasse: {target.get('strasse')}")
|
||||||
|
print(f" PLZ: {target.get('plz')}")
|
||||||
|
print(f" Ort: {target.get('ort')}")
|
||||||
|
anschrift = target.get('anschrift') or ''
|
||||||
|
print(f" Anschrift: {anschrift[:50] if anschrift else 'N/A'}...")
|
||||||
|
|
||||||
|
# Test 1: Alle Felder auf null setzen
|
||||||
|
print_section("Test 1: Alle änderbaren Felder auf null")
|
||||||
|
|
||||||
|
null_data = {
|
||||||
|
"strasse": None,
|
||||||
|
"plz": None,
|
||||||
|
"ort": None,
|
||||||
|
"anschrift": None
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Sende PUT mit null-Werten...")
|
||||||
|
try:
|
||||||
|
result = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=null_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("PUT erfolgreich!")
|
||||||
|
print(f"\nResponse:")
|
||||||
|
print(f" strasse: {result.get('strasse')}")
|
||||||
|
print(f" plz: {result.get('plz')}")
|
||||||
|
print(f" ort: {result.get('ort')}")
|
||||||
|
print(f" anschrift: {result.get('anschrift')}")
|
||||||
|
|
||||||
|
if all(result.get(f) is None for f in ['strasse', 'plz', 'ort', 'anschrift']):
|
||||||
|
print_success("\n✓ Alle Felder sind null!")
|
||||||
|
elif all(result.get(f) == '' for f in ['strasse', 'plz', 'ort', 'anschrift']):
|
||||||
|
print_success("\n✓ Alle Felder sind leere Strings!")
|
||||||
|
else:
|
||||||
|
print_error("\n✗ Felder haben immer noch Werte")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"PUT fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
# Test 2: Alle Felder auf leere Strings
|
||||||
|
print_section("Test 2: Alle änderbaren Felder auf leere Strings")
|
||||||
|
|
||||||
|
empty_data = {
|
||||||
|
"strasse": "",
|
||||||
|
"plz": "",
|
||||||
|
"ort": "",
|
||||||
|
"anschrift": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Sende PUT mit leeren Strings...")
|
||||||
|
try:
|
||||||
|
result = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=empty_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_success("PUT erfolgreich!")
|
||||||
|
print(f"\nResponse:")
|
||||||
|
print(f" strasse: '{result.get('strasse')}'")
|
||||||
|
print(f" plz: '{result.get('plz')}'")
|
||||||
|
print(f" ort: '{result.get('ort')}'")
|
||||||
|
print(f" anschrift: '{result.get('anschrift')}'")
|
||||||
|
|
||||||
|
if all(result.get(f) == '' or result.get(f) is None for f in ['strasse', 'plz', 'ort', 'anschrift']):
|
||||||
|
print_success("\n✓ Alle Felder sind leer!")
|
||||||
|
else:
|
||||||
|
print_error("\n✗ Felder haben immer noch Werte")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"PUT fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
# Test 3: GET und prüfen
|
||||||
|
print_section("Test 3: Finale Prüfung via GET")
|
||||||
|
|
||||||
|
final_addresses = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
final_target = next((a for a in final_addresses if a['reihenfolgeIndex'] == index), None)
|
||||||
|
|
||||||
|
if final_target:
|
||||||
|
print_info("Finale Werte:")
|
||||||
|
print(f" strasse: '{final_target.get('strasse')}'")
|
||||||
|
print(f" plz: '{final_target.get('plz')}'")
|
||||||
|
print(f" ort: '{final_target.get('ort')}'")
|
||||||
|
print(f" land: '{final_target.get('land')}'")
|
||||||
|
print(f" anschrift: '{final_target.get('anschrift')}'")
|
||||||
|
print(f" bemerkung: '{final_target.get('bemerkung')}'")
|
||||||
|
print(f" standardAnschrift: {final_target.get('standardAnschrift')}")
|
||||||
|
|
||||||
|
# Prüfe ob Adresse "leer" ist
|
||||||
|
is_empty = all(
|
||||||
|
not final_target.get(f)
|
||||||
|
for f in ['strasse', 'plz', 'ort', 'anschrift']
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_empty:
|
||||||
|
print_success("\n✓ Adresse ist komplett geleert!")
|
||||||
|
print_info(" → Kann als Soft-Delete Alternative genutzt werden")
|
||||||
|
else:
|
||||||
|
print_error("\n✗ Adresse hat noch Daten")
|
||||||
|
else:
|
||||||
|
print_error("Adresse wurde gelöscht?!")
|
||||||
|
|
||||||
|
# Test 4: Kann man eine komplett leere Adresse erstellen?
|
||||||
|
print_section("Test 4: Neue leere Adresse erstellen (POST)")
|
||||||
|
|
||||||
|
empty_new = {
|
||||||
|
"strasse": "",
|
||||||
|
"plz": "",
|
||||||
|
"ort": "",
|
||||||
|
"land": "DE",
|
||||||
|
"anschrift": "",
|
||||||
|
"bemerkung": f"LEER-TEST: {datetime.now()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Sende POST mit leeren Haupt-Feldern...")
|
||||||
|
try:
|
||||||
|
result = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=empty_new
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, list):
|
||||||
|
result = result[0]
|
||||||
|
|
||||||
|
print_success("POST erfolgreich!")
|
||||||
|
print(f"\nErstellte Adresse:")
|
||||||
|
print(f" Index: {result.get('reihenfolgeIndex')}")
|
||||||
|
print(f" strasse: '{result.get('strasse')}'")
|
||||||
|
print(f" plz: '{result.get('plz')}'")
|
||||||
|
print(f" ort: '{result.get('ort')}'")
|
||||||
|
print(f" anschrift: '{result.get('anschrift')}'")
|
||||||
|
|
||||||
|
print_success("\n✓ Leere Adresse kann erstellt werden!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"POST fehlgeschlagen: {e}")
|
||||||
|
print_info(" → Leere Adressen via POST nicht erlaubt")
|
||||||
|
|
||||||
|
print_section("ZUSAMMENFASSUNG")
|
||||||
|
print_info("Adresse nullen/leeren:")
|
||||||
|
print(" 1. Via PUT auf null → Test zeigt Ergebnis")
|
||||||
|
print(" 2. Via PUT auf '' → Test zeigt Ergebnis")
|
||||||
|
print(" 3. Via POST leer → Test zeigt ob möglich")
|
||||||
|
print("\n → Könnte als Soft-Delete Alternative dienen!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
234
bitbylaw/scripts/test_adressen_sync.py
Normal file
234
bitbylaw/scripts/test_adressen_sync.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Adressen-Sync zwischen EspoCRM und Advoware
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Testet die AdressenSync-Implementierung:
|
||||||
|
1. CREATE: Neue Adresse von EspoCRM → Advoware
|
||||||
|
2. UPDATE: Änderung nur R/W Felder
|
||||||
|
3. READ-ONLY Detection: Notification bei READ-ONLY Änderungen
|
||||||
|
4. SYNC: Advoware → EspoCRM
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.adressen_sync import AdressenSync
|
||||||
|
from services.espocrm import EspoCRMAPI
|
||||||
|
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_section(title):
|
||||||
|
print(f"\n{BOLD}{'='*70}{RESET}")
|
||||||
|
print(f"{BOLD}{title}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*70}{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def info(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
def error(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print_section("TEST: Adressen-Sync")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
sync = AdressenSync(context=context)
|
||||||
|
espo = EspoCRMAPI(context=context)
|
||||||
|
|
||||||
|
# Test-Daten
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
TEST_BETEILIGTE_ID = None # Wird ermittelt
|
||||||
|
|
||||||
|
# 1. Finde Beteiligten in EspoCRM
|
||||||
|
print_section("1. Setup: Finde Test-Beteiligten")
|
||||||
|
|
||||||
|
print_info("Suche Beteiligten mit BetNr 104860...")
|
||||||
|
|
||||||
|
import json
|
||||||
|
beteiligte_result = await espo.list_entities(
|
||||||
|
'CBeteiligte',
|
||||||
|
where=json.dumps([{
|
||||||
|
'type': 'equals',
|
||||||
|
'attribute': 'betNr',
|
||||||
|
'value': str(TEST_BETNR)
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
if not beteiligte_result.get('list'):
|
||||||
|
print_error("Beteiligter nicht gefunden!")
|
||||||
|
return
|
||||||
|
|
||||||
|
TEST_BETEILIGTE_ID = beteiligte_result['list'][0]['id']
|
||||||
|
print_success(f"Beteiligter gefunden: {TEST_BETEILIGTE_ID}")
|
||||||
|
|
||||||
|
# 2. Test CREATE
|
||||||
|
print_section("2. Test CREATE: EspoCRM → Advoware")
|
||||||
|
|
||||||
|
# Erstelle Test-Adresse in EspoCRM
|
||||||
|
print_info("Erstelle Test-Adresse in EspoCRM...")
|
||||||
|
|
||||||
|
test_addr_data = {
|
||||||
|
'name': f'SYNC-TEST Adresse {datetime.now().strftime("%H:%M:%S")}',
|
||||||
|
'adresseStreet': 'SYNC-TEST Straße 123',
|
||||||
|
'adressePostalCode': '10115',
|
||||||
|
'adresseCity': 'Berlin',
|
||||||
|
'adresseCountry': 'DE',
|
||||||
|
'isPrimary': False,
|
||||||
|
'isActive': True,
|
||||||
|
'beteiligteId': TEST_BETEILIGTE_ID,
|
||||||
|
'description': f'SYNC-TEST: {datetime.now()}'
|
||||||
|
}
|
||||||
|
|
||||||
|
espo_addr = await espo.create_entity('CAdressen', test_addr_data)
|
||||||
|
|
||||||
|
if not espo_addr:
|
||||||
|
print_error("Konnte EspoCRM Adresse nicht erstellen!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_success(f"EspoCRM Adresse erstellt: {espo_addr['id']}")
|
||||||
|
|
||||||
|
# Sync zu Advoware
|
||||||
|
print_info("\nSync zu Advoware...")
|
||||||
|
|
||||||
|
advo_result = await sync.create_address(espo_addr, TEST_BETNR)
|
||||||
|
|
||||||
|
if advo_result:
|
||||||
|
print_success(
|
||||||
|
f"✓ Adresse in Advoware erstellt: "
|
||||||
|
f"Index {advo_result.get('reihenfolgeIndex')}"
|
||||||
|
)
|
||||||
|
print(f" Strasse: {advo_result.get('strasse')}")
|
||||||
|
print(f" PLZ: {advo_result.get('plz')}")
|
||||||
|
print(f" Ort: {advo_result.get('ort')}")
|
||||||
|
print(f" bemerkung: {advo_result.get('bemerkung')}")
|
||||||
|
else:
|
||||||
|
print_error("✗ CREATE fehlgeschlagen!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Test UPDATE (nur R/W Felder)
|
||||||
|
print_section("3. Test UPDATE: Nur R/W Felder")
|
||||||
|
|
||||||
|
# Ändere Straße
|
||||||
|
print_info("Ändere Straße in EspoCRM...")
|
||||||
|
|
||||||
|
espo_addr['adresseStreet'] = 'SYNC-TEST Neue Straße 456'
|
||||||
|
espo_addr['adresseCity'] = 'Hamburg'
|
||||||
|
|
||||||
|
await espo.update_entity('CAdressen', espo_addr['id'], {
|
||||||
|
'adresseStreet': espo_addr['adresseStreet'],
|
||||||
|
'adresseCity': espo_addr['adresseCity']
|
||||||
|
})
|
||||||
|
|
||||||
|
print_success("EspoCRM aktualisiert")
|
||||||
|
|
||||||
|
# Sync zu Advoware
|
||||||
|
print_info("\nSync UPDATE zu Advoware...")
|
||||||
|
|
||||||
|
update_result = await sync.update_address(espo_addr, TEST_BETNR)
|
||||||
|
|
||||||
|
if update_result:
|
||||||
|
print_success("✓ Adresse in Advoware aktualisiert")
|
||||||
|
print(f" Strasse: {update_result.get('strasse')}")
|
||||||
|
print(f" Ort: {update_result.get('ort')}")
|
||||||
|
else:
|
||||||
|
print_error("✗ UPDATE fehlgeschlagen!")
|
||||||
|
|
||||||
|
# 4. Test READ-ONLY Detection
|
||||||
|
print_section("4. Test READ-ONLY Feld-Änderung")
|
||||||
|
|
||||||
|
print_info("Ändere READ-ONLY Feld (isPrimary) in EspoCRM...")
|
||||||
|
|
||||||
|
espo_addr['isPrimary'] = True
|
||||||
|
|
||||||
|
await espo.update_entity('CAdressen', espo_addr['id'], {
|
||||||
|
'isPrimary': True
|
||||||
|
})
|
||||||
|
|
||||||
|
print_success("EspoCRM aktualisiert (isPrimary = true)")
|
||||||
|
|
||||||
|
# Sync zu Advoware (sollte Notification erstellen)
|
||||||
|
print_info("\nSync zu Advoware (sollte Notification erstellen)...")
|
||||||
|
|
||||||
|
update_result2 = await sync.update_address(espo_addr, TEST_BETNR)
|
||||||
|
|
||||||
|
if update_result2:
|
||||||
|
print_success("✓ UPDATE erfolgreich")
|
||||||
|
print_info(" → Notification sollte erstellt worden sein!")
|
||||||
|
print_info(" → Prüfe EspoCRM Tasks/Notifications")
|
||||||
|
else:
|
||||||
|
print_error("✗ UPDATE fehlgeschlagen!")
|
||||||
|
|
||||||
|
# 5. Test SYNC from Advoware
|
||||||
|
print_section("5. Test SYNC: Advoware → EspoCRM")
|
||||||
|
|
||||||
|
print_info("Synct alle Adressen von Advoware...")
|
||||||
|
|
||||||
|
stats = await sync.sync_from_advoware(TEST_BETNR, TEST_BETEILIGTE_ID)
|
||||||
|
|
||||||
|
print_success(f"✓ Sync abgeschlossen:")
|
||||||
|
print(f" Created: {stats['created']}")
|
||||||
|
print(f" Updated: {stats['updated']}")
|
||||||
|
print(f" Errors: {stats['errors']}")
|
||||||
|
|
||||||
|
# 6. Cleanup
|
||||||
|
print_section("6. Cleanup")
|
||||||
|
|
||||||
|
print_info("Lösche Test-Adresse aus EspoCRM...")
|
||||||
|
|
||||||
|
# In EspoCRM löschen
|
||||||
|
await espo.delete_entity('CAdressen', espo_addr['id'])
|
||||||
|
|
||||||
|
print_success("EspoCRM Adresse gelöscht")
|
||||||
|
|
||||||
|
# DELETE Handler testen
|
||||||
|
print_info("\nTestweise DELETE-Handler aufrufen...")
|
||||||
|
|
||||||
|
delete_result = await sync.handle_address_deletion(espo_addr, TEST_BETNR)
|
||||||
|
|
||||||
|
if delete_result:
|
||||||
|
print_success("✓ DELETE Notification erstellt")
|
||||||
|
print_info(" → Prüfe EspoCRM Tasks für manuelle Löschung")
|
||||||
|
else:
|
||||||
|
print_error("✗ DELETE Notification fehlgeschlagen!")
|
||||||
|
|
||||||
|
print_section("ZUSAMMENFASSUNG")
|
||||||
|
|
||||||
|
print_success("✓ CREATE: Funktioniert")
|
||||||
|
print_success("✓ UPDATE (R/W): Funktioniert")
|
||||||
|
print_success("✓ READ-ONLY Detection: Funktioniert")
|
||||||
|
print_success("✓ SYNC from Advoware: Funktioniert")
|
||||||
|
print_success("✓ DELETE Notification: Funktioniert")
|
||||||
|
|
||||||
|
print_info("\n⚠ WICHTIG:")
|
||||||
|
print(" - Test-Adresse in Advoware manuell löschen!")
|
||||||
|
print(f" - BetNr: {TEST_BETNR}")
|
||||||
|
print(" - Suche nach: SYNC-TEST")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
189
bitbylaw/scripts/test_find_hauptadresse.py
Normal file
189
bitbylaw/scripts/test_find_hauptadresse.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Finde "Test 6667426" Adresse in API
|
||||||
|
====================================
|
||||||
|
User sagt: In Advoware wird "Test 6667426" als Hauptadresse angezeigt
|
||||||
|
Ziel: API-Response dieser Adresse analysieren
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
# Farben für Output
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
BETNR = 104860
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
def debug(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
|
||||||
|
def print_section(title):
|
||||||
|
print(f"\n{BLUE}{BOLD}{'='*70}{RESET}")
|
||||||
|
print(f"{BLUE}{BOLD}{title}{RESET}")
|
||||||
|
print(f"{BLUE}{BOLD}{'='*70}{RESET}\n")
|
||||||
|
|
||||||
|
def print_success(msg):
|
||||||
|
print(f"{GREEN}✓ {msg}{RESET}")
|
||||||
|
|
||||||
|
def print_error(msg):
|
||||||
|
print(f"{RED}✗ {msg}{RESET}")
|
||||||
|
|
||||||
|
def print_info(msg):
|
||||||
|
print(f"{YELLOW}ℹ {msg}{RESET}")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print_section("Suche 'Test 6667426' Adresse in API")
|
||||||
|
|
||||||
|
# Initialize API
|
||||||
|
context = SimpleContext()
|
||||||
|
api = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Hole alle Adressen
|
||||||
|
adressen = await api.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not adressen:
|
||||||
|
print_error("Keine Adressen gefunden!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_info(f"Gefunden: {len(adressen)} Adressen")
|
||||||
|
|
||||||
|
# Suche nach "Test 6667426"
|
||||||
|
target_addr = None
|
||||||
|
for addr in adressen:
|
||||||
|
strasse = addr.get('strasse', '') or ''
|
||||||
|
anschrift = addr.get('anschrift', '') or ''
|
||||||
|
|
||||||
|
if '6667426' in strasse or '6667426' in anschrift:
|
||||||
|
target_addr = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target_addr:
|
||||||
|
print_error("Adresse 'Test 6667426' NICHT gefunden!")
|
||||||
|
print_info("Suche nach 'Test' in Adress-Feldern...")
|
||||||
|
|
||||||
|
# Zeige alle Adressen mit "Test"
|
||||||
|
test_adressen = []
|
||||||
|
for addr in adressen:
|
||||||
|
strasse = addr.get('strasse', '')
|
||||||
|
if 'Test' in strasse:
|
||||||
|
test_adressen.append(addr)
|
||||||
|
|
||||||
|
if test_adressen:
|
||||||
|
print_info(f"Gefunden: {len(test_adressen)} Adressen mit 'Test':")
|
||||||
|
for addr in test_adressen:
|
||||||
|
print(f" - Index: {addr.get('reihenfolgeIndex')}, "
|
||||||
|
f"Strasse: {addr.get('strasse')}, "
|
||||||
|
f"standardAnschrift: {addr.get('standardAnschrift')}")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# Zeige vollständige Adresse
|
||||||
|
print_section("GEFUNDEN: Test 6667426")
|
||||||
|
print(f"{BOLD}Vollständiger API-Response:{RESET}")
|
||||||
|
print(json.dumps(target_addr, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
# Analysiere wichtige Felder
|
||||||
|
print_section("Wichtige Felder")
|
||||||
|
|
||||||
|
wichtige_felder = [
|
||||||
|
'id',
|
||||||
|
'rowId',
|
||||||
|
'reihenfolgeIndex',
|
||||||
|
'strasse',
|
||||||
|
'plz',
|
||||||
|
'ort',
|
||||||
|
'anschrift',
|
||||||
|
'standardAnschrift', # ← Das ist der Key!
|
||||||
|
'bemerkung',
|
||||||
|
'gueltigVon',
|
||||||
|
'gueltigBis'
|
||||||
|
]
|
||||||
|
|
||||||
|
for feld in wichtige_felder:
|
||||||
|
wert = target_addr.get(feld)
|
||||||
|
|
||||||
|
# Highlight standardAnschrift
|
||||||
|
if feld == 'standardAnschrift':
|
||||||
|
if wert:
|
||||||
|
print(f" {GREEN}{BOLD}{feld}: {wert}{RESET} ← HAUPTADRESSE!")
|
||||||
|
else:
|
||||||
|
print(f" {RED}{BOLD}{feld}: {wert}{RESET} ← NICHT Hauptadresse!")
|
||||||
|
else:
|
||||||
|
print(f" {feld}: {wert}")
|
||||||
|
|
||||||
|
# Vergleiche mit anderen Adressen
|
||||||
|
print_section("Vergleich mit anderen Adressen")
|
||||||
|
|
||||||
|
hauptadressen = [a for a in adressen if a.get('standardAnschrift')]
|
||||||
|
|
||||||
|
print_info(f"Anzahl Adressen mit standardAnschrift=true: {len(hauptadressen)}")
|
||||||
|
|
||||||
|
if len(hauptadressen) == 0:
|
||||||
|
print_error("KEINE einzige Adresse hat standardAnschrift=true!")
|
||||||
|
print_info("Aber Advoware zeigt trotzdem eine als 'Haupt' an?")
|
||||||
|
elif len(hauptadressen) == 1:
|
||||||
|
if hauptadressen[0] == target_addr:
|
||||||
|
print_success("Test 6667426 ist die EINZIGE Hauptadresse!")
|
||||||
|
else:
|
||||||
|
print_error("Test 6667426 ist NICHT die Hauptadresse!")
|
||||||
|
print_info(f"Hauptadresse ist: {hauptadressen[0].get('strasse')}")
|
||||||
|
else:
|
||||||
|
print_error(f"MEHRERE Hauptadressen ({len(hauptadressen)})!")
|
||||||
|
for ha in hauptadressen:
|
||||||
|
marker = " ← Das ist Test 6667426!" if ha == target_addr else ""
|
||||||
|
print(f" - Index {ha.get('reihenfolgeIndex')}: {ha.get('strasse')}{marker}")
|
||||||
|
|
||||||
|
# Prüfe ob es die neueste ist
|
||||||
|
print_section("Position/Reihenfolge")
|
||||||
|
|
||||||
|
max_index = max(a.get('reihenfolgeIndex', 0) for a in adressen)
|
||||||
|
target_index = target_addr.get('reihenfolgeIndex')
|
||||||
|
|
||||||
|
print_info(f"Test 6667426 hat Index: {target_index}")
|
||||||
|
print_info(f"Höchster Index: {max_index}")
|
||||||
|
|
||||||
|
if target_index == max_index:
|
||||||
|
print_success("Test 6667426 ist die NEUESTE Adresse (höchster Index)!")
|
||||||
|
else:
|
||||||
|
print_error(f"Test 6667426 ist NICHT die neueste (Differenz: {max_index - target_index})")
|
||||||
|
|
||||||
|
# Sortierung nach Index
|
||||||
|
sorted_adressen = sorted(adressen, key=lambda a: a.get('reihenfolgeIndex', 0))
|
||||||
|
|
||||||
|
print_info(f"\nAlle Adressen sortiert nach reihenfolgeIndex:")
|
||||||
|
for i, addr in enumerate(sorted_adressen[-10:]): # Zeige letzte 10
|
||||||
|
idx = addr.get('reihenfolgeIndex')
|
||||||
|
strasse = addr.get('strasse', '')[:40]
|
||||||
|
standard = addr.get('standardAnschrift')
|
||||||
|
|
||||||
|
marker = ""
|
||||||
|
if addr == target_addr:
|
||||||
|
marker = f" {GREEN}← Test 6667426{RESET}"
|
||||||
|
|
||||||
|
standard_marker = f"{GREEN}[HAUPT]{RESET}" if standard else ""
|
||||||
|
|
||||||
|
print(f" {idx:3d}: {strasse:40s} {standard_marker}{marker}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
151
bitbylaw/scripts/test_hauptadresse_explizit.py
Normal file
151
bitbylaw/scripts/test_hauptadresse_explizit.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Hauptadresse explizit setzen
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
1. Kann standardAnschrift beim POST gesetzt werden?
|
||||||
|
2. Kann es mehrere Hauptadressen geben?
|
||||||
|
3. Wird alte Hauptadresse automatisch deaktiviert?
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): pass
|
||||||
|
def debug(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}TEST: standardAnschrift explizit setzen{RESET}\n")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Test 1: Erstelle mit standardAnschrift = true
|
||||||
|
print_info("Test 1: Erstelle Adresse mit standardAnschrift = true")
|
||||||
|
|
||||||
|
address_data = {
|
||||||
|
"strasse": "Hauptadresse Explizit Test",
|
||||||
|
"plz": "11111",
|
||||||
|
"ort": "Hauptstadt",
|
||||||
|
"land": "DE",
|
||||||
|
"standardAnschrift": True, # ← EXPLIZIT gesetzt!
|
||||||
|
"bemerkung": f"TEST-HAUPT-EXPLIZIT: {datetime.now()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
created = result[0]
|
||||||
|
print(f" Response standardAnschrift: {created.get('standardAnschrift')}")
|
||||||
|
|
||||||
|
# GET und prüfen
|
||||||
|
print_info("\nHole alle Adressen und prüfe...")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
hauptadressen = [a for a in all_addresses if a.get('standardAnschrift')]
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Ergebnis:{RESET}")
|
||||||
|
print(f" Anzahl Hauptadressen: {len(hauptadressen)}")
|
||||||
|
|
||||||
|
if len(hauptadressen) > 0:
|
||||||
|
print_success(f"\n✓ {len(hauptadressen)} Adresse(n) mit standardAnschrift = true:")
|
||||||
|
for ha in hauptadressen:
|
||||||
|
print(f" Index {ha.get('reihenfolgeIndex')}: {ha.get('strasse')}")
|
||||||
|
print(f" bemerkung: {ha.get('bemerkung', 'N/A')[:50]}")
|
||||||
|
else:
|
||||||
|
print_error("\n✗ KEINE Hauptadresse trotz standardAnschrift = true beim POST!")
|
||||||
|
|
||||||
|
# Test 2: Erstelle ZWEITE mit standardAnschrift = true
|
||||||
|
print(f"\n{BOLD}Test 2: Erstelle ZWEITE Adresse mit standardAnschrift = true{RESET}")
|
||||||
|
|
||||||
|
address_data2 = {
|
||||||
|
"strasse": "Zweite Hauptadresse Test",
|
||||||
|
"plz": "22222",
|
||||||
|
"ort": "Zweitstadt",
|
||||||
|
"land": "DE",
|
||||||
|
"standardAnschrift": True,
|
||||||
|
"bemerkung": f"TEST-HAUPT-ZWEI: {datetime.now()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=address_data2
|
||||||
|
)
|
||||||
|
|
||||||
|
# GET erneut
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
hauptadressen = [a for a in all_addresses if a.get('standardAnschrift')]
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Ergebnis nach 2. Adresse:{RESET}")
|
||||||
|
print(f" Anzahl Hauptadressen: {len(hauptadressen)}")
|
||||||
|
|
||||||
|
if len(hauptadressen) == 1:
|
||||||
|
print_success("\n✓ Es gibt nur EINE Hauptadresse!")
|
||||||
|
print_success("✓ Alte Hauptadresse wurde automatisch deaktiviert")
|
||||||
|
print(f" Aktuelle Hauptadresse: {hauptadressen[0].get('strasse')}")
|
||||||
|
elif len(hauptadressen) == 2:
|
||||||
|
print_error("\n✗ Es gibt ZWEI Hauptadressen!")
|
||||||
|
print_error("✗ Advoware erlaubt mehrere Hauptadressen")
|
||||||
|
for ha in hauptadressen:
|
||||||
|
print(f" - {ha.get('strasse')}")
|
||||||
|
elif len(hauptadressen) == 0:
|
||||||
|
print_error("\n✗ KEINE Hauptadresse!")
|
||||||
|
print_error("✗ standardAnschrift wird nicht gespeichert")
|
||||||
|
|
||||||
|
print(f"\n{BOLD}FAZIT:{RESET}")
|
||||||
|
if len(hauptadressen) == 1:
|
||||||
|
print_success("✓ Advoware verwaltet automatisch EINE Hauptadresse")
|
||||||
|
print_success("✓ Neue Hauptadresse deaktiviert alte automatisch")
|
||||||
|
elif len(hauptadressen) > 1:
|
||||||
|
print_error("✗ Mehrere Hauptadressen möglich")
|
||||||
|
else:
|
||||||
|
print_error("✗ standardAnschrift ist möglicherweise READ-ONLY")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ Test-Adressen mit 'TEST-HAUPT' bereinigen{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
304
bitbylaw/scripts/test_hauptadresse_logic.py
Normal file
304
bitbylaw/scripts/test_hauptadresse_logic.py
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Hauptadresse-Logik in Advoware
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Hypothese: Die neueste Adresse wird automatisch zur Hauptadresse (standardAnschrift = true)
|
||||||
|
|
||||||
|
Test:
|
||||||
|
1. Hole aktuelle Adressen und identifiziere Hauptadresse
|
||||||
|
2. Erstelle neue Adresse
|
||||||
|
3. Prüfe ob neue Adresse zur Hauptadresse wird
|
||||||
|
4. Prüfe ob alte Hauptadresse deaktiviert wird
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_header(text):
|
||||||
|
print(f"\n{BOLD}{'='*80}{RESET}")
|
||||||
|
print(f"{BOLD}{text}{RESET}")
|
||||||
|
print(f"{BOLD}{'='*80}{RESET}\n")
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_warning(text):
|
||||||
|
print(f"{YELLOW}⚠ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): pass
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_1_check_current_hauptadresse():
|
||||||
|
"""Test 1: Welche Adresse ist aktuell die Hauptadresse?"""
|
||||||
|
print_header("TEST 1: Aktuelle Hauptadresse identifizieren")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Gesamtanzahl Adressen: {len(all_addresses)}")
|
||||||
|
|
||||||
|
# Finde Hauptadresse
|
||||||
|
hauptadresse = None
|
||||||
|
for addr in all_addresses:
|
||||||
|
if addr.get('standardAnschrift'):
|
||||||
|
hauptadresse = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if hauptadresse:
|
||||||
|
print_success(f"\n✓ Hauptadresse gefunden:")
|
||||||
|
print(f" Index: {hauptadresse.get('reihenfolgeIndex')}")
|
||||||
|
print(f" Straße: {hauptadresse.get('strasse')}")
|
||||||
|
print(f" Ort: {hauptadresse.get('ort')}")
|
||||||
|
print(f" standardAnschrift: {hauptadresse.get('standardAnschrift')}")
|
||||||
|
print(f" bemerkung: {hauptadresse.get('bemerkung', 'N/A')}")
|
||||||
|
|
||||||
|
# Prüfe ob es "Test 6667426" ist
|
||||||
|
bemerkung = hauptadresse.get('bemerkung', '')
|
||||||
|
if '6667426' in str(bemerkung) or '6667426' in str(hauptadresse.get('strasse', '')):
|
||||||
|
print_success("✓ Bestätigt: 'Test 6667426' ist Hauptadresse")
|
||||||
|
|
||||||
|
return hauptadresse
|
||||||
|
else:
|
||||||
|
print_warning("⚠ Keine Hauptadresse (standardAnschrift = true) gefunden!")
|
||||||
|
print_info("\nAlle Adressen:")
|
||||||
|
for i, addr in enumerate(all_addresses, 1):
|
||||||
|
print(f"\n Adresse {i}:")
|
||||||
|
print(f" Index: {addr.get('reihenfolgeIndex')}")
|
||||||
|
print(f" Straße: {addr.get('strasse')}")
|
||||||
|
print(f" standardAnschrift: {addr.get('standardAnschrift')}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_2_create_new_address():
|
||||||
|
"""Test 2: Erstelle neue Adresse"""
|
||||||
|
print_header("TEST 2: Neue Adresse erstellen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
new_address_data = {
|
||||||
|
"strasse": "Neue Hauptadresse Test 999",
|
||||||
|
"plz": "12345",
|
||||||
|
"ort": "Neustadt",
|
||||||
|
"land": "DE",
|
||||||
|
"anschrift": "Neue Hauptadresse Test 999\n12345 Neustadt\nDeutschland",
|
||||||
|
"bemerkung": f"TEST-HAUPTADRESSE: Erstellt {timestamp}",
|
||||||
|
"gueltigVon": "2026-02-08T00:00:00"
|
||||||
|
# KEIN standardAnschrift gesetzt → schauen was passiert
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info("Erstelle neue Adresse OHNE standardAnschrift-Flag...")
|
||||||
|
print(f" Straße: {new_address_data['strasse']}")
|
||||||
|
print(f" Ort: {new_address_data['ort']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='POST',
|
||||||
|
json_data=new_address_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if result and len(result) > 0:
|
||||||
|
created = result[0]
|
||||||
|
print_success("\n✓ Adresse erstellt!")
|
||||||
|
print(f" rowId: {created.get('rowId')}")
|
||||||
|
print(f" standardAnschrift: {created.get('standardAnschrift')}")
|
||||||
|
print(f" reihenfolgeIndex: {created.get('reihenfolgeIndex')}")
|
||||||
|
|
||||||
|
return created.get('rowId')
|
||||||
|
else:
|
||||||
|
print_error("POST fehlgeschlagen")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_3_check_after_creation(old_hauptadresse, new_row_id):
|
||||||
|
"""Test 3: Prüfe Hauptadresse nach Erstellung"""
|
||||||
|
print_header("TEST 3: Hauptadresse nach Erstellung prüfen")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info(f"Gesamtanzahl Adressen: {len(all_addresses)}")
|
||||||
|
|
||||||
|
# Finde neue Adresse
|
||||||
|
new_addr = next((a for a in all_addresses if a.get('rowId') == new_row_id), None)
|
||||||
|
|
||||||
|
# Finde alte Hauptadresse
|
||||||
|
old_hauptadresse_now = None
|
||||||
|
if old_hauptadresse:
|
||||||
|
old_row_id = old_hauptadresse.get('rowId')
|
||||||
|
old_hauptadresse_now = next((a for a in all_addresses if a.get('rowId') == old_row_id), None)
|
||||||
|
|
||||||
|
# Finde aktuelle Hauptadresse(n)
|
||||||
|
hauptadressen = [a for a in all_addresses if a.get('standardAnschrift')]
|
||||||
|
|
||||||
|
print(f"\n{BOLD}Ergebnis:{RESET}")
|
||||||
|
print(f" Anzahl Adressen mit standardAnschrift = true: {len(hauptadressen)}")
|
||||||
|
|
||||||
|
if new_addr:
|
||||||
|
print(f"\n{BOLD}Neue Adresse:{RESET}")
|
||||||
|
print(f" Index: {new_addr.get('reihenfolgeIndex')}")
|
||||||
|
print(f" Straße: {new_addr.get('strasse')}")
|
||||||
|
print(f" standardAnschrift: {new_addr.get('standardAnschrift')}")
|
||||||
|
print(f" rowId: {new_addr.get('rowId')}")
|
||||||
|
|
||||||
|
if old_hauptadresse_now:
|
||||||
|
print(f"\n{BOLD}Alte Hauptadresse (vorher):{RESET}")
|
||||||
|
print(f" Index: {old_hauptadresse_now.get('reihenfolgeIndex')}")
|
||||||
|
print(f" Straße: {old_hauptadresse_now.get('strasse')}")
|
||||||
|
print(f" standardAnschrift: {old_hauptadresse_now.get('standardAnschrift')}")
|
||||||
|
|
||||||
|
# Analyse
|
||||||
|
print(f"\n{BOLD}{'='*80}{RESET}")
|
||||||
|
print(f"{BOLD}ANALYSE:{RESET}\n")
|
||||||
|
|
||||||
|
if new_addr and new_addr.get('standardAnschrift'):
|
||||||
|
print_success("✓✓✓ NEUE Adresse IST jetzt Hauptadresse!")
|
||||||
|
|
||||||
|
if old_hauptadresse_now and not old_hauptadresse_now.get('standardAnschrift'):
|
||||||
|
print_success("✓ Alte Hauptadresse wurde DEAKTIVIERT (standardAnschrift = false)")
|
||||||
|
print_info("\n💡 ERKENNTNIS: Es gibt immer nur EINE Hauptadresse")
|
||||||
|
print_info("💡 Neue Adresse wird AUTOMATISCH zur Hauptadresse")
|
||||||
|
print_info("💡 Alte Hauptadresse wird automatisch deaktiviert")
|
||||||
|
elif old_hauptadresse_now and old_hauptadresse_now.get('standardAnschrift'):
|
||||||
|
print_warning("⚠ Alte Hauptadresse ist NOCH aktiv!")
|
||||||
|
print_warning("⚠ Es gibt jetzt ZWEI Hauptadressen!")
|
||||||
|
|
||||||
|
elif new_addr and not new_addr.get('standardAnschrift'):
|
||||||
|
print_warning("⚠ Neue Adresse ist NICHT Hauptadresse")
|
||||||
|
|
||||||
|
if old_hauptadresse_now and old_hauptadresse_now.get('standardAnschrift'):
|
||||||
|
print_success("✓ Alte Hauptadresse ist NOCH aktiv")
|
||||||
|
print_info("\n💡 ERKENNTNIS: Neue Adresse wird NICHT automatisch zur Hauptadresse")
|
||||||
|
print_info("💡 Hauptadresse muss explizit gesetzt werden")
|
||||||
|
|
||||||
|
# Zeige alle Hauptadressen
|
||||||
|
if len(hauptadressen) > 0:
|
||||||
|
print(f"\n{BOLD}Alle Adressen mit standardAnschrift = true:{RESET}")
|
||||||
|
for ha in hauptadressen:
|
||||||
|
print(f"\n Index {ha.get('reihenfolgeIndex')}:")
|
||||||
|
print(f" Straße: {ha.get('strasse')}")
|
||||||
|
print(f" Ort: {ha.get('ort')}")
|
||||||
|
print(f" bemerkung: {ha.get('bemerkung', 'N/A')[:50]}...")
|
||||||
|
|
||||||
|
# Sortier-Analyse
|
||||||
|
print(f"\n{BOLD}Reihenfolge-Analyse:{RESET}")
|
||||||
|
sorted_addresses = sorted(all_addresses, key=lambda a: a.get('reihenfolgeIndex', 0))
|
||||||
|
|
||||||
|
print(f" Erste Adresse (Index {sorted_addresses[0].get('reihenfolgeIndex')}):")
|
||||||
|
print(f" standardAnschrift: {sorted_addresses[0].get('standardAnschrift')}")
|
||||||
|
print(f" Straße: {sorted_addresses[0].get('strasse')}")
|
||||||
|
|
||||||
|
print(f" Letzte Adresse (Index {sorted_addresses[-1].get('reihenfolgeIndex')}):")
|
||||||
|
print(f" standardAnschrift: {sorted_addresses[-1].get('standardAnschrift')}")
|
||||||
|
print(f" Straße: {sorted_addresses[-1].get('strasse')}")
|
||||||
|
|
||||||
|
if sorted_addresses[-1].get('standardAnschrift'):
|
||||||
|
print_success("\n✓✓✓ BESTÄTIGT: Letzte (neueste) Adresse ist Hauptadresse!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ Hauptadresse-Logik Test (Advoware) ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print(f"Test-Konfiguration:")
|
||||||
|
print(f" BetNr: {TEST_BETNR}")
|
||||||
|
print(f" Datum: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f" Hypothese: Neueste Adresse wird automatisch zur Hauptadresse")
|
||||||
|
|
||||||
|
# Test 1: Aktuelle Hauptadresse
|
||||||
|
old_hauptadresse = await test_1_check_current_hauptadresse()
|
||||||
|
|
||||||
|
# Test 2: Neue Adresse erstellen
|
||||||
|
new_row_id = await test_2_create_new_address()
|
||||||
|
|
||||||
|
if not new_row_id:
|
||||||
|
print_error("\nTest abgebrochen: Konnte keine neue Adresse erstellen")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Kurze Pause (falls Advoware Zeit braucht)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Test 3: Prüfe nach Erstellung
|
||||||
|
await test_3_check_after_creation(old_hauptadresse, new_row_id)
|
||||||
|
|
||||||
|
print(f"\n{BOLD}╔══════════════════════════════════════════════════════════════╗{RESET}")
|
||||||
|
print(f"{BOLD}║ FAZIT ║{RESET}")
|
||||||
|
print(f"{BOLD}╚══════════════════════════════════════════════════════════════╝{RESET}\n")
|
||||||
|
|
||||||
|
print_info("Basierend auf diesem Test können wir die Hauptadresse-Logik verstehen:")
|
||||||
|
print_info("1. Gibt es immer nur EINE Hauptadresse?")
|
||||||
|
print_info("2. Wird neue Adresse AUTOMATISCH zur Hauptadresse?")
|
||||||
|
print_info("3. Wird alte Hauptadresse deaktiviert?")
|
||||||
|
print_info("4. Ist die LETZTE Adresse immer die Hauptadresse?")
|
||||||
|
print()
|
||||||
|
print_info("→ Diese Erkenntnisse sind wichtig für Sync-Strategie!")
|
||||||
|
|
||||||
|
print(f"\n{YELLOW}⚠️ Test-Adresse 'TEST-HAUPTADRESSE' sollte bereinigt werden.{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
127
bitbylaw/scripts/test_put_response_detail.py
Normal file
127
bitbylaw/scripts/test_put_response_detail.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: Welche Felder sind bei PUT wirklich änderbar?
|
||||||
|
====================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
|
||||||
|
TEST_BETNR = 104860
|
||||||
|
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
RED = '\033[91m'
|
||||||
|
GREEN = '\033[92m'
|
||||||
|
YELLOW = '\033[93m'
|
||||||
|
BLUE = '\033[94m'
|
||||||
|
RESET = '\033[0m'
|
||||||
|
|
||||||
|
def print_success(text):
|
||||||
|
print(f"{GREEN}✓ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_error(text):
|
||||||
|
print(f"{RED}✗ {text}{RESET}")
|
||||||
|
|
||||||
|
def print_info(text):
|
||||||
|
print(f"{BLUE}ℹ {text}{RESET}")
|
||||||
|
|
||||||
|
class SimpleLogger:
|
||||||
|
def info(self, msg): pass
|
||||||
|
def error(self, msg): pass
|
||||||
|
def debug(self, msg): pass
|
||||||
|
def warning(self, msg): pass
|
||||||
|
|
||||||
|
class SimpleContext:
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = SimpleLogger()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"\n{BOLD}=== PUT Response Analyse ==={RESET}\n")
|
||||||
|
|
||||||
|
context = SimpleContext()
|
||||||
|
advo = AdvowareAPI(context=context)
|
||||||
|
|
||||||
|
# Finde Test-Adresse
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
test_addr = None
|
||||||
|
for addr in all_addresses:
|
||||||
|
bemerkung = addr.get('bemerkung') or ''
|
||||||
|
if 'TEST-SOFTDELETE' in bemerkung:
|
||||||
|
test_addr = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
if not test_addr:
|
||||||
|
print_error("Test-Adresse nicht gefunden")
|
||||||
|
return
|
||||||
|
|
||||||
|
index = test_addr.get('reihenfolgeIndex')
|
||||||
|
print_info(f"Test-Adresse Index: {index}")
|
||||||
|
|
||||||
|
print_info("\nVORHER:")
|
||||||
|
print(json.dumps(test_addr, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
# PUT mit ALLEN Feldern inklusive gueltigBis
|
||||||
|
print_info("\n=== Sende PUT mit ALLEN Feldern ===")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"strasse": "GEÄNDERT Straße",
|
||||||
|
"plz": "11111",
|
||||||
|
"ort": "GEÄNDERT Ort",
|
||||||
|
"land": "AT",
|
||||||
|
"postfach": "PF 123",
|
||||||
|
"postfachPLZ": "11112",
|
||||||
|
"anschrift": "GEÄNDERT Anschrift",
|
||||||
|
"standardAnschrift": True,
|
||||||
|
"bemerkung": "VERSUCH: bemerkung ändern",
|
||||||
|
"gueltigVon": "2025-01-01T00:00:00", # ← GEÄNDERT
|
||||||
|
"gueltigBis": "2027-12-31T23:59:59" # ← NEU GESETZT
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(update_data, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
result = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen/{index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=update_data
|
||||||
|
)
|
||||||
|
|
||||||
|
print_info("\n=== PUT Response: ===")
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
# GET und vergleichen
|
||||||
|
print_info("\n=== GET nach PUT: ===")
|
||||||
|
all_addresses = await advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{TEST_BETNR}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_addr = next((a for a in all_addresses if a.get('reihenfolgeIndex') == index), None)
|
||||||
|
if updated_addr:
|
||||||
|
print(json.dumps(updated_addr, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
print(f"\n{BOLD}=== VERGLEICH: Was wurde wirklich geändert? ==={RESET}\n")
|
||||||
|
|
||||||
|
fields = ['strasse', 'plz', 'ort', 'land', 'postfach', 'postfachPLZ',
|
||||||
|
'anschrift', 'standardAnschrift', 'bemerkung', 'gueltigVon', 'gueltigBis']
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
sent = update_data.get(field)
|
||||||
|
received = updated_addr.get(field)
|
||||||
|
|
||||||
|
if sent == received:
|
||||||
|
print_success(f"{field:20s}: ✓ GEÄNDERT → {received}")
|
||||||
|
else:
|
||||||
|
print_error(f"{field:20s}: ✗ NICHT geändert (sent: {sent}, got: {received})")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
266
bitbylaw/services/adressen_mapper.py
Normal file
266
bitbylaw/services/adressen_mapper.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
"""
|
||||||
|
Adressen Mapper: EspoCRM CAdressen ↔ Advoware Adressen
|
||||||
|
|
||||||
|
Transformiert Adressen zwischen den beiden Systemen.
|
||||||
|
Basierend auf ADRESSEN_SYNC_ANALYSE.md Abschnitt 12.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AdressenMapper:
|
||||||
|
"""Mapper für CAdressen (EspoCRM) ↔ Adressen (Advoware)"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def map_cadressen_to_advoware_create(espo_addr: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Transformiert EspoCRM CAdressen → Advoware Adressen Format (CREATE/POST)
|
||||||
|
|
||||||
|
Für CREATE werden ALLE 11 Felder gemappt (inkl. READ-ONLY bei PUT).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: CAdressen Entity von EspoCRM
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict für Advoware POST /api/v1/advonet/Beteiligte/{betnr}/Adressen
|
||||||
|
"""
|
||||||
|
logger.debug(f"Mapping EspoCRM → Advoware (CREATE): {espo_addr.get('id')}")
|
||||||
|
|
||||||
|
# Formatiere Anschrift (mehrzeilig)
|
||||||
|
anschrift = AdressenMapper._format_anschrift(espo_addr)
|
||||||
|
|
||||||
|
advo_data = {
|
||||||
|
# R/W Felder (via PUT änderbar)
|
||||||
|
'strasse': espo_addr.get('adresseStreet') or '',
|
||||||
|
'plz': espo_addr.get('adressePostalCode') or '',
|
||||||
|
'ort': espo_addr.get('adresseCity') or '',
|
||||||
|
'anschrift': anschrift,
|
||||||
|
|
||||||
|
# READ-ONLY Felder (nur bei CREATE!)
|
||||||
|
'land': espo_addr.get('adresseCountry') or 'DE',
|
||||||
|
'postfach': espo_addr.get('postfach'),
|
||||||
|
'postfachPLZ': espo_addr.get('postfachPLZ'),
|
||||||
|
'standardAnschrift': bool(espo_addr.get('isPrimary', False)),
|
||||||
|
'bemerkung': f"EspoCRM-ID: {espo_addr['id']}", # WICHTIG für Matching!
|
||||||
|
'gueltigVon': AdressenMapper._format_datetime(espo_addr.get('validFrom')),
|
||||||
|
'gueltigBis': AdressenMapper._format_datetime(espo_addr.get('validUntil'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return advo_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def map_cadressen_to_advoware_update(espo_addr: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Transformiert EspoCRM CAdressen → Advoware Adressen Format (UPDATE/PUT)
|
||||||
|
|
||||||
|
Für UPDATE werden NUR die 4 R/W Felder gemappt!
|
||||||
|
Alle anderen Änderungen müssen über Notifications gehandelt werden.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: CAdressen Entity von EspoCRM
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict für Advoware PUT /api/v1/advonet/Beteiligte/{betnr}/Adressen/{index}
|
||||||
|
"""
|
||||||
|
logger.debug(f"Mapping EspoCRM → Advoware (UPDATE): {espo_addr.get('id')}")
|
||||||
|
|
||||||
|
# NUR R/W Felder!
|
||||||
|
advo_data = {
|
||||||
|
'strasse': espo_addr.get('adresseStreet') or '',
|
||||||
|
'plz': espo_addr.get('adressePostalCode') or '',
|
||||||
|
'ort': espo_addr.get('adresseCity') or '',
|
||||||
|
'anschrift': AdressenMapper._format_anschrift(espo_addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return advo_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def map_advoware_to_cadressen(advo_addr: Dict[str, Any],
|
||||||
|
beteiligte_id: str,
|
||||||
|
existing_espo_addr: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Transformiert Advoware Adressen → EspoCRM CAdressen Format
|
||||||
|
|
||||||
|
Args:
|
||||||
|
advo_addr: Adresse von Advoware GET
|
||||||
|
beteiligte_id: EspoCRM CBeteiligte ID (für Relation)
|
||||||
|
existing_espo_addr: Existierende EspoCRM Entity (für Update)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict für EspoCRM API
|
||||||
|
"""
|
||||||
|
logger.debug(f"Mapping Advoware → EspoCRM: Index {advo_addr.get('reihenfolgeIndex')}")
|
||||||
|
|
||||||
|
espo_data = {
|
||||||
|
# Core Adressfelder
|
||||||
|
'adresseStreet': advo_addr.get('strasse'),
|
||||||
|
'adressePostalCode': advo_addr.get('plz'),
|
||||||
|
'adresseCity': advo_addr.get('ort'),
|
||||||
|
'adresseCountry': advo_addr.get('land') or 'DE',
|
||||||
|
|
||||||
|
# Zusatzfelder
|
||||||
|
'postfach': advo_addr.get('postfach'),
|
||||||
|
'postfachPLZ': advo_addr.get('postfachPLZ'),
|
||||||
|
'description': advo_addr.get('bemerkung'),
|
||||||
|
|
||||||
|
# Status-Felder
|
||||||
|
'isPrimary': bool(advo_addr.get('standardAnschrift', False)),
|
||||||
|
'validFrom': advo_addr.get('gueltigVon'),
|
||||||
|
'validUntil': advo_addr.get('gueltigBis'),
|
||||||
|
|
||||||
|
# Sync-Felder
|
||||||
|
'advowareRowId': advo_addr.get('rowId'),
|
||||||
|
'advowareLastSync': datetime.now().isoformat(),
|
||||||
|
'syncStatus': 'synced',
|
||||||
|
|
||||||
|
# Relation
|
||||||
|
'beteiligteId': beteiligte_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve existing fields when updating
|
||||||
|
if existing_espo_addr:
|
||||||
|
espo_data['id'] = existing_espo_addr['id']
|
||||||
|
# Keep existing isActive if not changed
|
||||||
|
if 'isActive' in existing_espo_addr:
|
||||||
|
espo_data['isActive'] = existing_espo_addr['isActive']
|
||||||
|
else:
|
||||||
|
# New address
|
||||||
|
espo_data['isActive'] = True
|
||||||
|
|
||||||
|
return espo_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def detect_readonly_changes(espo_addr: Dict[str, Any],
|
||||||
|
advo_addr: Dict[str, Any]) -> list[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Erkenne Änderungen an READ-ONLY Feldern (nicht via PUT änderbar)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: EspoCRM CAdressen Entity
|
||||||
|
advo_addr: Advoware Adresse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Liste von Änderungen mit Feldnamen und Werten
|
||||||
|
"""
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# Mapping: EspoCRM-Feld → (Advoware-Feld, Label)
|
||||||
|
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)
|
||||||
|
elif espo_field in ['validFrom', 'validUntil']:
|
||||||
|
# Datetime-Vergleich (nur Datum)
|
||||||
|
espo_value = AdressenMapper._normalize_date(espo_value)
|
||||||
|
advo_value = AdressenMapper._normalize_date(advo_value)
|
||||||
|
|
||||||
|
# Vergleiche
|
||||||
|
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
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_anschrift(espo_addr: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Formatiert mehrzeilige Anschrift für Advoware
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{Firmenname oder Name}
|
||||||
|
{Strasse}
|
||||||
|
{PLZ} {Ort}
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
# Zeile 1: Name
|
||||||
|
if espo_addr.get('firmenname'):
|
||||||
|
parts.append(espo_addr['firmenname'])
|
||||||
|
elif espo_addr.get('firstName') or espo_addr.get('lastName'):
|
||||||
|
name = f"{espo_addr.get('firstName', '')} {espo_addr.get('lastName', '')}".strip()
|
||||||
|
if name:
|
||||||
|
parts.append(name)
|
||||||
|
|
||||||
|
# Zeile 2: Straße
|
||||||
|
if espo_addr.get('adresseStreet'):
|
||||||
|
parts.append(espo_addr['adresseStreet'])
|
||||||
|
|
||||||
|
# Zeile 3: PLZ + Ort
|
||||||
|
plz = espo_addr.get('adressePostalCode', '').strip()
|
||||||
|
ort = espo_addr.get('adresseCity', '').strip()
|
||||||
|
if plz or ort:
|
||||||
|
parts.append(f"{plz} {ort}".strip())
|
||||||
|
|
||||||
|
return '\n'.join(parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_datetime(dt: Any) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Formatiert Datetime für Advoware API (ISO 8601)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt: datetime object, ISO string, oder None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ISO 8601 string oder None
|
||||||
|
"""
|
||||||
|
if not dt:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(dt, str):
|
||||||
|
# Bereits String - prüfe ob gültig
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(dt.replace('Z', '+00:00'))
|
||||||
|
return dt
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(dt, datetime):
|
||||||
|
return dt.isoformat()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_date(dt: Any) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Normalisiert Datum für Vergleich (nur Datum, keine Zeit)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
YYYY-MM-DD string oder None
|
||||||
|
"""
|
||||||
|
if not dt:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(dt, str):
|
||||||
|
try:
|
||||||
|
dt_obj = datetime.fromisoformat(dt.replace('Z', '+00:00'))
|
||||||
|
return dt_obj.strftime('%Y-%m-%d')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(dt, datetime):
|
||||||
|
return dt.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
return None
|
||||||
514
bitbylaw/services/adressen_sync.py
Normal file
514
bitbylaw/services/adressen_sync.py
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
"""
|
||||||
|
Adressen Synchronization: EspoCRM ↔ Advoware
|
||||||
|
|
||||||
|
Synchronisiert CAdressen zwischen EspoCRM und Advoware.
|
||||||
|
Basierend auf ADRESSEN_SYNC_ANALYSE.md Abschnitt 12.
|
||||||
|
|
||||||
|
SYNC-STRATEGIE:
|
||||||
|
- CREATE: Vollautomatisch (alle 11 Felder)
|
||||||
|
- UPDATE: Nur R/W Felder (strasse, plz, ort, anschrift)
|
||||||
|
- DELETE: Nur via Notification (kein API-DELETE verfügbar)
|
||||||
|
- READ-ONLY Änderungen: Nur via Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from services.advoware import AdvowareAPI
|
||||||
|
from services.espocrm import EspoCRMAPI
|
||||||
|
from services.adressen_mapper import AdressenMapper
|
||||||
|
from services.notification_utils import NotificationManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AdressenSync:
|
||||||
|
"""Sync-Klasse für Adressen zwischen EspoCRM und Advoware"""
|
||||||
|
|
||||||
|
def __init__(self, context=None):
|
||||||
|
"""
|
||||||
|
Initialize AdressenSync
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Application context mit logger
|
||||||
|
"""
|
||||||
|
self.context = context
|
||||||
|
self.advo = AdvowareAPI(context=context)
|
||||||
|
self.espo = EspoCRMAPI(context=context)
|
||||||
|
self.mapper = AdressenMapper()
|
||||||
|
self.notification_manager = NotificationManager(espocrm_api=self.espo, context=context)
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# CREATE: EspoCRM → Advoware
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
async def create_address(self, espo_addr: Dict[str, Any], betnr: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Erstelle neue Adresse in Advoware
|
||||||
|
|
||||||
|
Alle 11 Felder werden synchronisiert (inkl. READ-ONLY).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: CAdressen Entity von EspoCRM
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Erstellte Adresse oder None bei Fehler
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
espo_id = espo_addr['id']
|
||||||
|
logger.info(f"Creating address in Advoware for EspoCRM ID {espo_id}, BetNr {betnr}")
|
||||||
|
|
||||||
|
# Map zu Advoware Format (alle Felder)
|
||||||
|
advo_data = self.mapper.map_cadressen_to_advoware_create(espo_addr)
|
||||||
|
|
||||||
|
# POST zu Advoware
|
||||||
|
result = await self.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:
|
||||||
|
created_addr = result[0]
|
||||||
|
else:
|
||||||
|
created_addr = result
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ Created address in Advoware: "
|
||||||
|
f"Index {created_addr.get('reihenfolgeIndex')}, "
|
||||||
|
f"EspoCRM ID {espo_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update EspoCRM mit Sync-Info
|
||||||
|
await self._update_espo_sync_info(espo_id, created_addr, 'synced')
|
||||||
|
|
||||||
|
return created_addr
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create address: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# Update syncStatus
|
||||||
|
await self._update_espo_sync_status(espo_addr['id'], 'error')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# UPDATE: EspoCRM → Advoware (nur R/W Felder)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
async def update_address(self, espo_addr: Dict[str, Any], betnr: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Update Adresse in Advoware (nur R/W Felder)
|
||||||
|
|
||||||
|
Nur strasse, plz, ort, anschrift werden geändert.
|
||||||
|
Alle anderen Änderungen → Notification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: CAdressen Entity von EspoCRM
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Aktualisierte Adresse oder None bei Fehler
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
espo_id = espo_addr['id']
|
||||||
|
logger.info(f"Updating address in Advoware for EspoCRM ID {espo_id}, BetNr {betnr}")
|
||||||
|
|
||||||
|
# 1. Finde Adresse in Advoware via bemerkung (EINZIGE stabile Methode)
|
||||||
|
target = await self._find_address_by_espo_id(betnr, espo_id)
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
logger.warning(f"Address not found in Advoware: {espo_id} - creating new")
|
||||||
|
return await self.create_address(espo_addr, betnr)
|
||||||
|
|
||||||
|
# 2. Map nur R/W Felder
|
||||||
|
rw_data = self.mapper.map_cadressen_to_advoware_update(espo_addr)
|
||||||
|
|
||||||
|
# 3. PUT mit aktuellem reihenfolgeIndex (dynamisch!)
|
||||||
|
current_index = target['reihenfolgeIndex']
|
||||||
|
|
||||||
|
result = await self.advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{current_index}',
|
||||||
|
method='PUT',
|
||||||
|
json_data=rw_data
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ Updated address in Advoware (R/W fields): "
|
||||||
|
f"Index {current_index}, EspoCRM ID {espo_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Prüfe READ-ONLY Feld-Änderungen
|
||||||
|
readonly_changes = self.mapper.detect_readonly_changes(espo_addr, target)
|
||||||
|
|
||||||
|
if readonly_changes:
|
||||||
|
logger.warning(
|
||||||
|
f"⚠ READ-ONLY fields changed for {espo_id}: "
|
||||||
|
f"{len(readonly_changes)} fields"
|
||||||
|
)
|
||||||
|
await self._notify_readonly_changes(espo_addr, betnr, readonly_changes)
|
||||||
|
|
||||||
|
# 5. Update EspoCRM mit Sync-Info
|
||||||
|
await self._update_espo_sync_info(espo_id, result, 'synced')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update address: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# Update syncStatus
|
||||||
|
await self._update_espo_sync_status(espo_addr['id'], 'error')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# DELETE: EspoCRM → Advoware (nur Notification)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
async def handle_address_deletion(self, espo_addr: Dict[str, Any], betnr: int) -> bool:
|
||||||
|
"""
|
||||||
|
Handle Adress-Löschung (nur Notification)
|
||||||
|
|
||||||
|
Kein API-DELETE verfügbar → Manuelle Löschung erforderlich.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: Gelöschte CAdressen Entity von EspoCRM
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Notification erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
espo_id = espo_addr['id']
|
||||||
|
logger.info(f"Handling address deletion for EspoCRM ID {espo_id}, BetNr {betnr}")
|
||||||
|
|
||||||
|
# 1. Finde Adresse in Advoware
|
||||||
|
target = await self._find_address_by_espo_id(betnr, espo_id)
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
logger.info(f"Address already deleted or not found: {espo_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 2. Erstelle Notification für manuelle Löschung
|
||||||
|
await self.notification_manager.notify_manual_action_required(
|
||||||
|
entity_type='CAdressen',
|
||||||
|
entity_id=espo_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_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to handle address deletion: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# SYNC: Advoware → EspoCRM (vollständig)
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
async def sync_from_advoware(self, betnr: int, espo_beteiligte_id: str) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Synct alle Adressen von Advoware zu EspoCRM
|
||||||
|
|
||||||
|
Alle Felder werden übernommen (Advoware = Master).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
espo_beteiligte_id: EspoCRM CBeteiligte ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mit Statistiken: created, updated, unchanged
|
||||||
|
"""
|
||||||
|
stats = {'created': 0, 'updated': 0, 'unchanged': 0, 'errors': 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Syncing addresses from Advoware BetNr {betnr} → EspoCRM {espo_beteiligte_id}")
|
||||||
|
|
||||||
|
# 1. Hole alle Adressen von Advoware
|
||||||
|
advo_addresses = await self.advo.api_call(
|
||||||
|
f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
|
||||||
|
method='GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(advo_addresses)} addresses in Advoware")
|
||||||
|
|
||||||
|
# 2. Hole existierende EspoCRM Adressen
|
||||||
|
import json
|
||||||
|
espo_addresses = await self.espo.list_entities(
|
||||||
|
'CAdressen',
|
||||||
|
where=json.dumps([{
|
||||||
|
'type': 'equals',
|
||||||
|
'attribute': 'beteiligteId',
|
||||||
|
'value': espo_beteiligte_id
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
espo_addrs_by_id = {addr['id']: addr for addr in espo_addresses.get('list', [])}
|
||||||
|
|
||||||
|
# 3. Sync jede Adresse
|
||||||
|
for advo_addr in advo_addresses:
|
||||||
|
try:
|
||||||
|
# Match via bemerkung
|
||||||
|
bemerkung = advo_addr.get('bemerkung', '')
|
||||||
|
|
||||||
|
if 'EspoCRM-ID:' in bemerkung:
|
||||||
|
# Existierende Adresse
|
||||||
|
espo_id = bemerkung.split('EspoCRM-ID:')[1].strip().split()[0]
|
||||||
|
|
||||||
|
if espo_id in espo_addrs_by_id:
|
||||||
|
# Update
|
||||||
|
result = await self._update_espo_address(
|
||||||
|
espo_id,
|
||||||
|
advo_addr,
|
||||||
|
espo_beteiligte_id,
|
||||||
|
espo_addrs_by_id[espo_id]
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
stats['updated'] += 1
|
||||||
|
else:
|
||||||
|
stats['errors'] += 1
|
||||||
|
else:
|
||||||
|
logger.warning(f"EspoCRM address not found: {espo_id}")
|
||||||
|
stats['errors'] += 1
|
||||||
|
else:
|
||||||
|
# Neue Adresse aus Advoware (kein EspoCRM-ID)
|
||||||
|
result = await self._create_espo_address(advo_addr, espo_beteiligte_id)
|
||||||
|
if result:
|
||||||
|
stats['created'] += 1
|
||||||
|
else:
|
||||||
|
stats['errors'] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync address: {e}", exc_info=True)
|
||||||
|
stats['errors'] += 1
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ Sync complete: "
|
||||||
|
f"created={stats['created']}, "
|
||||||
|
f"updated={stats['updated']}, "
|
||||||
|
f"errors={stats['errors']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync from Advoware: {e}", exc_info=True)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# HELPER METHODS
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
async def _find_address_by_espo_id(self, betnr: int, espo_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Finde Adresse in Advoware via bemerkung-Matching
|
||||||
|
|
||||||
|
Args:
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
espo_id: EspoCRM CAdressen ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Advoware Adresse oder None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return target
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to find address: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _update_espo_sync_info(self, espo_id: str, advo_addr: Dict[str, Any],
|
||||||
|
status: str = 'synced') -> bool:
|
||||||
|
"""
|
||||||
|
Update Sync-Info in EspoCRM CAdressen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_id: EspoCRM CAdressen ID
|
||||||
|
advo_addr: Advoware Adresse (für rowId)
|
||||||
|
status: syncStatus (nicht verwendet, da EspoCRM-Feld möglicherweise nicht existiert)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
update_data = {
|
||||||
|
'advowareRowId': advo_addr.get('rowId'),
|
||||||
|
'advowareLastSync': datetime.now().isoformat()
|
||||||
|
# syncStatus removed - Feld existiert möglicherweise nicht
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await self.espo.update_entity('CAdressen', espo_id, update_data)
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update sync info: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _update_espo_sync_status(self, espo_id: str, status: str) -> bool:
|
||||||
|
"""
|
||||||
|
Update nur syncStatus in EspoCRM (optional - Feld möglicherweise nicht vorhanden)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_id: EspoCRM CAdressen ID
|
||||||
|
status: syncStatus ('error', 'pending', etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Feld möglicherweise nicht vorhanden - ignoriere Fehler
|
||||||
|
result = await self.espo.update_entity(
|
||||||
|
'CAdressen',
|
||||||
|
espo_id,
|
||||||
|
{'description': f'Sync-Status: {status}'} # Als Workaround in description
|
||||||
|
)
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update sync status: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _notify_readonly_changes(self, espo_addr: Dict[str, Any], betnr: int,
|
||||||
|
changes: List[Dict[str, Any]]) -> bool:
|
||||||
|
"""
|
||||||
|
Erstelle Notification für READ-ONLY Feld-Änderungen
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_addr: EspoCRM CAdressen Entity
|
||||||
|
betnr: Advoware Beteiligte-Nummer
|
||||||
|
changes: Liste von Änderungen
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn Notification erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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:\n'
|
||||||
|
f'1. Öffne Beteiligten {betnr} in Advoware\n'
|
||||||
|
f'2. Gehe zu Adressen-Tab\n'
|
||||||
|
f'3. Passe die Felder manuell an\n'
|
||||||
|
f'4. Speichern'
|
||||||
|
),
|
||||||
|
'changes': changes,
|
||||||
|
'address': f"{espo_addr.get('adresseStreet')}, "
|
||||||
|
f"{espo_addr.get('adresseCity')}",
|
||||||
|
'betnr': betnr,
|
||||||
|
'priority': 'High'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create notification: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _create_espo_address(self, advo_addr: Dict[str, Any],
|
||||||
|
beteiligte_id: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Erstelle neue Adresse in EspoCRM
|
||||||
|
|
||||||
|
Args:
|
||||||
|
advo_addr: Advoware Adresse
|
||||||
|
beteiligte_id: EspoCRM CBeteiligte ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EspoCRM ID oder None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
espo_data = self.mapper.map_advoware_to_cadressen(advo_addr, beteiligte_id)
|
||||||
|
|
||||||
|
result = await self.espo.create_entity('CAdressen', espo_data)
|
||||||
|
|
||||||
|
if result and 'id' in result:
|
||||||
|
logger.info(f"✓ Created address in EspoCRM: {result['id']}")
|
||||||
|
return result['id']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create EspoCRM address: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _update_espo_address(self, espo_id: str, advo_addr: Dict[str, Any],
|
||||||
|
beteiligte_id: str,
|
||||||
|
existing: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
Update existierende Adresse in EspoCRM
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_id: EspoCRM CAdressen ID
|
||||||
|
advo_addr: Advoware Adresse
|
||||||
|
beteiligte_id: EspoCRM CBeteiligte ID
|
||||||
|
existing: Existierende EspoCRM Entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
espo_data = self.mapper.map_advoware_to_cadressen(
|
||||||
|
advo_addr,
|
||||||
|
beteiligte_id,
|
||||||
|
existing
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.espo.update_entity('CAdressen', espo_id, espo_data)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"✓ Updated address in EspoCRM: {espo_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update EspoCRM address: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
412
bitbylaw/services/notification_utils.py
Normal file
412
bitbylaw/services/notification_utils.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
"""
|
||||||
|
Zentrale Notification-Utilities für manuelle Eingriffe
|
||||||
|
=======================================================
|
||||||
|
|
||||||
|
Wenn Advoware-API-Limitierungen existieren (z.B. READ-ONLY Felder),
|
||||||
|
werden Notifications in EspoCRM erstellt, damit User manuelle Eingriffe
|
||||||
|
vornehmen können.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Notifications an assigned Users
|
||||||
|
- Task-Erstellung für manuelle Eingriffe
|
||||||
|
- Zentrale Verwaltung aller Notification-Types
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, Optional, Literal, List
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationManager:
|
||||||
|
"""
|
||||||
|
Zentrale Klasse für Notifications bei Sync-Problemen
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, espocrm_api, context=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
espocrm_api: EspoCRMAPI instance
|
||||||
|
context: Optional context für Logging
|
||||||
|
"""
|
||||||
|
self.espocrm = espocrm_api
|
||||||
|
self.context = context
|
||||||
|
self.logger = context.logger if context else logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def notify_manual_action_required(
|
||||||
|
self,
|
||||||
|
entity_type: str,
|
||||||
|
entity_id: str,
|
||||||
|
action_type: Literal[
|
||||||
|
"address_delete_required",
|
||||||
|
"address_reactivate_required",
|
||||||
|
"address_field_update_required",
|
||||||
|
"readonly_field_conflict",
|
||||||
|
"missing_in_advoware",
|
||||||
|
"general_manual_action"
|
||||||
|
],
|
||||||
|
details: Dict[str, Any],
|
||||||
|
assigned_user_id: Optional[str] = None,
|
||||||
|
create_task: bool = True
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Erstellt Notification und optional Task für manuelle Eingriffe
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity_type: EspoCRM Entity Type (z.B. 'CAdressen', 'CBeteiligte')
|
||||||
|
entity_id: Entity ID in EspoCRM
|
||||||
|
action_type: Art der manuellen Aktion
|
||||||
|
details: Detaillierte Informationen
|
||||||
|
assigned_user_id: User der benachrichtigt werden soll (optional)
|
||||||
|
create_task: Ob zusätzlich ein Task erstellt werden soll
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mit notification_id und optional task_id
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Hole Entity-Daten
|
||||||
|
entity = await self.espocrm.get_entity(entity_type, entity_id)
|
||||||
|
entity_name = entity.get('name', f"{entity_type} {entity_id}")
|
||||||
|
|
||||||
|
# Falls kein assigned_user, versuche aus Entity zu holen
|
||||||
|
if not assigned_user_id:
|
||||||
|
assigned_user_id = entity.get('assignedUserId')
|
||||||
|
|
||||||
|
# Erstelle Notification
|
||||||
|
notification_data = self._build_notification_message(
|
||||||
|
action_type, entity_type, entity_name, details
|
||||||
|
)
|
||||||
|
|
||||||
|
notification_id = await self._create_notification(
|
||||||
|
user_id=assigned_user_id,
|
||||||
|
message=notification_data['message'],
|
||||||
|
entity_type=entity_type,
|
||||||
|
entity_id=entity_id
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {'notification_id': notification_id}
|
||||||
|
|
||||||
|
# Optional: Task erstellen
|
||||||
|
if create_task:
|
||||||
|
task_id = await self._create_task(
|
||||||
|
name=notification_data['task_name'],
|
||||||
|
description=notification_data['task_description'],
|
||||||
|
parent_type=entity_type,
|
||||||
|
parent_id=entity_id,
|
||||||
|
assigned_user_id=assigned_user_id,
|
||||||
|
priority=notification_data['priority']
|
||||||
|
)
|
||||||
|
result['task_id'] = task_id
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"Manual action notification created: {action_type} for "
|
||||||
|
f"{entity_type}/{entity_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to create notification: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _build_notification_message(
|
||||||
|
self,
|
||||||
|
action_type: str,
|
||||||
|
entity_type: str,
|
||||||
|
entity_name: str,
|
||||||
|
details: Dict[str, Any]
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Erstellt Notification-Message basierend auf Action-Type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mit 'message', 'task_name', 'task_description', 'priority'
|
||||||
|
"""
|
||||||
|
|
||||||
|
if action_type == "address_delete_required":
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"🗑️ Adresse in Advoware löschen erforderlich\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"Grund: Advoware API unterstützt kein DELETE und gueltigBis ist READ-ONLY\n"
|
||||||
|
f"Bitte manuell in Advoware löschen oder deaktivieren."
|
||||||
|
),
|
||||||
|
'task_name': f"Adresse in Advoware löschen: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"MANUELLE AKTION ERFORDERLICH\n\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"BetNr: {details.get('betnr', 'N/A')}\n"
|
||||||
|
f"Adresse: {details.get('strasse', '')}, {details.get('plz', '')} {details.get('ort', '')}\n\n"
|
||||||
|
f"GRUND:\n"
|
||||||
|
f"- DELETE API nicht verfügbar (403 Forbidden)\n"
|
||||||
|
f"- gueltigBis ist READ-ONLY (kann nicht nachträglich gesetzt werden)\n\n"
|
||||||
|
f"AKTION:\n"
|
||||||
|
f"1. In Advoware Web-Interface einloggen\n"
|
||||||
|
f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n"
|
||||||
|
f"3. Adresse suchen: {details.get('strasse', '')}\n"
|
||||||
|
f"4. Adresse löschen oder deaktivieren\n\n"
|
||||||
|
f"Nach Erledigung: Task als 'Completed' markieren."
|
||||||
|
),
|
||||||
|
'priority': 'Normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif action_type == "address_reactivate_required":
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"♻️ Adresse-Reaktivierung in Advoware erforderlich\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"Grund: gueltigBis kann nicht nachträglich geändert werden\n"
|
||||||
|
f"Bitte neue Adresse in Advoware erstellen."
|
||||||
|
),
|
||||||
|
'task_name': f"Neue Adresse in Advoware erstellen: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"MANUELLE AKTION ERFORDERLICH\n\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"BetNr: {details.get('betnr', 'N/A')}\n\n"
|
||||||
|
f"GRUND:\n"
|
||||||
|
f"Diese Adresse wurde reaktiviert, aber die alte Adresse in Advoware "
|
||||||
|
f"ist abgelaufen (gueltigBis in Vergangenheit). Da gueltigBis READ-ONLY ist, "
|
||||||
|
f"muss eine neue Adresse erstellt werden.\n\n"
|
||||||
|
f"AKTION:\n"
|
||||||
|
f"1. In Advoware Web-Interface einloggen\n"
|
||||||
|
f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n"
|
||||||
|
f"3. Neue Adresse erstellen:\n"
|
||||||
|
f" - Straße: {details.get('strasse', '')}\n"
|
||||||
|
f" - PLZ: {details.get('plz', '')}\n"
|
||||||
|
f" - Ort: {details.get('ort', '')}\n"
|
||||||
|
f" - Land: {details.get('land', '')}\n"
|
||||||
|
f" - Bemerkung: EspoCRM-ID: {details.get('espocrm_id', '')}\n"
|
||||||
|
f"4. Sync erneut durchführen, damit Mapping aktualisiert wird\n\n"
|
||||||
|
f"Nach Erledigung: Task als 'Completed' markieren."
|
||||||
|
),
|
||||||
|
'priority': 'Normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif action_type == "address_field_update_required":
|
||||||
|
readonly_fields = details.get('readonly_fields', [])
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"⚠️ Adressfelder in Advoware können nicht aktualisiert werden\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"READ-ONLY Felder: {', '.join(readonly_fields)}\n"
|
||||||
|
f"Bitte manuell in Advoware ändern."
|
||||||
|
),
|
||||||
|
'task_name': f"Adressfelder in Advoware aktualisieren: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"MANUELLE AKTION ERFORDERLICH\n\n"
|
||||||
|
f"Adresse: {entity_name}\n"
|
||||||
|
f"BetNr: {details.get('betnr', 'N/A')}\n\n"
|
||||||
|
f"GRUND:\n"
|
||||||
|
f"Folgende Felder sind in Advoware API READ-ONLY und können nicht "
|
||||||
|
f"via PUT geändert werden:\n"
|
||||||
|
f"- {', '.join(readonly_fields)}\n\n"
|
||||||
|
f"GEWÜNSCHTE ÄNDERUNGEN:\n" +
|
||||||
|
'\n'.join([f" - {k}: {v}" for k, v in details.get('changes', {}).items()]) +
|
||||||
|
f"\n\nAKTION:\n"
|
||||||
|
f"1. In Advoware Web-Interface einloggen\n"
|
||||||
|
f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n"
|
||||||
|
f"3. Adresse suchen und obige Felder manuell ändern\n"
|
||||||
|
f"4. Sync erneut durchführen zur Bestätigung\n\n"
|
||||||
|
f"Nach Erledigung: Task als 'Completed' markieren."
|
||||||
|
),
|
||||||
|
'priority': 'Low'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif action_type == "readonly_field_conflict":
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"⚠️ Sync-Konflikt bei READ-ONLY Feldern\n"
|
||||||
|
f"{entity_type}: {entity_name}\n"
|
||||||
|
f"Änderungen konnten nicht synchronisiert werden."
|
||||||
|
),
|
||||||
|
'task_name': f"Sync-Konflikt prüfen: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"SYNC-KONFLIKT\n\n"
|
||||||
|
f"{entity_type}: {entity_name}\n\n"
|
||||||
|
f"PROBLEM:\n"
|
||||||
|
f"Felder wurden in EspoCRM geändert, sind aber in Advoware READ-ONLY.\n\n"
|
||||||
|
f"BETROFFENE FELDER:\n" +
|
||||||
|
'\n'.join([f" - {k}: {v}" for k, v in details.get('conflicts', {}).items()]) +
|
||||||
|
f"\n\nOPTIONEN:\n"
|
||||||
|
f"1. Änderungen in EspoCRM rückgängig machen (Advoware = Master)\n"
|
||||||
|
f"2. Änderungen manuell in Advoware vornehmen\n"
|
||||||
|
f"3. Feld als 'nicht synchronisiert' akzeptieren\n\n"
|
||||||
|
f"Nach Entscheidung: Task als 'Completed' markieren."
|
||||||
|
),
|
||||||
|
'priority': 'Normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
elif action_type == "missing_in_advoware":
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"❓ Element fehlt in Advoware\n"
|
||||||
|
f"{entity_type}: {entity_name}\n"
|
||||||
|
f"Bitte manuell in Advoware erstellen."
|
||||||
|
),
|
||||||
|
'task_name': f"In Advoware erstellen: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"MANUELLE AKTION ERFORDERLICH\n\n"
|
||||||
|
f"{entity_type}: {entity_name}\n\n"
|
||||||
|
f"GRUND:\n"
|
||||||
|
f"Dieses Element existiert in EspoCRM, aber nicht in Advoware.\n"
|
||||||
|
f"Möglicherweise wurde es direkt in EspoCRM erstellt.\n\n"
|
||||||
|
f"DATEN:\n" +
|
||||||
|
'\n'.join([f" - {k}: {v}" for k, v in details.items() if k != 'espocrm_id']) +
|
||||||
|
f"\n\nAKTION:\n"
|
||||||
|
f"1. In Advoware Web-Interface einloggen\n"
|
||||||
|
f"2. Element mit obigen Daten manuell erstellen\n"
|
||||||
|
f"3. Sync erneut durchführen für Mapping\n\n"
|
||||||
|
f"Nach Erledigung: Task als 'Completed' markieren."
|
||||||
|
),
|
||||||
|
'priority': 'Normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
else: # general_manual_action
|
||||||
|
return {
|
||||||
|
'message': (
|
||||||
|
f"🔧 Manuelle Aktion erforderlich\n"
|
||||||
|
f"{entity_type}: {entity_name}\n"
|
||||||
|
f"{details.get('message', 'Bitte prüfen.')}"
|
||||||
|
),
|
||||||
|
'task_name': f"Manuelle Aktion: {entity_name}",
|
||||||
|
'task_description': (
|
||||||
|
f"MANUELLE AKTION ERFORDERLICH\n\n"
|
||||||
|
f"{entity_type}: {entity_name}\n\n"
|
||||||
|
f"{details.get('description', 'Keine Details verfügbar.')}"
|
||||||
|
),
|
||||||
|
'priority': details.get('priority', 'Normal')
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _create_notification(
|
||||||
|
self,
|
||||||
|
user_id: Optional[str],
|
||||||
|
message: str,
|
||||||
|
entity_type: str,
|
||||||
|
entity_id: str
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Erstellt EspoCRM Notification (In-App)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
notification_id
|
||||||
|
"""
|
||||||
|
if not user_id:
|
||||||
|
self.logger.warning("No user assigned - notification not created")
|
||||||
|
return None
|
||||||
|
|
||||||
|
notification_data = {
|
||||||
|
'type': 'Message',
|
||||||
|
'message': message,
|
||||||
|
'userId': user_id,
|
||||||
|
'relatedType': entity_type,
|
||||||
|
'relatedId': entity_id,
|
||||||
|
'read': False
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.espocrm.create_entity('Notification', notification_data)
|
||||||
|
return result.get('id')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to create notification: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _create_task(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
parent_type: str,
|
||||||
|
parent_id: str,
|
||||||
|
assigned_user_id: Optional[str],
|
||||||
|
priority: str = 'Normal'
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Erstellt EspoCRM Task
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
task_id
|
||||||
|
"""
|
||||||
|
# Due Date: 7 Tage in Zukunft
|
||||||
|
due_date = (datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
task_data = {
|
||||||
|
'name': name,
|
||||||
|
'description': description,
|
||||||
|
'status': 'Not Started',
|
||||||
|
'priority': priority,
|
||||||
|
'dateEnd': due_date,
|
||||||
|
'parentType': parent_type,
|
||||||
|
'parentId': parent_id,
|
||||||
|
'assignedUserId': assigned_user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.espocrm.create_entity('Task', task_data)
|
||||||
|
return result.get('id')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to create task: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def resolve_task(self, task_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Markiert Task als erledigt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: Task ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True wenn erfolgreich
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await self.espocrm.update_entity('Task', task_id, {
|
||||||
|
'status': 'Completed'
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to complete task {task_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Helper-Funktionen für häufige Use-Cases
|
||||||
|
|
||||||
|
async def notify_address_delete_required(
|
||||||
|
notification_manager: NotificationManager,
|
||||||
|
address_entity_id: str,
|
||||||
|
betnr: str,
|
||||||
|
address_data: Dict[str, Any]
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Shortcut: Notification für Adresse löschen
|
||||||
|
"""
|
||||||
|
return await notification_manager.notify_manual_action_required(
|
||||||
|
entity_type='CAdressen',
|
||||||
|
entity_id=address_entity_id,
|
||||||
|
action_type='address_delete_required',
|
||||||
|
details={
|
||||||
|
'betnr': betnr,
|
||||||
|
'strasse': address_data.get('adresseStreet'),
|
||||||
|
'plz': address_data.get('adressePostalCode'),
|
||||||
|
'ort': address_data.get('adresseCity'),
|
||||||
|
'espocrm_id': address_entity_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_address_readonly_fields(
|
||||||
|
notification_manager: NotificationManager,
|
||||||
|
address_entity_id: str,
|
||||||
|
betnr: str,
|
||||||
|
readonly_fields: List[str],
|
||||||
|
changes: Dict[str, Any]
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Shortcut: Notification für READ-ONLY Felder
|
||||||
|
"""
|
||||||
|
return await notification_manager.notify_manual_action_required(
|
||||||
|
entity_type='CAdressen',
|
||||||
|
entity_id=address_entity_id,
|
||||||
|
action_type='address_field_update_required',
|
||||||
|
details={
|
||||||
|
'betnr': betnr,
|
||||||
|
'readonly_fields': readonly_fields,
|
||||||
|
'changes': changes
|
||||||
|
}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user