Migrate VMH Integration - Phase 3: Core sync handlers & utilities
- Added 5 core service modules:
* services/notification_utils.py: NotificationManager for manual actions (412 lines)
* services/advoware_service.py: Extended Advoware operations wrapper
* services/espocrm_mapper.py: BeteiligteMapper for data transformation (198 lines)
* services/bankverbindungen_mapper.py: BankverbindungenMapper (174 lines)
* services/beteiligte_sync_utils.py: BeteiligteSync with complex logic (663 lines)
- Distributed locking via Redis + syncStatus
- rowId-based change detection (primary) + timestamp fallback
- Retry with exponential backoff (1min, 5min, 15min, 1h, 4h)
- Conflict resolution (EspoCRM wins)
- Soft-delete handling & round-trip validation
- Added 2 event handler steps:
* steps/vmh/beteiligte_sync_event_step.py: Handles vmh.beteiligte.* queue events
- CREATE: New Beteiligte → POST to Advoware
- UPDATE: Bi-directional sync with conflict resolution
- DELETE: Not yet implemented
- SYNC_CHECK: Periodic sync check via cron
* steps/vmh/bankverbindungen_sync_event_step.py: Handles vmh.bankverbindungen.* events
- CREATE: New Bankverbindung → POST to Advoware
- UPDATE/DELETE: Send notification (API limitation - no PUT/DELETE support)
- Added pytz dependency to pyproject.toml (required for timezone handling)
System Status:
- 27 steps registered (14 VMH-specific)
- 8 queue subscriptions active (4 Beteiligte + 4 Bankverbindungen)
- All webhook endpoints operational and emitting queue events
- Sync handlers ready for processing
Note: Kommunikation sync (kommunikation_sync_utils.py) not yet migrated.
Complex bi-directional sync for Telefon/Email/Fax will be added in Phase 4.
Updated MIGRATION_STATUS.md
This commit is contained in:
193
services/espocrm_mapper.py
Normal file
193
services/espocrm_mapper.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
EspoCRM ↔ Advoware Entity Mapper
|
||||
|
||||
Transformiert Beteiligte zwischen den beiden Systemen basierend auf ENTITY_MAPPING_CBeteiligte_Advoware.md
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BeteiligteMapper:
|
||||
"""Mapper für CBeteiligte (EspoCRM) ↔ Beteiligte (Advoware)"""
|
||||
|
||||
@staticmethod
|
||||
def map_cbeteiligte_to_advoware(espo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Transformiert EspoCRM CBeteiligte → Advoware Beteiligte Format (STAMMDATEN)
|
||||
|
||||
WICHTIG: Kontaktdaten (Telefon, Email, Fax, Bankverbindungen) werden über
|
||||
separate Advoware-Endpoints gesynct und sind NICHT Teil dieser Mapping-Funktion.
|
||||
|
||||
Args:
|
||||
espo_entity: CBeteiligte Entity von EspoCRM
|
||||
|
||||
Returns:
|
||||
Dict mit Stammdaten für Advoware API (POST/PUT /api/v1/advonet/Beteiligte)
|
||||
"""
|
||||
logger.debug(f"Mapping EspoCRM → Advoware STAMMDATEN: {espo_entity.get('id')}")
|
||||
|
||||
# Bestimme ob Person oder Firma (über firmenname-Feld)
|
||||
firmenname = espo_entity.get('firmenname')
|
||||
is_firma = bool(firmenname and firmenname.strip())
|
||||
|
||||
# Basis-Struktur (nur die funktionierenden Felder!)
|
||||
advo_data = {
|
||||
'rechtsform': espo_entity.get('rechtsform', ''),
|
||||
}
|
||||
|
||||
# NAME: Person vs. Firma
|
||||
if is_firma:
|
||||
# Firma: Lese von firmenname-Feld
|
||||
advo_data['name'] = firmenname
|
||||
advo_data['vorname'] = None
|
||||
else:
|
||||
# Natürliche Person: Lese von lastName/firstName
|
||||
advo_data['name'] = espo_entity.get('lastName', '')
|
||||
advo_data['vorname'] = espo_entity.get('firstName', '')
|
||||
|
||||
# ANREDE & TITEL (funktionierende Felder)
|
||||
salutation = espo_entity.get('salutationName')
|
||||
if salutation:
|
||||
advo_data['anrede'] = salutation
|
||||
|
||||
titel = espo_entity.get('titel')
|
||||
if titel:
|
||||
advo_data['titel'] = titel
|
||||
|
||||
# BRIEFANREDE (bAnrede)
|
||||
brief_anrede = espo_entity.get('briefAnrede')
|
||||
if brief_anrede:
|
||||
advo_data['bAnrede'] = brief_anrede
|
||||
|
||||
# ZUSATZ
|
||||
zusatz = espo_entity.get('zusatz')
|
||||
if zusatz:
|
||||
advo_data['zusatz'] = zusatz
|
||||
|
||||
# GEBURTSDATUM
|
||||
date_of_birth = espo_entity.get('dateOfBirth')
|
||||
if date_of_birth:
|
||||
advo_data['geburtsdatum'] = date_of_birth
|
||||
|
||||
# HINWEIS: handelsRegisterNummer und registergericht funktionieren NICHT!
|
||||
# Advoware ignoriert diese Felder im PUT (trotz Swagger Schema)
|
||||
|
||||
logger.debug(f"Mapped to Advoware STAMMDATEN: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}, rechtsform={advo_data.get('rechtsform')}")
|
||||
|
||||
return advo_data
|
||||
|
||||
@staticmethod
|
||||
def map_advoware_to_cbeteiligte(advo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Transformiert Advoware Beteiligte → EspoCRM CBeteiligte Format
|
||||
|
||||
Args:
|
||||
advo_entity: Beteiligter von Advoware API
|
||||
|
||||
Returns:
|
||||
Dict für EspoCRM API (POST/PUT /api/v1/CBeteiligte)
|
||||
"""
|
||||
logger.debug(f"Mapping Advoware → EspoCRM: betNr={advo_entity.get('betNr')}")
|
||||
|
||||
# Bestimme ob Person oder Firma
|
||||
vorname = advo_entity.get('vorname')
|
||||
is_person = bool(vorname)
|
||||
|
||||
# Basis-Struktur
|
||||
espo_data = {
|
||||
'rechtsform': advo_entity.get('rechtsform', ''),
|
||||
'betnr': advo_entity.get('betNr'), # Link zu Advoware
|
||||
'advowareRowId': advo_entity.get('rowId'), # Änderungserkennung
|
||||
}
|
||||
|
||||
# NAME: Person vs. Firma (EspoCRM blendet lastName/firstName aus bei Firmen)
|
||||
if is_person:
|
||||
# Natürliche Person → lastName/firstName verwenden
|
||||
espo_data['firstName'] = vorname
|
||||
espo_data['lastName'] = advo_entity.get('name', '')
|
||||
espo_data['name'] = f"{vorname} {advo_entity.get('name', '')}".strip()
|
||||
espo_data['firmenname'] = None # Firma-Feld leer lassen
|
||||
else:
|
||||
# Firma → firmenname verwenden (EspoCRM zeigt dann nur dieses Feld)
|
||||
firma_name = advo_entity.get('name', '')
|
||||
espo_data['firmenname'] = firma_name
|
||||
espo_data['name'] = firma_name
|
||||
# lastName/firstName nicht setzen (EspoCRM blendet sie aus bei Firmen)
|
||||
espo_data['firstName'] = None
|
||||
espo_data['lastName'] = None
|
||||
|
||||
# ANREDE & TITEL
|
||||
anrede = advo_entity.get('anrede')
|
||||
if anrede:
|
||||
espo_data['salutationName'] = anrede
|
||||
|
||||
titel = advo_entity.get('titel')
|
||||
if titel:
|
||||
espo_data['titel'] = titel
|
||||
|
||||
# BRIEFANREDE
|
||||
b_anrede = advo_entity.get('bAnrede')
|
||||
if b_anrede:
|
||||
espo_data['briefAnrede'] = b_anrede
|
||||
|
||||
# ZUSATZ
|
||||
zusatz = advo_entity.get('zusatz')
|
||||
if zusatz:
|
||||
espo_data['zusatz'] = zusatz
|
||||
|
||||
# GEBURTSDATUM (nur Datum-Teil ohne Zeit)
|
||||
geburtsdatum = advo_entity.get('geburtsdatum')
|
||||
if geburtsdatum:
|
||||
# Advoware gibt '2001-01-05T00:00:00', EspoCRM will nur '2001-01-05'
|
||||
espo_data['dateOfBirth'] = geburtsdatum.split('T')[0] if 'T' in geburtsdatum else geburtsdatum
|
||||
|
||||
logger.debug(f"Mapped to EspoCRM STAMMDATEN: name={espo_data.get('name')}")
|
||||
|
||||
# WICHTIG: Entferne None-Werte (EspoCRM mag keine expliziten None bei required fields)
|
||||
espo_data = {k: v for k, v in espo_data.items() if v is not None}
|
||||
|
||||
return espo_data
|
||||
|
||||
@staticmethod
|
||||
def get_changed_fields(espo_entity: Dict[str, Any], advo_entity: Dict[str, Any]) -> List[str]:
|
||||
"""
|
||||
Vergleicht zwei Entities und gibt Liste der geänderten Felder zurück
|
||||
|
||||
Args:
|
||||
espo_entity: EspoCRM CBeteiligte
|
||||
advo_entity: Advoware Beteiligte
|
||||
|
||||
Returns:
|
||||
Liste von Feldnamen die unterschiedlich sind
|
||||
"""
|
||||
# Mappe Advoware zu EspoCRM Format für Vergleich
|
||||
mapped_advo = BeteiligteMapper.map_advoware_to_cbeteiligte(advo_entity)
|
||||
|
||||
changed = []
|
||||
|
||||
# Vergleiche wichtige Felder
|
||||
compare_fields = [
|
||||
'name', 'firstName', 'lastName', 'firmenname',
|
||||
'emailAddress', 'phoneNumber',
|
||||
'dateOfBirth', 'rechtsform',
|
||||
'handelsregisterNummer', 'handelsregisterArt', 'registergericht',
|
||||
'betnr', 'advowareRowId'
|
||||
]
|
||||
|
||||
for field in compare_fields:
|
||||
espo_val = espo_entity.get(field)
|
||||
advo_val = mapped_advo.get(field)
|
||||
|
||||
# Normalisiere None und leere Strings
|
||||
espo_val = espo_val if espo_val else None
|
||||
advo_val = advo_val if advo_val else None
|
||||
|
||||
if espo_val != advo_val:
|
||||
changed.append(field)
|
||||
logger.debug(f"Field '{field}' changed: EspoCRM='{espo_val}' vs Advoware='{advo_val}'")
|
||||
|
||||
return changed
|
||||
Reference in New Issue
Block a user