feat: Implement entity comparison logic for improved sync detection between EspoCRM and Advoware
This commit is contained in:
@@ -217,6 +217,65 @@ class BeteiligteSync:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def compare_entities(
|
||||||
|
self,
|
||||||
|
espo_entity: Dict[str, Any],
|
||||||
|
advo_entity: Dict[str, Any]
|
||||||
|
) -> TimestampResult:
|
||||||
|
"""
|
||||||
|
Vergleicht Änderungen zwischen EspoCRM und Advoware
|
||||||
|
|
||||||
|
PRIMÄR: rowId-Vergleich (Advoware rowId ändert sich bei jedem Update - SEHR zuverlässig!)
|
||||||
|
FALLBACK: Timestamp-Vergleich (wenn rowId nicht verfügbar)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
espo_entity: EspoCRM CBeteiligte
|
||||||
|
advo_entity: Advoware Beteiligte
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
"espocrm_newer": EspoCRM wurde geändert
|
||||||
|
"advoware_newer": Advoware wurde geändert
|
||||||
|
"conflict": Beide wurden geändert
|
||||||
|
"no_change": Keine Änderungen
|
||||||
|
"""
|
||||||
|
# PRIMÄR: rowId-basierte Änderungserkennung (zuverlässiger!)
|
||||||
|
espo_rowid = espo_entity.get('advowareRowId')
|
||||||
|
advo_rowid = advo_entity.get('rowId')
|
||||||
|
|
||||||
|
if espo_rowid and advo_rowid:
|
||||||
|
if espo_rowid != advo_rowid:
|
||||||
|
# rowId unterschiedlich → Advoware wurde geändert
|
||||||
|
self._log(f"Advoware rowId geändert: {espo_rowid[:20]}... → {advo_rowid[:20]}...")
|
||||||
|
return 'advoware_newer'
|
||||||
|
else:
|
||||||
|
# rowId gleich → keine Änderung in Advoware
|
||||||
|
# Prüfe ob EspoCRM geändert wurde (via modifiedAt)
|
||||||
|
espo_modified = espo_entity.get('modifiedAt')
|
||||||
|
last_sync = espo_entity.get('advowareLastSync')
|
||||||
|
|
||||||
|
if espo_modified and last_sync:
|
||||||
|
try:
|
||||||
|
espo_ts = self.parse_timestamp(espo_modified)
|
||||||
|
sync_ts = self.parse_timestamp(last_sync)
|
||||||
|
|
||||||
|
if espo_ts and sync_ts and espo_ts > sync_ts:
|
||||||
|
self._log(f"EspoCRM neuer (rowId gleich, aber modifiedAt > lastSync)")
|
||||||
|
return 'espocrm_newer'
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"Timestamp-Parse-Fehler: {e}", level='debug')
|
||||||
|
|
||||||
|
# Keine Änderungen
|
||||||
|
self._log("Keine Änderungen (rowId identisch)")
|
||||||
|
return 'no_change'
|
||||||
|
|
||||||
|
# FALLBACK: Timestamp-Vergleich (wenn rowId nicht verfügbar)
|
||||||
|
self._log("rowId nicht verfügbar, fallback auf Timestamp-Vergleich", level='debug')
|
||||||
|
return self.compare_timestamps(
|
||||||
|
espo_entity.get('modifiedAt'),
|
||||||
|
advo_entity.get('geaendertAm'),
|
||||||
|
espo_entity.get('advowareLastSync')
|
||||||
|
)
|
||||||
|
|
||||||
def compare_timestamps(
|
def compare_timestamps(
|
||||||
self,
|
self,
|
||||||
espo_modified_at: Any,
|
espo_modified_at: Any,
|
||||||
@@ -224,7 +283,7 @@ class BeteiligteSync:
|
|||||||
last_sync_ts: Any
|
last_sync_ts: Any
|
||||||
) -> TimestampResult:
|
) -> TimestampResult:
|
||||||
"""
|
"""
|
||||||
Vergleicht Timestamps und bestimmt Sync-Richtung
|
Vergleicht Timestamps und bestimmt Sync-Richtung (FALLBACK wenn rowId nicht verfügbar)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
espo_modified_at: EspoCRM modifiedAt
|
espo_modified_at: EspoCRM modifiedAt
|
||||||
@@ -412,7 +471,8 @@ class BeteiligteSync:
|
|||||||
entity_id: str,
|
entity_id: str,
|
||||||
espo_entity: Dict[str, Any],
|
espo_entity: Dict[str, Any],
|
||||||
advo_entity: Dict[str, Any],
|
advo_entity: Dict[str, Any],
|
||||||
conflict_details: str
|
conflict_details: str,
|
||||||
|
extra_fields: Optional[Dict[str, Any]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Löst Konflikt auf: EspoCRM wins (überschreibt Advoware)
|
Löst Konflikt auf: EspoCRM wins (überschreibt Advoware)
|
||||||
@@ -422,13 +482,26 @@ class BeteiligteSync:
|
|||||||
espo_entity: EspoCRM Entity-Daten
|
espo_entity: EspoCRM Entity-Daten
|
||||||
advo_entity: Advoware Entity-Daten
|
advo_entity: Advoware Entity-Daten
|
||||||
conflict_details: Details zum Konflikt
|
conflict_details: Details zum Konflikt
|
||||||
|
extra_fields: Zusätzliche Felder (z.B. advowareRowId)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
now = datetime.now(pytz.UTC).isoformat()
|
# EspoCRM datetime format
|
||||||
|
now_utc = datetime.now(pytz.UTC)
|
||||||
|
espo_datetime = now_utc.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
# Markiere als gelöst mit Konflikt-Info
|
# Markiere als gelöst mit Konflikt-Info
|
||||||
await self.espocrm.update_entity('CBeteiligte', entity_id, {
|
update_data = {
|
||||||
'syncStatus': 'clean', # Gelöst!
|
'syncStatus': 'clean', # Gelöst!
|
||||||
|
'advowareLastSync': espo_datetime,
|
||||||
|
'syncErrorMessage': f'Konflikt: {conflict_details}',
|
||||||
|
'syncRetryCount': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge extra fields (z.B. advowareRowId)
|
||||||
|
if extra_fields:
|
||||||
|
update_data.update(extra_fields)
|
||||||
|
|
||||||
|
await self.espocrm.update_entity('CBeteiligte', entity_id, update_data)
|
||||||
'advowareLastSync': now,
|
'advowareLastSync': now,
|
||||||
'syncErrorMessage': f"Konflikt am {now}: {conflict_details}. EspoCRM hat gewonnen.",
|
'syncErrorMessage': f"Konflikt am {now}: {conflict_details}. EspoCRM hat gewonnen.",
|
||||||
'syncRetryCount': 0
|
'syncRetryCount': 0
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ class BeteiligteMapper:
|
|||||||
if hr_nummer:
|
if hr_nummer:
|
||||||
advo_data['handelsRegisterNummer'] = hr_nummer
|
advo_data['handelsRegisterNummer'] = hr_nummer
|
||||||
|
|
||||||
|
# Registergericht
|
||||||
|
registergericht = espo_entity.get('registergericht')
|
||||||
|
if registergericht:
|
||||||
|
advo_data['registergericht'] = registergericht
|
||||||
|
|
||||||
# TODO: Weitere Stammdaten-Felder hier ergänzen (Steuernummer, etc.)
|
# TODO: Weitere Stammdaten-Felder hier ergänzen (Steuernummer, etc.)
|
||||||
|
|
||||||
logger.debug(f"Mapped to Advoware STAMMDATEN: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}, rechtsform={rechtsform}")
|
logger.debug(f"Mapped to Advoware STAMMDATEN: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}, rechtsform={rechtsform}")
|
||||||
@@ -92,6 +97,7 @@ class BeteiligteMapper:
|
|||||||
espo_data = {
|
espo_data = {
|
||||||
'rechtsform': advo_entity.get('rechtsform', ''),
|
'rechtsform': advo_entity.get('rechtsform', ''),
|
||||||
'betnr': advo_entity.get('betNr'), # Link zu Advoware
|
'betnr': advo_entity.get('betNr'), # Link zu Advoware
|
||||||
|
'advowareRowId': advo_entity.get('rowId'), # Änderungserkennung
|
||||||
}
|
}
|
||||||
|
|
||||||
# NAME: Person vs. Firma
|
# NAME: Person vs. Firma
|
||||||
@@ -124,6 +130,11 @@ class BeteiligteMapper:
|
|||||||
if hr_nummer:
|
if hr_nummer:
|
||||||
espo_data['handelsregisterNummer'] = hr_nummer
|
espo_data['handelsregisterNummer'] = hr_nummer
|
||||||
|
|
||||||
|
# Registergericht
|
||||||
|
registergericht = advo_entity.get('registergericht')
|
||||||
|
if registergericht:
|
||||||
|
espo_data['registergericht'] = registergericht
|
||||||
|
|
||||||
# TODO: Weitere Stammdaten-Felder hier ergänzen
|
# TODO: Weitere Stammdaten-Felder hier ergänzen
|
||||||
# HINWEIS: Kontaktdaten (Telefon, Email, Fax) werden über separate Endpoints gesynct
|
# HINWEIS: Kontaktdaten (Telefon, Email, Fax) werden über separate Endpoints gesynct
|
||||||
|
|
||||||
@@ -153,7 +164,8 @@ class BeteiligteMapper:
|
|||||||
'name', 'firstName', 'lastName', 'firmenname',
|
'name', 'firstName', 'lastName', 'firmenname',
|
||||||
'emailAddress', 'phoneNumber',
|
'emailAddress', 'phoneNumber',
|
||||||
'dateOfBirth', 'rechtsform',
|
'dateOfBirth', 'rechtsform',
|
||||||
'handelsregisterNummer'
|
'handelsregisterNummer', 'handelsregisterArt', 'registergericht',
|
||||||
|
'betnr', 'advowareRowId'
|
||||||
]
|
]
|
||||||
|
|
||||||
for field in compare_fields:
|
for field in compare_fields:
|
||||||
|
|||||||
@@ -184,14 +184,10 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
|
|
||||||
context.logger.info(f"📥 Von Advoware geladen: {advo_entity.get('name')}")
|
context.logger.info(f"📥 Von Advoware geladen: {advo_entity.get('name')}")
|
||||||
|
|
||||||
# TIMESTAMP-VERGLEICH
|
# ÄNDERUNGSERKENNUNG (Primary: rowId, Fallback: Timestamps)
|
||||||
comparison = sync_utils.compare_timestamps(
|
comparison = sync_utils.compare_entities(espo_entity, advo_entity)
|
||||||
espo_entity.get('modifiedAt'),
|
|
||||||
advo_entity.get('geaendertAm'),
|
|
||||||
espo_entity.get('advowareLastSync')
|
|
||||||
)
|
|
||||||
|
|
||||||
context.logger.info(f"⏱️ Timestamp-Vergleich: {comparison}")
|
context.logger.info(f"⏱️ Vergleich: {comparison}")
|
||||||
|
|
||||||
# SPECIAL: Wenn LastSync null → immer von EspoCRM syncen (initial sync)
|
# SPECIAL: Wenn LastSync null → immer von EspoCRM syncen (initial sync)
|
||||||
if not espo_entity.get('advowareLastSync'):
|
if not espo_entity.get('advowareLastSync'):
|
||||||
@@ -206,7 +202,12 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
data=merged_data
|
data=merged_data
|
||||||
)
|
)
|
||||||
|
|
||||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
# Speichere rowId für zukünftige Vergleiche
|
||||||
|
await sync_utils.release_sync_lock(
|
||||||
|
entity_id,
|
||||||
|
'clean',
|
||||||
|
extra_fields={'advowareRowId': advo_entity.get('rowId')}
|
||||||
|
)
|
||||||
context.logger.info(f"✅ Advoware aktualisiert (initial sync)")
|
context.logger.info(f"✅ Advoware aktualisiert (initial sync)")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -229,7 +230,16 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
data=merged_data
|
data=merged_data
|
||||||
)
|
)
|
||||||
|
|
||||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
# Hole aktualisierte Entity um neue rowId zu bekommen
|
||||||
|
updated_advo = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}', method='GET')
|
||||||
|
if isinstance(updated_advo, list):
|
||||||
|
updated_advo = updated_advo[0]
|
||||||
|
|
||||||
|
await sync_utils.release_sync_lock(
|
||||||
|
entity_id,
|
||||||
|
'clean',
|
||||||
|
extra_fields={'advowareRowId': updated_advo.get('rowId')}
|
||||||
|
)
|
||||||
context.logger.info(f"✅ Advoware aktualisiert")
|
context.logger.info(f"✅ Advoware aktualisiert")
|
||||||
|
|
||||||
# ADVOWARE NEUER → Update EspoCRM
|
# ADVOWARE NEUER → Update EspoCRM
|
||||||
@@ -239,7 +249,11 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
espo_data = mapper.map_advoware_to_cbeteiligte(advo_entity)
|
espo_data = mapper.map_advoware_to_cbeteiligte(advo_entity)
|
||||||
|
|
||||||
await espocrm.update_entity('CBeteiligte', entity_id, espo_data)
|
await espocrm.update_entity('CBeteiligte', entity_id, espo_data)
|
||||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
await sync_utils.release_sync_lock(
|
||||||
|
entity_id,
|
||||||
|
'clean',
|
||||||
|
extra_fields={'advowareRowId': advo_entity.get('rowId')}
|
||||||
|
)
|
||||||
context.logger.info(f"✅ EspoCRM aktualisiert")
|
context.logger.info(f"✅ EspoCRM aktualisiert")
|
||||||
|
|
||||||
# KONFLIKT → EspoCRM WINS
|
# KONFLIKT → EspoCRM WINS
|
||||||
@@ -255,6 +269,11 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
data=merged_data
|
data=merged_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Hole aktualisierte Entity um neue rowId zu bekommen
|
||||||
|
updated_advo = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}', method='GET')
|
||||||
|
if isinstance(updated_advo, list):
|
||||||
|
updated_advo = updated_advo[0]
|
||||||
|
|
||||||
conflict_msg = (
|
conflict_msg = (
|
||||||
f"EspoCRM: {espo_entity.get('modifiedAt')}, "
|
f"EspoCRM: {espo_entity.get('modifiedAt')}, "
|
||||||
f"Advoware: {advo_entity.get('geaendertAm')}. "
|
f"Advoware: {advo_entity.get('geaendertAm')}. "
|
||||||
@@ -265,7 +284,8 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
|||||||
entity_id,
|
entity_id,
|
||||||
espo_entity,
|
espo_entity,
|
||||||
advo_entity,
|
advo_entity,
|
||||||
conflict_msg
|
conflict_msg,
|
||||||
|
extra_fields={'advowareRowId': updated_advo.get('rowId')}
|
||||||
)
|
)
|
||||||
|
|
||||||
context.logger.info(f"✅ Konflikt gelöst: EspoCRM → Advoware")
|
context.logger.info(f"✅ Konflikt gelöst: EspoCRM → Advoware")
|
||||||
|
|||||||
Reference in New Issue
Block a user