Implement EspoCRM-based sync strategy for Beteiligte entities
- Add SYNC_STRATEGY_ESPOCRM_BASED.md detailing the sync flows and status management. - Create utilities for sync operations in services/beteiligte_sync_utils.py, including locking, timestamp comparison, conflict resolution, and notification handling. - Implement entity mapping between EspoCRM and Advoware in services/espocrm_mapper.py. - Develop a cron job for periodic sync checks in steps/vmh/beteiligte_sync_cron_step.py, emitting events for entities needing synchronization.
This commit is contained in:
341
bitbylaw/services/beteiligte_sync_utils.py
Normal file
341
bitbylaw/services/beteiligte_sync_utils.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
Beteiligte Sync Utilities
|
||||
|
||||
Hilfsfunktionen für Sync-Operationen:
|
||||
- Locking via syncStatus
|
||||
- Timestamp-Vergleich
|
||||
- Konfliktauflösung (EspoCRM wins)
|
||||
- EspoCRM In-App Notifications
|
||||
- Soft-Delete Handling
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Tuple, Literal
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import logging
|
||||
from services.espocrm import EspoCRMAPI
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Timestamp-Vergleich Ergebnis-Typen
|
||||
TimestampResult = Literal["espocrm_newer", "advoware_newer", "conflict", "no_change"]
|
||||
|
||||
|
||||
class BeteiligteSync:
|
||||
"""Utility-Klasse für Beteiligte-Synchronisation"""
|
||||
|
||||
def __init__(self, espocrm_api: EspoCRMAPI, context=None):
|
||||
self.espocrm = espocrm_api
|
||||
self.context = context
|
||||
|
||||
def _log(self, message: str, level: str = 'info'):
|
||||
"""Logging mit Context-Support"""
|
||||
if self.context and hasattr(self.context, 'logger'):
|
||||
getattr(self.context.logger, level)(message)
|
||||
else:
|
||||
getattr(logger, level)(message)
|
||||
|
||||
async def acquire_sync_lock(self, entity_id: str) -> bool:
|
||||
"""
|
||||
Setzt syncStatus auf "syncing" (atomares Lock)
|
||||
|
||||
Args:
|
||||
entity_id: EspoCRM CBeteiligte ID
|
||||
|
||||
Returns:
|
||||
True wenn Lock erfolgreich, False wenn bereits im Sync
|
||||
"""
|
||||
try:
|
||||
entity = await self.espocrm.get_entity('CBeteiligte', entity_id)
|
||||
|
||||
current_status = entity.get('syncStatus')
|
||||
|
||||
if current_status == 'syncing':
|
||||
self._log(f"Entity {entity_id} bereits im Sync-Prozess", level='warning')
|
||||
return False
|
||||
|
||||
# Setze Lock
|
||||
await self.espocrm.update_entity('CBeteiligte', entity_id, {
|
||||
'syncStatus': 'syncing'
|
||||
})
|
||||
|
||||
self._log(f"Sync-Lock für {entity_id} erworben (vorher: {current_status})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Fehler beim Acquire Lock: {e}", level='error')
|
||||
return False
|
||||
|
||||
async def release_sync_lock(
|
||||
self,
|
||||
entity_id: str,
|
||||
new_status: str = 'clean',
|
||||
error_message: Optional[str] = None,
|
||||
increment_retry: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Gibt Sync-Lock frei und setzt finalen Status
|
||||
|
||||
Args:
|
||||
entity_id: EspoCRM CBeteiligte ID
|
||||
new_status: Neuer syncStatus (clean, failed, conflict, etc.)
|
||||
error_message: Optional: Fehlermeldung für syncErrorMessage
|
||||
increment_retry: Ob syncRetryCount erhöht werden soll
|
||||
"""
|
||||
try:
|
||||
update_data = {
|
||||
'syncStatus': new_status,
|
||||
'advowareLastSync': datetime.now(pytz.UTC).isoformat()
|
||||
}
|
||||
|
||||
if error_message:
|
||||
update_data['syncErrorMessage'] = error_message[:2000] # Max. 2000 chars
|
||||
else:
|
||||
update_data['syncErrorMessage'] = None
|
||||
|
||||
if increment_retry:
|
||||
# Hole aktuellen Retry-Count
|
||||
entity = await self.espocrm.get_entity('CBeteiligte', entity_id)
|
||||
current_retry = entity.get('syncRetryCount') or 0
|
||||
update_data['syncRetryCount'] = current_retry + 1
|
||||
else:
|
||||
update_data['syncRetryCount'] = 0
|
||||
|
||||
await self.espocrm.update_entity('CBeteiligte', entity_id, update_data)
|
||||
|
||||
self._log(f"Sync-Lock released: {entity_id} → {new_status}")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Fehler beim Release Lock: {e}", level='error')
|
||||
|
||||
@staticmethod
|
||||
def parse_timestamp(ts: Any) -> Optional[datetime]:
|
||||
"""
|
||||
Parse verschiedene Timestamp-Formate zu datetime
|
||||
|
||||
Args:
|
||||
ts: String, datetime oder None
|
||||
|
||||
Returns:
|
||||
datetime-Objekt oder None
|
||||
"""
|
||||
if not ts:
|
||||
return None
|
||||
|
||||
if isinstance(ts, datetime):
|
||||
return ts
|
||||
|
||||
if isinstance(ts, str):
|
||||
# EspoCRM Format: "2026-02-07 14:30:00"
|
||||
# Advoware Format: "2026-02-07T14:30:00" oder "2026-02-07T14:30:00Z"
|
||||
try:
|
||||
# Entferne trailing Z falls vorhanden
|
||||
ts = ts.rstrip('Z')
|
||||
|
||||
# Versuche verschiedene Formate
|
||||
for fmt in [
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%Y-%m-%dT%H:%M:%S',
|
||||
'%Y-%m-%d',
|
||||
]:
|
||||
try:
|
||||
return datetime.strptime(ts, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Fallback: ISO-Format
|
||||
return datetime.fromisoformat(ts)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte Timestamp nicht parsen: {ts} - {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def compare_timestamps(
|
||||
self,
|
||||
espo_modified_at: Any,
|
||||
advo_geaendert_am: Any,
|
||||
last_sync_ts: Any
|
||||
) -> TimestampResult:
|
||||
"""
|
||||
Vergleicht Timestamps und bestimmt Sync-Richtung
|
||||
|
||||
Args:
|
||||
espo_modified_at: EspoCRM modifiedAt
|
||||
advo_geaendert_am: Advoware geaendertAm
|
||||
last_sync_ts: Letzter Sync (advowareLastSync)
|
||||
|
||||
Returns:
|
||||
"espocrm_newer": EspoCRM wurde nach last_sync geändert und ist neuer
|
||||
"advoware_newer": Advoware wurde nach last_sync geändert und ist neuer
|
||||
"conflict": Beide wurden nach last_sync geändert
|
||||
"no_change": Keine Änderungen seit last_sync
|
||||
"""
|
||||
espo_ts = self.parse_timestamp(espo_modified_at)
|
||||
advo_ts = self.parse_timestamp(advo_geaendert_am)
|
||||
sync_ts = self.parse_timestamp(last_sync_ts)
|
||||
|
||||
# Logging
|
||||
self._log(
|
||||
f"Timestamp-Vergleich: EspoCRM={espo_ts}, Advoware={advo_ts}, LastSync={sync_ts}",
|
||||
level='debug'
|
||||
)
|
||||
|
||||
# Falls kein last_sync → erster Sync, vergleiche direkt
|
||||
if not sync_ts:
|
||||
if not espo_ts or not advo_ts:
|
||||
return "no_change"
|
||||
|
||||
if espo_ts > advo_ts:
|
||||
return "espocrm_newer"
|
||||
elif advo_ts > espo_ts:
|
||||
return "advoware_newer"
|
||||
else:
|
||||
return "no_change"
|
||||
|
||||
# Check ob seit last_sync Änderungen
|
||||
espo_changed = espo_ts and espo_ts > sync_ts
|
||||
advo_changed = advo_ts and advo_ts > sync_ts
|
||||
|
||||
if espo_changed and advo_changed:
|
||||
# Beide geändert seit last_sync → Konflikt
|
||||
return "conflict"
|
||||
elif espo_changed:
|
||||
# Nur EspoCRM geändert
|
||||
return "espocrm_newer" if (not advo_ts or espo_ts > advo_ts) else "conflict"
|
||||
elif advo_changed:
|
||||
# Nur Advoware geändert
|
||||
return "advoware_newer"
|
||||
else:
|
||||
# Keine Änderungen
|
||||
return "no_change"
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
entity_id: str,
|
||||
notification_type: Literal["conflict", "deleted"],
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Sendet EspoCRM In-App Notification
|
||||
|
||||
Args:
|
||||
entity_id: CBeteiligte Entity ID
|
||||
notification_type: "conflict" oder "deleted"
|
||||
extra_data: Zusätzliche Daten für Nachricht
|
||||
"""
|
||||
try:
|
||||
# Hole Entity-Daten
|
||||
entity = await self.espocrm.get_entity('CBeteiligte', entity_id)
|
||||
name = entity.get('name', 'Unbekannt')
|
||||
betnr = entity.get('betnr')
|
||||
assigned_user = entity.get('assignedUserId')
|
||||
|
||||
# Erstelle Nachricht basierend auf Typ
|
||||
if notification_type == "conflict":
|
||||
message = (
|
||||
f"⚠️ Sync-Konflikt bei Beteiligten '{name}' (betNr: {betnr}). "
|
||||
f"EspoCRM hat Vorrang - Änderungen wurden nach Advoware übertragen. "
|
||||
f"Bitte prüfen Sie die Details."
|
||||
)
|
||||
elif notification_type == "deleted":
|
||||
deleted_at = entity.get('advowareDeletedAt', 'unbekannt')
|
||||
message = (
|
||||
f"🗑️ Beteiligter '{name}' (betNr: {betnr}) wurde in Advoware gelöscht "
|
||||
f"(am {deleted_at}). Der Datensatz wurde in EspoCRM markiert, aber nicht gelöscht. "
|
||||
f"Bitte prüfen Sie, ob dies beabsichtigt war."
|
||||
)
|
||||
else:
|
||||
message = f"Benachrichtigung für Beteiligten '{name}'"
|
||||
|
||||
# Erstelle Notification in EspoCRM
|
||||
notification_data = {
|
||||
'type': 'message',
|
||||
'message': message,
|
||||
'relatedType': 'CBeteiligte',
|
||||
'relatedId': entity_id,
|
||||
}
|
||||
|
||||
# Wenn assigned user vorhanden, sende an diesen
|
||||
if assigned_user:
|
||||
notification_data['userId'] = assigned_user
|
||||
|
||||
# Sende via API
|
||||
result = await self.espocrm.api_call(
|
||||
'Notification',
|
||||
method='POST',
|
||||
data=notification_data
|
||||
)
|
||||
|
||||
self._log(f"Notification gesendet für {entity_id}: {notification_type}")
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Fehler beim Senden der Notification: {e}", level='error')
|
||||
|
||||
async def handle_advoware_deleted(
|
||||
self,
|
||||
entity_id: str,
|
||||
error_details: str
|
||||
) -> None:
|
||||
"""
|
||||
Behandelt Fall dass Beteiligter in Advoware gelöscht wurde (404)
|
||||
|
||||
Args:
|
||||
entity_id: CBeteiligte Entity ID
|
||||
error_details: Fehlerdetails von Advoware API
|
||||
"""
|
||||
try:
|
||||
now = datetime.now(pytz.UTC).isoformat()
|
||||
|
||||
# Update Entity: Soft-Delete Flag
|
||||
await self.espocrm.update_entity('CBeteiligte', entity_id, {
|
||||
'syncStatus': 'deleted_in_advoware',
|
||||
'advowareDeletedAt': now,
|
||||
'syncErrorMessage': f"Beteiligter existiert nicht mehr in Advoware. {error_details}"
|
||||
})
|
||||
|
||||
self._log(f"Entity {entity_id} als deleted_in_advoware markiert")
|
||||
|
||||
# Sende Notification
|
||||
await self.send_notification(entity_id, 'deleted')
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Fehler beim Handle Deleted: {e}", level='error')
|
||||
|
||||
async def resolve_conflict_espocrm_wins(
|
||||
self,
|
||||
entity_id: str,
|
||||
espo_entity: Dict[str, Any],
|
||||
advo_entity: Dict[str, Any],
|
||||
conflict_details: str
|
||||
) -> None:
|
||||
"""
|
||||
Löst Konflikt auf: EspoCRM wins (überschreibt Advoware)
|
||||
|
||||
Args:
|
||||
entity_id: CBeteiligte Entity ID
|
||||
espo_entity: EspoCRM Entity-Daten
|
||||
advo_entity: Advoware Entity-Daten
|
||||
conflict_details: Details zum Konflikt
|
||||
"""
|
||||
try:
|
||||
now = datetime.now(pytz.UTC).isoformat()
|
||||
|
||||
# Markiere als gelöst mit Konflikt-Info
|
||||
await self.espocrm.update_entity('CBeteiligte', entity_id, {
|
||||
'syncStatus': 'clean', # Gelöst!
|
||||
'advowareLastSync': now,
|
||||
'syncErrorMessage': f"Konflikt am {now}: {conflict_details}. EspoCRM hat gewonnen.",
|
||||
'syncRetryCount': 0
|
||||
})
|
||||
|
||||
self._log(f"Konflikt gelöst für {entity_id}: EspoCRM wins")
|
||||
|
||||
# Sende Notification
|
||||
await self.send_notification(entity_id, 'conflict', {
|
||||
'details': conflict_details
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Fehler beim Resolve Conflict: {e}", level='error')
|
||||
243
bitbylaw/services/espocrm_mapper.py
Normal file
243
bitbylaw/services/espocrm_mapper.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Args:
|
||||
espo_entity: CBeteiligte Entity von EspoCRM
|
||||
|
||||
Returns:
|
||||
Dict für Advoware API (POST/PUT /api/v1/advonet/Beteiligte)
|
||||
"""
|
||||
logger.debug(f"Mapping EspoCRM → Advoware: {espo_entity.get('id')}")
|
||||
|
||||
# Bestimme ob Person oder Firma
|
||||
is_firma = bool(espo_entity.get('firmenname'))
|
||||
rechtsform = espo_entity.get('rechtsform', '')
|
||||
|
||||
# Basis-Struktur
|
||||
advo_data = {
|
||||
'rechtsform': rechtsform,
|
||||
}
|
||||
|
||||
# NAME: Person vs. Firma
|
||||
if is_firma:
|
||||
# Firma: name = firmenname
|
||||
advo_data['name'] = espo_entity.get('firmenname', '')
|
||||
advo_data['vorname'] = None
|
||||
else:
|
||||
# Person: name = lastName, vorname = firstName
|
||||
advo_data['name'] = espo_entity.get('lastName', '')
|
||||
advo_data['vorname'] = espo_entity.get('firstName', '')
|
||||
|
||||
# ANREDE
|
||||
salutation = espo_entity.get('salutationName', '')
|
||||
if salutation:
|
||||
advo_data['anrede'] = salutation
|
||||
|
||||
# GEBURTSDATUM
|
||||
date_of_birth = espo_entity.get('dateOfBirth')
|
||||
if date_of_birth:
|
||||
advo_data['geburtsdatum'] = date_of_birth
|
||||
|
||||
# KONTAKTDATEN
|
||||
# E-Mail (emailAddressData ist Array, wir nehmen Primary)
|
||||
email_data = espo_entity.get('emailAddressData')
|
||||
if email_data and isinstance(email_data, list):
|
||||
primary_email = next((e for e in email_data if e.get('primary')), None)
|
||||
if primary_email:
|
||||
advo_data['emailGesch'] = primary_email.get('emailAddress')
|
||||
elif espo_entity.get('emailAddress'):
|
||||
advo_data['emailGesch'] = espo_entity.get('emailAddress')
|
||||
|
||||
# Telefon (phoneNumberData ist Array, wir nehmen Primary)
|
||||
phone_data = espo_entity.get('phoneNumberData')
|
||||
if phone_data and isinstance(phone_data, list):
|
||||
primary_phone = next((p for p in phone_data if p.get('primary')), None)
|
||||
if primary_phone:
|
||||
phone_num = primary_phone.get('phoneNumber')
|
||||
phone_type = primary_phone.get('type', '').lower()
|
||||
|
||||
if 'mobile' in phone_type or 'mobil' in phone_type:
|
||||
advo_data['mobil'] = phone_num
|
||||
else:
|
||||
advo_data['telGesch'] = phone_num
|
||||
elif espo_entity.get('phoneNumber'):
|
||||
advo_data['telGesch'] = espo_entity.get('phoneNumber')
|
||||
|
||||
# HANDELSREGISTER (nur für Firmen)
|
||||
if is_firma:
|
||||
hr_nummer = espo_entity.get('handelsregisterNummer')
|
||||
if hr_nummer:
|
||||
advo_data['handelsRegisterNummer'] = hr_nummer
|
||||
|
||||
# DISGTYP (EspoCRM spezifisch - falls vorhanden)
|
||||
disgtyp = espo_entity.get('disgTyp')
|
||||
if disgtyp:
|
||||
advo_data['disgTyp'] = disgtyp
|
||||
|
||||
logger.debug(f"Mapped to Advoware: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# NAME: Person vs. Firma
|
||||
if is_person:
|
||||
# Person
|
||||
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
|
||||
else:
|
||||
# Firma
|
||||
espo_data['firmenname'] = advo_entity.get('name', '')
|
||||
espo_data['name'] = advo_entity.get('name', '')
|
||||
espo_data['firstName'] = None
|
||||
espo_data['lastName'] = None
|
||||
|
||||
# ANREDE
|
||||
anrede = advo_entity.get('anrede')
|
||||
if anrede:
|
||||
espo_data['salutationName'] = anrede
|
||||
|
||||
# GEBURTSDATUM
|
||||
geburtsdatum = advo_entity.get('geburtsdatum')
|
||||
if geburtsdatum:
|
||||
espo_data['dateOfBirth'] = geburtsdatum
|
||||
|
||||
# KONTAKTDATEN
|
||||
# E-Mail (emailGesch ist primary)
|
||||
email_gesch = advo_entity.get('emailGesch')
|
||||
email = advo_entity.get('email')
|
||||
|
||||
primary_email = email_gesch or email
|
||||
if primary_email:
|
||||
espo_data['emailAddress'] = primary_email
|
||||
espo_data['emailAddressData'] = [
|
||||
{
|
||||
'emailAddress': primary_email,
|
||||
'primary': True,
|
||||
'optOut': False,
|
||||
'invalid': False
|
||||
}
|
||||
]
|
||||
|
||||
# Telefon (telGesch ist primary, mobil als secondary)
|
||||
tel_gesch = advo_entity.get('telGesch')
|
||||
tel_privat = advo_entity.get('telPrivat')
|
||||
mobil = advo_entity.get('mobil')
|
||||
|
||||
phone_data = []
|
||||
|
||||
# Primary: telGesch oder telPrivat
|
||||
primary_tel = tel_gesch or tel_privat
|
||||
if primary_tel:
|
||||
espo_data['phoneNumber'] = primary_tel
|
||||
phone_data.append({
|
||||
'phoneNumber': primary_tel,
|
||||
'primary': True,
|
||||
'type': 'Office' if tel_gesch else 'Home'
|
||||
})
|
||||
|
||||
# Secondary: mobil
|
||||
if mobil and mobil != primary_tel:
|
||||
phone_data.append({
|
||||
'phoneNumber': mobil,
|
||||
'primary': False,
|
||||
'type': 'Mobile'
|
||||
})
|
||||
|
||||
if phone_data:
|
||||
espo_data['phoneNumberData'] = phone_data
|
||||
|
||||
# HANDELSREGISTER (nur für Firmen)
|
||||
if not is_person:
|
||||
hr_nummer = advo_entity.get('handelsRegisterNummer')
|
||||
if hr_nummer:
|
||||
espo_data['handelsregisterNummer'] = hr_nummer
|
||||
|
||||
# DISGTYP
|
||||
disgtyp = advo_entity.get('disgTyp')
|
||||
if disgtyp:
|
||||
espo_data['disgTyp'] = disgtyp
|
||||
|
||||
logger.debug(f"Mapped to EspoCRM: name={espo_data.get('name')}")
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
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