import os import time import re import gspread import wikipedia import requests import openai from bs4 import BeautifulSoup from oauth2client.service_account import ServiceAccountCredentials from datetime import datetime from difflib import SequenceMatcher import unicodedata import csv # ==================== KONFIGURATION ==================== class Config: VERSION = "v1.3.8" # v1.3.8: Neuer Modus 5 als Schreibtest für das Contacts-Sheet; restliche Funktionen unverändert. LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" MAX_RETRIES = 3 RETRY_DELAY = 5 LOG_CSV = "gpt_antworten_log.csv" SIMILARITY_THRESHOLD = 0.65 DEBUG = True WIKIPEDIA_SEARCH_RESULTS = 5 HTML_PARSER = "html.parser" # ==================== RETRY-DECORATOR ==================== def retry_on_failure(func): def wrapper(*args, **kwargs): for attempt in range(Config.MAX_RETRIES): try: return func(*args, **kwargs) except Exception as e: print(f"⚠️ Fehler bei {func.__name__} (Versuch {attempt+1}): {str(e)[:100]}") time.sleep(Config.RETRY_DELAY) return None return wrapper # ==================== LOGGING & HELPER FUNCTIONS ==================== if not os.path.exists("Log"): os.makedirs("Log") LOG_FILE = os.path.join("Log", f"{datetime.now().strftime('%d-%m-%Y_%H-%M')}_{Config.VERSION.replace('.', '')}.txt") def debug_print(message): if Config.DEBUG: print(f"[DEBUG] {message}") try: with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"[DEBUG] {message}\n") except Exception as e: print(f"[DEBUG] Log-Schreibfehler: {e}") def clean_text(text): if not text: return "k.A." text = unicodedata.normalize("NFKC", str(text)) text = re.sub(r'\[\d+\]', '', text) text = re.sub(r'\s+', ' ', text).strip() return text if text else "k.A." def normalize_company_name(name): if not name: return "" forms = [ r'gmbh', r'g\.m\.b\.h\.', r'ug', r'u\.g\.', r'ug \(haftungsbeschränkt\)', r'u\.g\. \(haftungsbeschränkt\)', r'ag', r'a\.g\.', r'ohg', r'o\.h\.g\.', r'kg', r'k\.g\.', r'gmbh & co\.?\s*kg', r'g\.m\.b\.h\. & co\.?\s*k\.g\.', r'ag & co\.?\s*kg', r'a\.g\. & co\.?\s*k\.g\.', r'e\.k\.', r'e\.kfm\.', r'e\.kfr\.', r'ltd\.', r'ltd & co\.?\s*kg', r's\.a r\.l\.', r'stiftung', r'genossenschaft', r'ggmbh', r'gug', r'partg', r'partgmbb', r'kgaa', r'se', r'og', r'o\.g\.', r'e\.u\.', r'ges\.n\.b\.r\.', r'genmbh', r'verein', r'kollektivgesellschaft', r'kommanditgesellschaft', r'einzelfirma', r'sàrl', r'sa', r'sagl', r'gmbh & co\.?\s*ohg', r'ag & co\.?\s*ohg', r'gmbh & co\.?\s*kgaa', r'ag & co\.?\s*kgaa', r's\.a\.', r's\.p\.a\.', r'b\.v\.', r'n\.v\.' ] pattern = r'\b(' + '|'.join(forms) + r')\b' normalized = re.sub(pattern, '', name, flags=re.IGNORECASE) normalized = re.sub(r'[\-–]', ' ', normalized) normalized = re.sub(r'\s+', ' ', normalized).strip() return normalized.lower() def extract_numeric_value(raw_value, is_umsatz=False): raw_value = raw_value.strip() if not raw_value: return "k.A." raw_value = re.sub(r'\b(ca\.?|circa|über)\b', '', raw_value, flags=re.IGNORECASE) raw = raw_value.lower().replace("\xa0", " ") match = re.search(r'([\d.,]+)', raw, flags=re.UNICODE) if not match or not match.group(1).strip(): debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'") return "k.A." num_str = match.group(1) if ',' in num_str: num_str = num_str.replace('.', '').replace(',', '.') try: num = float(num_str) except Exception as e: debug_print(f"Fehler bei der Umwandlung von '{num_str}' (Rohtext: '{raw_value}'): {e}") return raw_value else: num_str = num_str.replace(' ', '').replace('.', '') try: num = float(num_str) except Exception as e: debug_print(f"Fehler bei der Umwandlung von '{num_str}' (Rohtext: '{raw_value}'): {e}") return raw_value if is_umsatz: if "mrd" in raw or "milliarden" in raw: num *= 1000 elif "mio" in raw or "millionen" in raw: pass else: num /= 1e6 return str(int(round(num))) else: return str(int(round(num))) def compare_umsatz_values(crm, wiki): debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'") try: crm_val = float(crm) wiki_val = float(wiki) except Exception as e: debug_print(f"Fehler beim Umwandeln der Werte: CRM='{crm}', Wiki='{wiki}': {e}") return "Daten unvollständig" if crm_val == 0: return "CRM Umsatz 0" diff = abs(crm_val - wiki_val) / crm_val if diff < 0.1: return "OK" else: diff_mio = abs(crm_val - wiki_val) return f"Abweichung: {int(round(diff_mio))} Mio €" def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des API-Tokens: {e}") return "k.A." openai.api_key = api_key prompt = ( f"Bitte schätze den Umsatz in Mio. Euro für das Unternehmen '{company_name}'. " f"Die Wikipedia-Daten zeigen: '{wiki_umsatz}'. " "Antworte nur mit der Zahl." ) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.0 ) result = response.choices[0].message.content.strip() debug_print(f"ChatGPT Umsatzschätzung: '{result}'") try: value = float(result.replace(',', '.')) return str(int(round(value))) except Exception as conv_e: debug_print(f"Fehler bei der Verarbeitung der Umsatzschätzung '{result}': {conv_e}") return result except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für Umsatzschätzung: {e}") return "k.A." def validate_article_with_chatgpt(crm_data, wiki_data): crm_headers = "Firmenname;Website;Ort;Beschreibung;Aktuelle Branche;Beschreibung Branche extern;Anzahl Techniker;Umsatz (CRM);Anzahl Mitarbeiter (CRM)" wiki_headers = "Wikipedia URL;Wikipedia Absatz;Wikipedia Branche;Wikipedia Umsatz;Wikipedia Mitarbeiter;Wikipedia Kategorien" prompt_text = ( "Bitte überprüfe, ob die folgenden beiden Datensätze grundsätzlich zum gleichen Unternehmen gehören. " "Berücksichtige dabei, dass leichte Abweichungen in Firmennamen (z. B. unterschiedliche Schreibweisen, Mutter-Tochter-Beziehungen) " "oder im Ort (z. B. 'Oberndorf' vs. 'Oberndorf/Neckar') tolerierbar sind. " "Vergleiche insbesondere den Firmennamen, den Ort und die Branche. Unterschiede im Umsatz können bis zu 10% abweichen. " "Wenn die Daten im Wesentlichen übereinstimmen, antworte ausschließlich mit 'OK'. " "Falls nicht, nenne bitte den wichtigsten Grund und eine kurze Begründung, warum die Abweichung plausibel sein könnte.\n\n" f"CRM-Daten:\n{crm_headers}\n{crm_data}\n\n" f"Wikipedia-Daten:\n{wiki_headers}\n{wiki_data}\n\n" "Antwort: " ) try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des API-Tokens: {e}") return "k.A." openai.api_key = api_key try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "system", "content": prompt_text}], temperature=0.0 ) result = response.choices[0].message.content.strip() debug_print(f"Validierungsantwort ChatGPT: '{result}'") return result except Exception as e: debug_print(f"Fehler beim Validierungs-API-Aufruf: {e}") return "k.A." def evaluate_fsm_suitability(company_name, company_data): try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des API-Tokens (FSM): {e}") return {"suitability": "k.A.", "justification": "k.A."} openai.api_key = api_key prompt = ( f"Bitte bewerte, ob das Unternehmen '{company_name}' für den Einsatz einer Field Service Management Lösung geeignet ist. " "Berücksichtige, dass ein Unternehmen mit einem technischen Außendienst, idealerweise mit über 50 Technikern und " "Disponenten, die mit der Planung mobiler Ressourcen beschäftigt sind, als geeignet gilt. Nutze dabei verifizierte " "Wikipedia-Daten und deine eigene Einschätzung. Antworte ausschließlich mit 'Ja' oder 'Nein' und gib eine kurze Begründung." ) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "system", "content": prompt}], temperature=0.0 ) result = response.choices[0].message.content.strip() debug_print(f"FSM-Eignungsantwort ChatGPT: '{result}'") suitability = "k.A." justification = "" lines = result.split("\n") if len(lines) == 1: parts = result.split(" ", 1) suitability = parts[0].strip() justification = parts[1].strip() if len(parts) > 1 else "" else: for line in lines: if line.lower().startswith("eignung:"): suitability = line.split(":", 1)[1].strip() elif line.lower().startswith("begründung:"): justification = line.split(":", 1)[1].strip() if suitability not in ["Ja", "Nein"]: parts = result.split(" ", 1) suitability = parts[0].strip() justification = " ".join(result.split()[1:]).strip() return {"suitability": suitability, "justification": justification} except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für FSM-Eignungsprüfung: {e}") return {"suitability": "k.A.", "justification": "k.A."} def evaluate_servicetechnicians_estimate(company_name, company_data): try: with open("serpApiKey.txt", "r") as f: serp_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des SerpAPI-Schlüssels (Servicetechniker): {e}") return "k.A." try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des API-Tokens (Servicetechniker): {e}") return "k.A." openai.api_key = api_key prompt = ( f"Bitte schätze auf Basis öffentlich zugänglicher Informationen (vor allem verifizierte Wikipedia-Daten) " f"die Anzahl der Servicetechniker des Unternehmens '{company_name}' ein. " "Gib die Antwort ausschließlich in einer der folgenden Kategorien aus: " "'<50 Techniker', '>100 Techniker', '>200 Techniker', '>500 Techniker'." ) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "system", "content": prompt}], temperature=0.0 ) result = response.choices[0].message.content.strip() debug_print(f"Schätzung Servicetechniker ChatGPT: '{result}'") return result except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Schätzung: {e}") return "k.A." def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data): try: with open("api_key.txt", "r") as f: api_key = f.read().strip() except Exception as e: debug_print(f"Fehler beim Lesen des API-Tokens (ST-Erklärung): {e}") return "k.A." openai.api_key = api_key prompt = ( f"Bitte erkläre, warum du für das Unternehmen '{company_name}' die Anzahl der Servicetechniker als '{st_estimate}' geschätzt hast. " "Berücksichtige dabei öffentlich zugängliche Informationen wie Branche, Umsatz, Mitarbeiterzahl und andere relevante Daten." ) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "system", "content": prompt}], temperature=0.0 ) result = response.choices[0].message.content.strip() debug_print(f"Servicetechniker-Erklärung ChatGPT: '{result}'") return result except Exception as e: debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Erklärung: {e}") return "k.A." def map_internal_technicians(value): try: num = int(value) except Exception: return "k.A." if num < 50: return "<50 Techniker" elif num < 100: return ">100 Techniker" elif num < 200: return ">200 Techniker" else: return ">500 Techniker" def wait_for_sheet_update(sheet, cell, expected_value, timeout=5): start_time = time.time() while time.time() - start_time < timeout: try: current_value = sheet.acell(cell).value if current_value == expected_value: return True except Exception as e: debug_print(f"Fehler beim Lesen von Zelle {cell}: {e}") time.sleep(0.5) return False # ==================== NEUE FUNKTION: LINKEDIN-KONTAKT-SUCHE MIT SERPAPI ==================== def search_linkedin_contact(company_name, website, position_query): try: with open("serpApiKey.txt", "r") as f: serp_key = f.read().strip() except Exception as e: debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e)) return None query = f'site:linkedin.com/in "{position_query}" "{company_name}"' debug_print(f"Erstelle LinkedIn-Query: {query}") params = { "engine": "google", "q": query, "api_key": serp_key, "hl": "de" } try: response = requests.get("https://serpapi.com/search", params=params) data = response.json() debug_print(f"SerpAPI-Response für Query '{query}': {data.get('organic_results', [])[:1]}") if "organic_results" in data and len(data["organic_results"]) > 0: result = data["organic_results"][0] title = result.get("title", "") debug_print(f"LinkedIn-Suchergebnis-Titel: {title}") if "–" in title: parts = title.split("–") elif "-" in title: parts = title.split("-") else: parts = [title] if len(parts) >= 2: name_part = parts[0].strip() pos = parts[1].split("|")[0].strip() name_parts = name_part.split(" ", 1) if len(name_parts) == 2: firstname, lastname = name_parts else: firstname = name_part lastname = "" debug_print(f"Kontakt gefunden: {firstname} {lastname}, Position: {pos}") return {"Firmenname": company_name, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos} else: debug_print(f"Kontakt gefunden, aber unvollständige Informationen: {title}") return {"Firmenname": company_name, "Website": website, "Vorname": "", "Nachname": "", "Position": title} else: debug_print(f"Keine LinkedIn-Ergebnisse für Query: {query}") return None except Exception as e: debug_print(f"Fehler bei der SerpAPI-Suche: {e}") return None # ==================== NEUER MODUS: CONTACTS (LinkedIn) ==================== def process_contacts(): debug_print("Starte LinkedIn-Kontaktsuche...") gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) sh = gc.open_by_url(Config.SHEET_URL) try: contacts_sheet = sh.worksheet("Contacts") except gspread.exceptions.WorksheetNotFound: contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="10") header = ["Firmenname", "Website", "Vorname", "Nachname", "Position", "Anrede", "E-Mail"] contacts_sheet.update("A1:G1", [header]) debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.") main_sheet = sh.sheet1 data = main_sheet.get_all_values() positions = ["Serviceleiter", "IT-Leiter", "Leiter After Sales", "Leiter Einsatzplanung"] new_rows = [] for idx, row in enumerate(data[1:], start=2): company_name = row[1] if len(row) > 1 else "" website = row[2] if len(row) > 2 else "" debug_print(f"Verarbeite Firma: '{company_name}' (Zeile {idx}), Website: '{website}'") if not company_name or not website: debug_print("Überspringe, da Firmenname oder Website fehlt.") continue for pos in positions: debug_print(f"Suche nach Position: '{pos}' bei '{company_name}'") contact = search_linkedin_contact(company_name, website, pos) if contact: debug_print(f"Kontakt gefunden: {contact}") new_rows.append([contact["Firmenname"], contact["Website"], contact["Vorname"], contact["Nachname"], contact["Position"], "", ""]) else: debug_print(f"Kein Kontakt für Position '{pos}' bei '{company_name}' gefunden.") if new_rows: last_row = len(contacts_sheet.get_all_values()) + 1 range_str = f"A{last_row}:G{last_row + len(new_rows) - 1}" contacts_sheet.update(range_str, new_rows) debug_print(f"{len(new_rows)} Kontakte in 'Contacts' hinzugefügt.") else: debug_print("Keine Kontakte gefunden in der Haupttabelle.") # ==================== NEUER MODUS 4: NUR WIKIPEDIA-SUCHE ==================== def process_wikipedia_only(): debug_print("Starte ausschließlich Wikipedia-Suche (Modus 4)...") gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) sh = gc.open_by_url(Config.SHEET_URL) main_sheet = sh.sheet1 data = main_sheet.get_all_values() start_index = GoogleSheetHandler().get_start_index() debug_print(f"Starte bei Zeile {start_index+1}") for i, row in enumerate(data[1:], start=2): if i < start_index: continue company_name = row[1] if len(row) > 1 else "" website = row[2] if len(row) > 2 else "" debug_print(f"Verarbeite Zeile {i}: {company_name}") article = WikipediaScraper().search_company_article(company_name, website) if article: company_data = WikipediaScraper().extract_company_data(article.url) else: company_data = { 'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.' } wiki_values = [ row[10] if len(row) > 10 and row[10].strip() not in ["", "k.A."] else "k.A.", company_data.get('url', 'k.A.'), company_data.get('first_paragraph', 'k.A.'), company_data.get('branche', 'k.A.'), company_data.get('umsatz', 'k.A.'), company_data.get('mitarbeiter', 'k.A.'), company_data.get('categories', 'k.A.') ] wiki_range = f"K{i}:Q{i}" main_sheet.update(values=[wiki_values], range_name=wiki_range) debug_print(f"Zeile {i} mit Wikipedia-Daten aktualisiert.") current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") main_sheet.update(values=[[current_dt]], range_name=f"AH{i}") main_sheet.update(values=[[Config.VERSION]], range_name=f"AI{i}") time.sleep(Config.RETRY_DELAY) debug_print("Wikipedia-Suche abgeschlossen.") # ==================== GOOGLE SHEET HANDLER (für Hauptdaten) ==================== class GoogleSheetHandler: def __init__(self): self.sheet = None self.sheet_values = [] self._connect() def _connect(self): scope = ["https://www.googleapis.com/auth/spreadsheets"] creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, scope) self.sheet = gspread.authorize(creds).open_by_url(Config.SHEET_URL).sheet1 self.sheet_values = self.sheet.get_all_values() def get_start_index(self): filled_n = [row[13] if len(row) > 13 else '' for row in self.sheet_values[1:]] return next((i + 1 for i, v in enumerate(filled_n, start=1) if not str(v).strip()), len(filled_n) + 1) # ==================== NEUER MODUS 5: CONTACTS-ALIGNMENT DEMO (Schreibtest Contacts) ==================== def contacts_alignment_demo(): debug_print("Starte Contacts-Alignment-Demo (Schreibtest)...") gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name( Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"])) sh = gc.open_by_url(Config.SHEET_URL) try: contacts_sheet = sh.worksheet("Contacts") except gspread.exceptions.WorksheetNotFound: contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="10") debug_print("Neues Blatt 'Contacts' erstellt.") # Schreibe Header falls noch nicht vorhanden if not contacts_sheet.get_all_values(): header = ["Firmenname", "Website", "Vorname", "Nachname", "Position", "Anrede", "E-Mail"] contacts_sheet.update("A1:G1", [header]) debug_print("Header in 'Contacts' geschrieben.") # Schreibe eine Testzeile test_row = ["TestFirma", "www.test.de", "Max", "Mustermann", "Testposition", "Herr", "max.mustermann@test.de"] contacts_sheet.update("A2:G2", [test_row]) debug_print("Testzeile in 'Contacts' geschrieben.") # ==================== ALIGNMENT DEMO (Modus 3) ==================== def alignment_demo(sheet): new_headers = [ "Spalte A (ReEval Flag)", "Spalte B (Firmenname)", "Spalte C (Website)", "Spalte D (Ort)", "Spalte E (Beschreibung)", "Spalte F (Aktuelle Branche)", "Spalte G (Beschreibung Branche extern)", "Spalte H (Anzahl Techniker CRM)", "Spalte I (Umsatz CRM)", "Spalte J (Anzahl Mitarbeiter CRM)", "Spalte K (Vorschlag Wiki URL)", "Spalte L (Wikipedia URL)", "Spalte M (Wikipedia Absatz)", "Spalte N (Wikipedia Branche)", "Spalte O (Wikipedia Umsatz)", "Spalte P (Wikipedia Mitarbeiter)", "Spalte Q (Wikipedia Kategorien)", "Spalte R (Konsistenzprüfung)", "Spalte S (Begründung bei Inkonsistenz)", "Spalte T (Vorschlag Wiki Artikel ChatGPT)", "Spalte U (Begründung bei Abweichung)", "Spalte V (Vorschlag neue Branche)", "Spalte W (Konsistenzprüfung Branche)", "Spalte X (Begründung Abweichung Branche)", "Spalte Y (FSM Relevanz Ja / Nein)", "Spalte Z (Begründung für FSM Relevanz)", "Spalte AA (Schätzung Anzahl Mitarbeiter)", "Spalte AB (Konsistenzprüfung Mitarbeiterzahl)", "Spalte AC (Begründung für Abweichung Mitarbeiterzahl)", "Spalte AD (Einschätzung Anzahl Servicetechniker)", "Spalte AE (Begründung bei Abweichung Anzahl Servicetechniker)", "Spalte AF (Schätzung Umsatz ChatGPT)", "Spalte AG (Begründung für Abweichung Umsatz)", "Spalte AH (Timestamp Wiki Update)", "Spalte AI (Timestamp ChatGPT Bewertung)", "Spalte AJ (Version)" ] header_range = "A11200:AJ11200" sheet.update(values=[new_headers], range_name=header_range) print("Alignment-Demo abgeschlossen: Neue Spaltenüberschriften in Zeile 11200 geschrieben.") # ==================== MAIN PROGRAMM ==================== if __name__ == "__main__": print("Modi: 1 = regulärer Modus, 2 = Re-Evaluierungsmodus, 3 = Alignment-Demo, 4 = Nur Wikipedia-Suche, 5 = Contacts-Alignment Demo (Schreibtest)") mode_input = input("Wählen Sie den Modus: ").strip() if mode_input == "2": MODE = "2" elif mode_input == "3": MODE = "3" elif mode_input == "4": MODE = "4" elif mode_input == "5": MODE = "5" else: MODE = "1" if MODE == "1": try: num_rows = int(input("Wieviele Zeilen sollen überprüft werden? ")) except Exception as e: print("Ungültige Eingabe. Bitte eine Zahl eingeben.") exit(1) processor = DataProcessor() processor.process_rows(num_rows) elif MODE in ["2", "3"]: processor = DataProcessor() processor.process_rows() elif MODE == "4": process_wikipedia_only() elif MODE == "5": contacts_alignment_demo() print(f"\n✅ Auswertung abgeschlossen ({Config.VERSION})")