Add requests dependency to project
- Included 'requests' in the dependencies list of uv.lock. - Specified minimum version of 'requests' as 2.32.0 in requires-dist.
This commit is contained in:
333
services/kommunikation_mapper.py
Normal file
333
services/kommunikation_mapper.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Kommunikation Mapper: Advoware ↔ EspoCRM
|
||||
|
||||
Mapping-Strategie:
|
||||
- Marker in Advoware bemerkung: [ESPOCRM:hash:kommKz]
|
||||
- Typ-Erkennung: Marker > Top-Level > Wert > Default
|
||||
- Bidirektional mit Slot-Wiederverwendung
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import re
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
|
||||
# kommKz Enum
|
||||
KOMMKZ_TEL_GESCH = 1
|
||||
KOMMKZ_FAX_GESCH = 2
|
||||
KOMMKZ_MOBIL = 3
|
||||
KOMMKZ_MAIL_GESCH = 4
|
||||
KOMMKZ_INTERNET = 5
|
||||
KOMMKZ_TEL_PRIVAT = 6
|
||||
KOMMKZ_FAX_PRIVAT = 7
|
||||
KOMMKZ_MAIL_PRIVAT = 8
|
||||
KOMMKZ_AUTO_TELEFON = 9
|
||||
KOMMKZ_SONSTIGE = 10
|
||||
KOMMKZ_EPOST = 11
|
||||
KOMMKZ_BEA = 12
|
||||
|
||||
# EspoCRM phone type mapping
|
||||
KOMMKZ_TO_PHONE_TYPE = {
|
||||
KOMMKZ_TEL_GESCH: 'Office',
|
||||
KOMMKZ_FAX_GESCH: 'Fax',
|
||||
KOMMKZ_MOBIL: 'Mobile',
|
||||
KOMMKZ_TEL_PRIVAT: 'Home',
|
||||
KOMMKZ_FAX_PRIVAT: 'Fax',
|
||||
KOMMKZ_AUTO_TELEFON: 'Mobile',
|
||||
KOMMKZ_SONSTIGE: 'Other',
|
||||
}
|
||||
|
||||
# Reverse mapping: EspoCRM phone type to kommKz
|
||||
PHONE_TYPE_TO_KOMMKZ = {
|
||||
'Office': KOMMKZ_TEL_GESCH,
|
||||
'Fax': KOMMKZ_FAX_GESCH,
|
||||
'Mobile': KOMMKZ_MOBIL,
|
||||
'Home': KOMMKZ_TEL_PRIVAT,
|
||||
'Other': KOMMKZ_SONSTIGE,
|
||||
}
|
||||
|
||||
# Email kommKz values
|
||||
EMAIL_KOMMKZ = [KOMMKZ_MAIL_GESCH, KOMMKZ_MAIL_PRIVAT, KOMMKZ_EPOST, KOMMKZ_BEA]
|
||||
|
||||
# Phone kommKz values
|
||||
PHONE_KOMMKZ = [KOMMKZ_TEL_GESCH, KOMMKZ_FAX_GESCH, KOMMKZ_MOBIL,
|
||||
KOMMKZ_TEL_PRIVAT, KOMMKZ_FAX_PRIVAT, KOMMKZ_AUTO_TELEFON, KOMMKZ_SONSTIGE]
|
||||
|
||||
|
||||
def encode_value(value: str) -> str:
|
||||
"""Encodiert Wert mit Base64 (URL-safe) für Marker"""
|
||||
return base64.urlsafe_b64encode(value.encode('utf-8')).decode('ascii').rstrip('=')
|
||||
|
||||
|
||||
def decode_value(encoded: str) -> str:
|
||||
"""Decodiert Base64-kodierten Wert aus Marker"""
|
||||
# Add padding if needed
|
||||
padding = 4 - (len(encoded) % 4)
|
||||
if padding != 4:
|
||||
encoded += '=' * padding
|
||||
return base64.urlsafe_b64decode(encoded.encode('ascii')).decode('utf-8')
|
||||
|
||||
|
||||
def calculate_hash(value: str) -> str:
|
||||
"""Legacy: Hash-Berechnung (für Rückwärtskompatibilität mit alten Markern)"""
|
||||
return hashlib.sha256(value.encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def parse_marker(bemerkung: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Parse ESPOCRM-Marker aus bemerkung
|
||||
|
||||
Returns:
|
||||
{'synced_value': '...', 'kommKz': 4, 'is_slot': False, 'user_text': '...'}
|
||||
oder None (synced_value ist decoded, nicht base64)
|
||||
"""
|
||||
if not bemerkung:
|
||||
return None
|
||||
|
||||
# Match SLOT: [ESPOCRM-SLOT:kommKz]
|
||||
slot_pattern = r'\[ESPOCRM-SLOT:(\d+)\](.*)'
|
||||
slot_match = re.match(slot_pattern, bemerkung)
|
||||
|
||||
if slot_match:
|
||||
return {
|
||||
'synced_value': '',
|
||||
'kommKz': int(slot_match.group(1)),
|
||||
'is_slot': True,
|
||||
'user_text': slot_match.group(2).strip()
|
||||
}
|
||||
|
||||
# Match: [ESPOCRM:base64_value:kommKz]
|
||||
pattern = r'\[ESPOCRM:([^:]+):(\d+)\](.*)'
|
||||
match = re.match(pattern, bemerkung)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
encoded_value = match.group(1)
|
||||
|
||||
# Decode Base64 value
|
||||
try:
|
||||
synced_value = decode_value(encoded_value)
|
||||
except Exception as e:
|
||||
# Fallback: Könnte alter Hash-Marker sein
|
||||
synced_value = encoded_value
|
||||
|
||||
return {
|
||||
'synced_value': synced_value,
|
||||
'kommKz': int(match.group(2)),
|
||||
'is_slot': False,
|
||||
'user_text': match.group(3).strip()
|
||||
}
|
||||
|
||||
|
||||
def create_marker(value: str, kommkz: int, user_text: str = '') -> str:
|
||||
"""Erstellt ESPOCRM-Marker mit Base64-encodiertem Wert"""
|
||||
encoded = encode_value(value)
|
||||
suffix = f" {user_text}" if user_text else ""
|
||||
return f"[ESPOCRM:{encoded}:{kommkz}]{suffix}"
|
||||
|
||||
|
||||
def create_slot_marker(kommkz: int) -> str:
|
||||
"""Erstellt Slot-Marker für gelöschte Einträge"""
|
||||
return f"[ESPOCRM-SLOT:{kommkz}]"
|
||||
|
||||
|
||||
def detect_kommkz(value: str, beteiligte: Optional[Dict] = None,
|
||||
bemerkung: Optional[str] = None,
|
||||
espo_type: Optional[str] = None) -> int:
|
||||
"""
|
||||
Erkenne kommKz mit mehrstufiger Strategie
|
||||
|
||||
Priorität:
|
||||
1. Aus bemerkung-Marker (wenn vorhanden)
|
||||
2. Aus EspoCRM type (wenn von EspoCRM kommend)
|
||||
3. Aus Top-Level Feldern in beteiligte
|
||||
4. Aus Wert (Email vs. Phone)
|
||||
5. Default
|
||||
|
||||
Args:
|
||||
espo_type: EspoCRM phone type ('Office', 'Mobile', 'Fax', etc.) oder 'email'
|
||||
"""
|
||||
# 1. Aus Marker
|
||||
if bemerkung:
|
||||
marker = parse_marker(bemerkung)
|
||||
if marker:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[KOMMKZ] Detected from marker: kommKz={marker['kommKz']}")
|
||||
return marker['kommKz']
|
||||
|
||||
# 2. Aus EspoCRM type (für EspoCRM->Advoware Sync)
|
||||
if espo_type:
|
||||
if espo_type == 'email':
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[KOMMKZ] Detected from espo_type 'email': kommKz={KOMMKZ_MAIL_GESCH}")
|
||||
return KOMMKZ_MAIL_GESCH
|
||||
elif espo_type in PHONE_TYPE_TO_KOMMKZ:
|
||||
kommkz = PHONE_TYPE_TO_KOMMKZ[espo_type]
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[KOMMKZ] Detected from espo_type '{espo_type}': kommKz={kommkz}")
|
||||
return kommkz
|
||||
|
||||
# 3. Aus Top-Level Feldern (für genau EINEN Eintrag pro Typ)
|
||||
if beteiligte:
|
||||
top_level_map = {
|
||||
'telGesch': KOMMKZ_TEL_GESCH,
|
||||
'faxGesch': KOMMKZ_FAX_GESCH,
|
||||
'mobil': KOMMKZ_MOBIL,
|
||||
'emailGesch': KOMMKZ_MAIL_GESCH,
|
||||
'email': KOMMKZ_MAIL_GESCH,
|
||||
'internet': KOMMKZ_INTERNET,
|
||||
'telPrivat': KOMMKZ_TEL_PRIVAT,
|
||||
'faxPrivat': KOMMKZ_FAX_PRIVAT,
|
||||
'autotelefon': KOMMKZ_AUTO_TELEFON,
|
||||
'ePost': KOMMKZ_EPOST,
|
||||
'bea': KOMMKZ_BEA,
|
||||
}
|
||||
|
||||
for field, kommkz in top_level_map.items():
|
||||
if beteiligte.get(field) == value:
|
||||
return kommkz
|
||||
|
||||
# 3. Aus Wert (Email vs. Phone)
|
||||
if '@' in value:
|
||||
return KOMMKZ_MAIL_GESCH # Default Email
|
||||
elif value.strip():
|
||||
return KOMMKZ_TEL_GESCH # Default Phone
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_email_type(kommkz: int) -> bool:
|
||||
"""Prüft ob kommKz ein Email-Typ ist"""
|
||||
return kommkz in EMAIL_KOMMKZ
|
||||
|
||||
|
||||
def is_phone_type(kommkz: int) -> bool:
|
||||
"""Prüft ob kommKz ein Telefon-Typ ist"""
|
||||
return kommkz in PHONE_KOMMKZ
|
||||
|
||||
|
||||
def advoware_to_espocrm_email(advo_komm: Dict, beteiligte: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Konvertiert Advoware Kommunikation zu EspoCRM emailAddressData
|
||||
|
||||
Args:
|
||||
advo_komm: Advoware Kommunikation
|
||||
beteiligte: Vollständiger Beteiligte (für Top-Level Felder)
|
||||
|
||||
Returns:
|
||||
EspoCRM emailAddressData Element
|
||||
"""
|
||||
value = (advo_komm.get('tlf') or '').strip()
|
||||
|
||||
return {
|
||||
'emailAddress': value,
|
||||
'lower': value.lower(),
|
||||
'primary': advo_komm.get('online', False),
|
||||
'optOut': False,
|
||||
'invalid': False
|
||||
}
|
||||
|
||||
|
||||
def advoware_to_espocrm_phone(advo_komm: Dict, beteiligte: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Konvertiert Advoware Kommunikation zu EspoCRM phoneNumberData
|
||||
|
||||
Args:
|
||||
advo_komm: Advoware Kommunikation
|
||||
beteiligte: Vollständiger Beteiligte (für Top-Level Felder)
|
||||
|
||||
Returns:
|
||||
EspoCRM phoneNumberData Element
|
||||
"""
|
||||
value = (advo_komm.get('tlf') or '').strip()
|
||||
bemerkung = advo_komm.get('bemerkung')
|
||||
|
||||
# Erkenne kommKz
|
||||
kommkz = detect_kommkz(value, beteiligte, bemerkung)
|
||||
|
||||
# Mappe zu EspoCRM type
|
||||
phone_type = KOMMKZ_TO_PHONE_TYPE.get(kommkz, 'Other')
|
||||
|
||||
return {
|
||||
'phoneNumber': value,
|
||||
'type': phone_type,
|
||||
'primary': advo_komm.get('online', False),
|
||||
'optOut': False,
|
||||
'invalid': False
|
||||
}
|
||||
|
||||
|
||||
def find_matching_advoware(espo_value: str, advo_kommunikationen: List[Dict]) -> Optional[Dict]:
|
||||
"""
|
||||
Findet passende Advoware-Kommunikation für EspoCRM Wert
|
||||
|
||||
Matching via synced_value in bemerkung-Marker
|
||||
"""
|
||||
for k in advo_kommunikationen:
|
||||
bemerkung = k.get('bemerkung') or ''
|
||||
marker = parse_marker(bemerkung)
|
||||
|
||||
if marker and not marker['is_slot'] and marker['synced_value'] == espo_value:
|
||||
return k
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_empty_slot(kommkz: int, advo_kommunikationen: List[Dict]) -> Optional[Dict]:
|
||||
"""
|
||||
Findet leeren Slot mit passendem kommKz
|
||||
|
||||
Leere Slots haben: tlf='' (WIRKLICH leer!) UND bemerkung='[ESPOCRM-SLOT:kommKz]'
|
||||
|
||||
WICHTIG: User könnte Wert in einen Slot eingetragen haben → dann ist es KEIN Empty Slot mehr!
|
||||
"""
|
||||
for k in advo_kommunikationen:
|
||||
tlf = (k.get('tlf') or '').strip()
|
||||
bemerkung = k.get('bemerkung') or ''
|
||||
|
||||
# Muss BEIDES erfüllen: tlf leer UND Slot-Marker
|
||||
if not tlf:
|
||||
marker = parse_marker(bemerkung)
|
||||
if marker and marker.get('is_slot') and marker.get('kommKz') == kommkz:
|
||||
return k
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def should_sync_to_espocrm(advo_komm: Dict) -> bool:
|
||||
"""
|
||||
Prüft ob Advoware-Kommunikation zu EspoCRM synchronisiert werden soll
|
||||
|
||||
Nur wenn:
|
||||
- Wert vorhanden (tlf ist nicht leer)
|
||||
|
||||
WICHTIG: Ein Slot-Marker allein bedeutet NICHT "nicht sync-relevant"!
|
||||
User könnte einen Wert in einen Slot eingetragen haben.
|
||||
"""
|
||||
tlf = (advo_komm.get('tlf') or '').strip()
|
||||
|
||||
# Nur relevante Kriterium: Hat tlf einen Wert?
|
||||
return bool(tlf)
|
||||
|
||||
|
||||
def get_user_bemerkung(advo_komm: Dict) -> str:
|
||||
"""Extrahiert User-Bemerkung (ohne Marker)"""
|
||||
bemerkung = advo_komm.get('bemerkung') or ''
|
||||
marker = parse_marker(bemerkung)
|
||||
|
||||
if marker:
|
||||
return marker['user_text']
|
||||
|
||||
return bemerkung
|
||||
|
||||
|
||||
def set_user_bemerkung(marker: str, user_text: str) -> str:
|
||||
"""Fügt User-Bemerkung zu Marker hinzu"""
|
||||
if user_text:
|
||||
return f"{marker} {user_text}"
|
||||
return marker
|
||||
Reference in New Issue
Block a user