diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 05ca0fc1..9ed50676 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -808,14 +808,14 @@ def extract_numeric_value(raw_value, is_umsatz=False): # Extrahiert und normalisiert Zahlenwerte fuer Vergleichslogik. # Nutzt globale Helfer: clean_text, re. # Globale Funktion (ersetzen Sie Ihre bestehende Version) +# Globale Funktion (ersetzen Sie Ihre bestehende Version) def get_numeric_filter_value(value_str, is_umsatz=False): - logger = logging.getLogger(__name__) + logger = logging.getLogger(__name__ + ".get_numeric_filter_value") if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': return 0.0 if is_umsatz else 0 raw_value_str_original = str(value_str).strip() - # "0"-Strings und "k.A." für Filterlogik als 0 behandeln if raw_value_str_original.lower() in ['k.a.', 'n/a', '-', '0', '0.0', '0,0', '0.00', '0,000', '0.000']: return 0.0 if is_umsatz else 0 @@ -828,108 +828,52 @@ def get_numeric_filter_value(value_str, is_umsatz=False): processed_value = re.sub(r'[€$£¥]', '', processed_value).strip() processed_value = re.split(r'\s*(-|–|bis)\s*', processed_value, 1)[0].strip() - # Tausendertrenner entfernen (Apostroph, Punkt wenn nicht Dezimal) - # Komma zu Punkt für Dezimal machen - num_extraction_str = processed_value.replace("'", "") + num_extraction_str = processed_value.replace("'", "").replace(" ", "") + if not num_extraction_str: return 0.0 if is_umsatz else 0 - # Fall 1: Enthält Punkt UND Komma (z.B. 1.234,56 oder 1,234.56) - if '.' in num_extraction_str and ',' in num_extraction_str: - if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US-Stil: 1,234.56 -> Kommas entfernen + has_dot = '.' in num_extraction_str + has_comma = ',' in num_extraction_str + + if has_dot and has_comma: + if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US: 1,234.56 num_extraction_str = num_extraction_str.replace(',', '') - else: # EU-Stil: 1.234,56 -> Punkte entfernen, Komma zu Punkt + else: # EU: 1.234,56 num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') - # Fall 2: Enthält nur Komma (z.B. 1234,56) -> Komma zu Punkt - elif ',' in num_extraction_str: - num_extraction_str = num_extraction_str.replace(',', '.') - # Fall 3: Enthält nur Punkt(e) (z.B. 4.380 oder 123.45 oder 1.234.567) - elif '.' in num_extraction_str: - # Wenn der String klar ein Tausenderformat hat (Endet nicht mit .XX und hat Punkte) - if re.fullmatch(r'^\d{1,3}(\.\d{3})+$', num_extraction_str) and not re.search(r'\.\d{1,2}$', num_extraction_str): + elif has_comma: # Nur Kommas + if num_extraction_str.count(',') == 1 and re.search(r',\d{1,2}$', num_extraction_str) and not re.search(r',\d{3}', num_extraction_str): + num_extraction_str = num_extraction_str.replace(',', '.') # Dezimalkomma + else: # Tausenderkommas + num_extraction_str = num_extraction_str.replace(',', '') + elif has_dot: # Nur Punkte + if num_extraction_str.count('.') == 1 and re.search(r'\.\d{1,2}$', num_extraction_str) and not re.search(r'\.\d{3}', num_extraction_str): + pass # Dezimalpunkt, bleibt + else: # Tausenderpunkte num_extraction_str = num_extraction_str.replace('.', '') - # Sonst ist ein einzelner Punkt ein Dezimalpunkt (z.B. "123.45") - # Bei mehreren Punkten, die nicht Tausenderformat sind (z.B. 1.2.3), wird es später als Fehler behandelt. - elif num_extraction_str.count('.') > 1: - logger.debug(f"get_numeric_filter_value: Mehrere Punkte in '{num_extraction_str}' (nicht als Tausenderformat erkannt).") - return 0.0 if is_umsatz else 0 - match = re.search(r'([\d.\-]+)', num_extraction_str) - if not match: + # Finale Validierung mit Regex, ob es eine gültige Zahl ist + if not re.fullmatch(r'-?\d+(\.\d+)?', num_extraction_str): + logger.debug(f"Kein gültiger numerischer String nach Trennzeichenbehandlung: '{num_extraction_str}' (Original: '{raw_value_str_original}')") return 0.0 if is_umsatz else 0 - - num_str_for_float = match.group(1) - - if not num_str_for_float or num_str_for_float == '.' or num_str_for_float == '-' or \ - (num_str_for_float.count('.') > 1) or \ - (num_str_for_float.count('-') > 1) or \ - (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): - return 0.0 if is_umsatz else 0 - - num_as_float = float(num_str_for_float) # Dies ist der Wert, wie er im String steht (sollte Mio. € sein für Umsatz, wenn keine Einheit) + + num_as_float = float(num_extraction_str) scaled_num = num_as_float original_lower = raw_value_str_original.lower() - if is_umsatz: # Zielwert soll in Millionen sein + if is_umsatz: if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - scaled_num = num_as_float * 1000.0 # z.B. "2 Mrd" -> num_as_float=2 -> 2000 Mio - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - scaled_num = num_as_float / 1000.0 # z.B. "500 Tsd" -> num_as_float=500 -> 0.5 Mio - # Ansonsten wird angenommen, dass num_as_float bereits den Wert in Mio. € darstellt (z.B. "173" aus CRM) - else: # Mitarbeiter (absolute Zahl) - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - scaled_num = num_as_float * 1000000000.0 - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): - scaled_num = num_as_float * 1000000.0 - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): scaled_num = num_as_float * 1000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + scaled_num = num_as_float / 1000.0 + else: # Mitarbeiter + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): scaled_num = num_as_float * 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): scaled_num = num_as_float * 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): scaled_num = num_as_float * 1000.0 return scaled_num if scaled_num > 0 else (0.0 if is_umsatz else 0) except ValueError as e: - logger.debug(f"get_numeric_filter_value: ValueError '{e}' bei Konvertierung von '{num_str_for_float if 'num_str_for_float' in locals() else raw_value_str_original[:30]}...'") - return 0.0 if is_umsatz else 0 - except Exception as e_general: - logger.error(f"Unerwarteter Fehler in get_numeric_filter_value für '{raw_value_str_original[:50]}...': {e_general}") - logger.debug(traceback.format_exc()) - return 0.0 if is_umsatz else 0 - - # Extrahiere die Zahl (kann jetzt einen Dezimalpunkt enthalten) - match = re.search(r'([\d.\-]+)', num_extraction_str) - if not match: - return 0.0 if is_umsatz else 0 - - num_str_for_float = match.group(1) - - # Finale Prüfung des extrahierten Zahlenstrings - if not num_str_for_float or num_str_for_float == '.' or num_str_for_float == '-' or \ - (num_str_for_float.count('.') > 1) or \ - (num_str_for_float.count('-') > 1) or \ - (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): - return 0.0 if is_umsatz else 0 - - num_as_float = float(num_str_for_float) - - scaled_num = num_as_float - original_lower = raw_value_str_original.lower() - - if is_umsatz: # Wert soll in Mio. € sein - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - scaled_num = num_as_float * 1000.0 # von Mrd-Einheit zu Mio-Einheit - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - scaled_num = num_as_float / 1000.0 # von Tsd-Einheit zu Mio-Einheit - # Ansonsten wird angenommen, dass num_as_float bereits den Wert in Mio. € darstellt - else: # Mitarbeiter (absolute Zahl) - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - scaled_num = num_as_float * 1000000000.0 - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): - scaled_num = num_as_float * 1000000.0 - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - scaled_num = num_as_float * 1000.0 - - return scaled_num if scaled_num > 0 else (0.0 if is_umsatz else 0) - - except ValueError as e: - logger.debug(f"get_numeric_filter_value: ValueError '{e}' bei Konvertierung von '{num_str_for_float if 'num_str_for_float' in locals() else raw_value_str_original[:30]}...'") + logger.debug(f"ValueError '{e}' bei Konvertierung (get_numeric_filter_value) von '{num_extraction_str if 'num_extraction_str' in locals() else raw_value_str_original[:30]}...'") return 0.0 if is_umsatz else 0 except Exception as e_general: logger.error(f"Unerwarteter Fehler in get_numeric_filter_value für '{raw_value_str_original[:50]}...': {e_general}") @@ -4836,37 +4780,53 @@ class DataProcessor: final_umsatz_str_konsolidiert = "FEHLER KONSO" final_ma_str_konsolidiert = "FEHLER KONSO" - # --- NEU: 3f. Plausibilitäts-Checks durchführen (BB-BG) --- - self.logger.debug(f" -> Führe Plausibilitäts-Checks für Zeile {row_num_in_sheet} durch...") + # --- NEU: 3f. Plausibilitäts-Checks durchführen (BG-BM) --- + self.logger.debug(f" Zeile {row_num_in_sheet}: Führe Plausibilitäts-Checks durch...") + plausi_check_erfolgreich_durchgefuehrt = False # Flag try: - # Erstelle ein Dictionary mit den benötigten Werten für den Plausi-Check + # ... (Erstellung von plausi_input_data wie zuvor) ... plausi_input_data = { "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, "CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"), - "Wiki Umsatz": final_wiki_data.get('umsatz', 'k.A.'), # Nutze den Wiki-Wert, der für Konsolidierung verwendet wurde + "Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"), "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), - "Wiki Mitarbeiter": final_wiki_data.get('mitarbeiter', 'k.A.') # Nutze den Wiki-Wert, der für Konsolidierung verwendet wurde + "Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") } plausi_results = self._check_financial_plausibility(plausi_input_data) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "FEHLER")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "FEHLER")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "FEHLER")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "FEHLER")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "FEHLER")]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler bei Begründungserstellung")]]}) - self.logger.debug(f" -> Plausi-Ergebnisse: U:{plausi_results.get('plaus_umsatz_flag')} MA:{plausi_results.get('plaus_ma_flag')} Ratio:{plausi_results.get('plaus_ratio_flag')} AbwU:{plausi_results.get('abweichung_umsatz_flag')} AbwMA:{plausi_results.get('abweichung_ma_flag')}") - except Exception as e_plausi: - self.logger.error(f"FEHLER bei Plausibilitäts-Checks für Zeile {row_num_in_sheet}: {e_plausi}") - # Setze Fehler-Flags, falls noch nicht geschehen - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', 'values': [['FEHLER_CHECK']]}) - # ... (ähnlich für andere Plausi-Spalten) ... - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', 'values': [[f"Systemfehler Plausi-Check: {str(e_plausi)[:50]}"]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "ERR_FLAG")]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) + + plausi_check_erfolgreich_durchgefuehrt = True # Markieren, dass der Check zumindest versucht wurde + + except Exception as e_plausi_in_single_row: + self.logger.error(f"FEHLER bei Plausibilitäts-Checks in _process_single_row für Zeile {row_num_in_sheet}: {e_plausi_in_single_row}") + # Fehler-Flags für Plausi-Spalten setzen + error_val_plausi = [['FEHLER_PLAUSI_CALL']] + for key_flag_plausi in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: + if COLUMN_MAP.get(key_flag_plausi) is not None: # Sicherstellen, dass der Key existiert + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag_plausi] + 1)}{row_num_in_sheet}', 'values': error_val_plausi}) + if COLUMN_MAP.get("Plausibilität Begründung") is not None: + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_in_sheet}', 'values': [[f"Systemfehler Plausi-Call: {str(e_plausi_in_single_row)[:50]}"]]}) + + # Plausi-Timestamp setzen, wenn der Plausi-Block (3f) erreicht wurde + # (entweder erfolgreich oder mit Fehler im try-Block) + if COLUMN_MAP.get("Plausibilität Prüfdatum") is not None: + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + else: + self.logger.error("FEHLER: Schlüssel 'Plausibilität Prüfdatum' nicht in COLUMN_MAP. Kann Timestamp nicht setzen.") - # Setze den Timestamp letzte Pruefung (BI - vorher BB), da die ChatGPT-Evaluationen (und jetzt Plausi) liefen - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Pruefung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + # Setze den "Timestamp letzte Prüfung" (BO - für ChatGPT-Evaluationen), da dieser Block lief + if COLUMN_MAP.get("Timestamp letzte Prüfung") is not None: + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + else: + self.logger.error("FEHLER: Schlüssel 'Timestamp letzte Prüfung' nicht in COLUMN_MAP. Kann Haupt-Timestamp nicht setzen.") # else if run_chat_step (aber nicht processing_needed): # self.logger.debug(f"Zeile {row_num_in_sheet}: Ueberspringe CHATGPT Evaluationen und Plausi-Checks (Timestamp BI gesetzt, Wiki nicht aktualisiert und kein Re-Eval).") @@ -7958,7 +7918,7 @@ class DataProcessor: # Innerhalb der DataProcessor Klasse def _get_numeric_value_for_plausi(self, value_str, is_umsatz=False): - logger = logging.getLogger(__name__ + "._get_numeric_value_for_plausi") # Eigener Logger + logger = logging.getLogger(__name__ + "._get_numeric_value_for_plausi") if value_str is None or pd.isna(value_str): return np.nan raw_value_str_clean = str(value_str).strip() @@ -7973,58 +7933,53 @@ class DataProcessor: temp_val = re.sub(r'[€$£¥]', '', temp_val).strip() temp_val = re.split(r'\s*(-|–|bis)\s*', temp_val, 1)[0].strip() - num_extraction_str = temp_val.replace("'", "") - if '.' in num_extraction_str and ',' in num_extraction_str: + num_extraction_str = temp_val.replace("'", "").replace(" ", "") + if not num_extraction_str: return np.nan + + has_dot = '.' in num_extraction_str + has_comma = ',' in num_extraction_str + + if has_dot and has_comma: if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): num_extraction_str = num_extraction_str.replace(',', '') else: num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') - elif ',' in num_extraction_str: - num_extraction_str = num_extraction_str.replace(',', '.') - elif '.' in num_extraction_str: - if re.fullmatch(r'^\d{1,3}(\.\d{3})+$', num_extraction_str) and not re.search(r'\.\d{1,2}$', num_extraction_str): + elif has_comma: + if num_extraction_str.count(',') == 1 and re.search(r',\d{1,2}$', num_extraction_str) and not re.search(r',\d{3}', num_extraction_str): + num_extraction_str = num_extraction_str.replace(',', '.') + else: + num_extraction_str = num_extraction_str.replace(',', '') + elif has_dot: + if num_extraction_str.count('.') == 1 and re.search(r'\.\d{1,2}$', num_extraction_str) and not re.search(r'\.\d{3}', num_extraction_str): + pass + else: num_extraction_str = num_extraction_str.replace('.', '') - elif num_extraction_str.count('.') > 1: - logger.debug(f"Mehrere Punkte in '{num_extraction_str}' (nicht als Tausenderformat erkannt).") - return np.nan - match = re.search(r'([\d.\-]+)', num_extraction_str) - if not match: - logger.debug(f"Kein numerischer Match in '{num_extraction_str}' (von '{raw_value_str_clean}') -> NaN.") + if not re.fullmatch(r'-?\d+(\.\d+)?', num_extraction_str): + logger.debug(f"Kein gültiger numerischer String nach Trennzeichenbehandlung: '{num_extraction_str}' (Original: '{raw_value_str_clean}')") return np.nan - - num_str_for_float = match.group(1) - try: - if not num_str_for_float or num_str_for_float == '.' or num_str_for_float == '-' or \ - (num_str_for_float.count('.') > 1) or \ - (num_str_for_float.count('-') > 1) or \ - (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): - raise ValueError("Ungültiger Zahlenstring nach Regex") - num_val = float(num_str_for_float) - + try: + num_val = float(num_extraction_str) original_lower = raw_value_str_clean.lower() final_num_absolute = num_val - if is_umsatz: # Ziel ist der absolute Euro-Betrag + if is_umsatz: if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): final_num_absolute = num_val * 1000000000.0 elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): final_num_absolute = num_val * 1000.0 - else: # Annahme: num_val ist bereits in Mio (z.B. "173" aus CRM), konvertiere zu absolutem Euro + else: final_num_absolute = num_val * 1000000.0 - else: # Mitarbeiter (absolute Zahl, außer bei expliziten Einheiten) + else: if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): final_num_absolute = num_val * 1000000000.0 elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): final_num_absolute = num_val * 1000000.0 elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): final_num_absolute = num_val * 1000.0 - # Explizite "0"-Strings wurden oben bereits zu NaN. - # Wenn final_num_absolute hier 0.0 ist, dann kam es von einer Berechnung (z.B. "0 Tsd"). - # Die Plausi-Checks können dann mit diesem numerischen Wert 0.0 arbeiten. return final_num_absolute except ValueError as e: - logger.debug(f"ValueError '{e}' bei Konvertierung von '{num_str_for_float}' (von '{raw_value_str_clean}') -> NaN.") + logger.debug(f"ValueError '{e}' bei Konvertierung von '{num_extraction_str}' (von '{raw_value_str_clean}') -> NaN.") return np.nan except Exception as e_general: logger.error(f"Unerwarteter Fehler in _get_numeric_value_for_plausi für '{raw_value_str_clean[:50]}...': {e_general}")