From fbc9c242c46f730b291c83abe2010e8201689c19 Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 27 May 2025 06:59:23 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 363 ++++++++++++++++++++++++------------------ 1 file changed, 210 insertions(+), 153 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index dc1be8c4..0a371723 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8194,220 +8194,270 @@ class DataProcessor: def _check_financial_plausibility(self, row_data_dict): - results = { # Defaults + results = { "plaus_umsatz_flag": "NICHT_PRUEFBAR", "plaus_ma_flag": "NICHT_PRUEFBAR", "plaus_ratio_flag": "NICHT_PRUEFBAR", "abweichung_umsatz_flag": "N/A", - "abweichung_ma_flag": "N/A", "begruendungen": [] + "abweichung_ma_flag": "N/A", "begruendungen": [], # Wird jetzt zu 'plausi_begruendung_final' am Ende + "plausi_begruendung_final": "Plausibilität OK" # Default, wird überschrieben wenn es Begründungen gibt } + + # Hole den Parent Account Name aus dem übergebenen Dictionary + parent_account_name_d_val = row_data_dict.get("Parent Account Name", "").strip() # Spalte D + is_konzern_tochter = bool(parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.') + + if is_konzern_tochter: + self.logger.debug(f" PlausiCheck: Tochter von Konzern '{parent_account_name_d_val}' (D). Abweichungs-Checks werden als INFO_KONZERN_LOGIK behandelt.") + # --- 1. Plausibilität Finaler Umsatz (BG) --- final_umsatz_str = row_data_dict.get("Finaler Umsatz (Wiki>CRM)", "k.A.") - final_ma_str = row_data_dict.get("Finaler Mitarbeiter (Wiki>CRM)", "k.A.") - self.logger.debug(f" PlausiCheck Input: final_umsatz_str='{final_umsatz_str}', final_ma_str='{final_ma_str}'") - - # Werte für absolute Plausi-Checks (Umsatz in Euro, MA absolut) umsatz_num_absolut = self._get_numeric_value_for_plausi(final_umsatz_str, is_umsatz=True) - ma_num_absolut = self._get_numeric_value_for_plausi(final_ma_str, is_umsatz=False) - self.logger.debug(f" PlausiCheck Numerisch (Absolut): umsatz_abs={umsatz_num_absolut}, ma_abs={ma_num_absolut}") + + # ... (Logik für plaus_umsatz_flag und Begründung für Umsatz bleibt wie gehabt) ... + # Beispielhaft, wie Begründungen gesammelt werden: + temp_begruendungen = [] # Lokale Liste für Begründungen in diesem Check - # --- Plausi-Checks für absolute Werte und Ratio (verwenden umsatz_num_absolut, ma_num_absolut) --- - # (Dieser Teil bleibt wie im letzten Vorschlag, der die Debug-Blöcke enthielt, - # aber er arbeitet jetzt mit umsatz_num_absolut und ma_num_absolut) - - # 1. Plausibilität Umsatz (absolut) if pd.isna(umsatz_num_absolut): - exclusion_list = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] - if final_umsatz_str.lower().strip() not in exclusion_list: + exclusion_list_common = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] + if final_umsatz_str.lower().strip() not in exclusion_list_common and not final_umsatz_str.startswith("FEHLER"): # Auch "FEHLER..." ausschließen results["plaus_umsatz_flag"] = "FEHLER_FORMAT" - results["begruendungen"].append(f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden (und war kein 'k.A.' oder '0').") + temp_begruendungen.append(f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden.") + # else: bleibt NICHT_PRUEFBAR, wenn es k.A./0/Fehler war else: # umsatz_num_absolut ist eine gültige Zahl results["plaus_umsatz_flag"] = "OK" - if umsatz_num_absolut == 0: # Hier ist 0 ein echter numerischer Wert + if umsatz_num_absolut == 0 and final_umsatz_str != "0": # Echte numerische 0, aber nicht aus Input "0" (der als unbekannt gilt) results["plaus_umsatz_flag"] = "WARNUNG_NULL_WERT" - results["begruendungen"].append(f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').") + temp_begruendungen.append(f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').") elif umsatz_num_absolut < getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000): results["plaus_umsatz_flag"] = "WARNUNG_NIEDRIG" - # ... Begründung ... + temp_begruendungen.append(f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) < Min-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):,.0f} €).") elif umsatz_num_absolut > getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000): results["plaus_umsatz_flag"] = "WARNUNG_HOCH" - # ... Begründung ... + temp_begruendungen.append(f"Finaler Umsatz ({umsatz_num_absolut:,.0f} €) > Max-Schwelle ({getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):,.0f} €).") + + + # --- 2. Plausibilität Finale Mitarbeiter (BH) --- + final_ma_str = row_data_dict.get("Finaler Mitarbeiter (Wiki>CRM)", "k.A.") + ma_num_absolut = self._get_numeric_value_for_plausi(final_ma_str, is_umsatz=False) - # 2. Plausibilität Mitarbeiter (absolut) - analog zu Umsatz + # ... (Logik für plaus_ma_flag und Begründung für Mitarbeiter bleibt wie gehabt) ... if pd.isna(ma_num_absolut): - exclusion_list = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] - if final_ma_str.lower().strip() not in exclusion_list: + if final_ma_str.lower().strip() not in exclusion_list_common and not final_ma_str.startswith("FEHLER"): results["plaus_ma_flag"] = "FEHLER_FORMAT" - # ... Begründung ... + temp_begruendungen.append(f"Finale MA ('{final_ma_str}') konnte nicht als Zahl interpretiert werden.") else: results["plaus_ma_flag"] = "OK" - if ma_num_absolut == 0: + if ma_num_absolut == 0 and final_ma_str != "0": results["plaus_ma_flag"] = "WARNUNG_NULL_WERT" - # ... Begründung ... + temp_begruendungen.append(f"Finale MA ist numerisch 0 (aus '{final_ma_str}').") elif ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1): results["plaus_ma_flag"] = "WARNUNG_NIEDRIG" - # ... Begründung ... - # ... (Rest der MA-Checks, inkl. spezifischer Check für wenig MA bei Umsatz) + temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) < Min-Schwelle ({getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1):.0f}).") + elif not pd.isna(umsatz_num_absolut) and umsatz_num_absolut >= getattr(Config, 'PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK', 1000000) and \ + ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3): + results["plaus_ma_flag"] = "WARNUNG_ZU_WENIG_MA_BEI_UMSATZ" + temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) auffällig niedrig für Umsatz ({umsatz_num_absolut:,.0f} €).") + elif ma_num_absolut > getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000): + results["plaus_ma_flag"] = "WARNUNG_HOCH" + temp_begruendungen.append(f"Finale MA ({ma_num_absolut:.0f}) > Max-Schwelle ({getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000):,.0f}).") - # 3. Umsatz/MA Ratio (verwendet umsatz_num_absolut, ma_num_absolut) - if not pd.isna(umsatz_num_absolut) and not pd.isna(ma_num_absolut): + # --- 3. Plausibilität Umsatz/MA Ratio (BI) --- + if not pd.isna(umsatz_num_absolut) and not pd.isna(ma_num_absolut) and umsatz_num_absolut > 0 : # Nur wenn Umsatz > 0 if ma_num_absolut > 0: ratio = umsatz_num_absolut / ma_num_absolut results["plaus_ratio_flag"] = "OK" - # ... (Ratio-Warnungen basierend auf Config-Werten für PLAUSI_RATIO_UMSATZ_PRO_MA_MIN/MAX) - # ... (Behandlung ma_num_absolut == 0) + if ratio < getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000): + results["plaus_ratio_flag"] = "WARNUNG_RATIO_NIEDRIG" + temp_begruendungen.append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) < Min-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000):,.0f} €/MA).") + elif ratio > getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000): + results["plaus_ratio_flag"] = "WARNUNG_RATIO_HOCH" + temp_begruendungen.append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) > Max-Schwelle ({getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):,.0f} €/MA).") + elif ma_num_absolut == 0: # Umsatz vorhanden, aber MA ist 0 + results["plaus_ratio_flag"] = "FEHLER_MA_NULL_BEI_UMSATZ" + temp_begruendungen.append("Umsatz vorhanden, aber MA ist 0. Ratio nicht berechenbar.") + # else: MA < 0 (sollte nicht passieren) oder NaN (bereits oben abgedeckt) + elif pd.isna(umsatz_num_absolut) or umsatz_num_absolut <= 0 : # Wenn Umsatz NaN oder <= 0 ist, ist Ratio nicht sinnvoll prüfbar + results["plaus_ratio_flag"] = "NICHT_PRUEFBAR_UMSATZ_FEHLT" + # Der Fall "ma_num_absolut ist NaN" ist implizit durch die erste Bedingung abgedeckt. - - # --- 4. Abgleich CRM vs. Wiki --- - # HIER VERWENDEN WIR get_numeric_filter_value, um die Mio-Werte für den Vergleich zu bekommen + # --- 4. Abgleich CRM vs. Wiki (BJ, BK) --- crm_umsatz_str = row_data_dict.get("CRM Umsatz", "k.A.") - wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.") + wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.") # Dies ist der Wert aus Spalte W (kann von Parent sein) crm_ma_str = row_data_dict.get("CRM Anzahl Mitarbeiter", "k.A.") - wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.") + wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.") # Dies ist der Wert aus Spalte X (kann von Parent sein) - # Diese geben jetzt Umsatz in Mio zurück, oder 0 bei Unbekannt/Fehler für den Filter - crm_u_mio = get_numeric_filter_value(crm_umsatz_str, True) - wiki_u_mio = get_numeric_filter_value(wiki_umsatz_str, True) - # Diese geben absolute MA-Zahl zurück, oder 0 bei Unbekannt/Fehler für den Filter - crm_m_abs = get_numeric_filter_value(crm_ma_str, False) - wiki_m_abs = get_numeric_filter_value(wiki_ma_str, False) + # _get_numeric_value_for_plausi liefert absolute Werte oder NaN + crm_u_abs = self._get_numeric_value_for_plausi(crm_umsatz_str, is_umsatz=True) + wiki_u_abs = self._get_numeric_value_for_plausi(wiki_umsatz_str, is_umsatz=True) + crm_m_abs_comp = self._get_numeric_value_for_plausi(crm_ma_str, is_umsatz=False) # anderer Variablenname, um Konflikt mit ma_num_absolut oben zu vermeiden + wiki_m_abs_comp = self._get_numeric_value_for_plausi(wiki_ma_str, is_umsatz=False) abweichung_prozent_config = getattr(Config, 'PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT', 30) / 100.0 - # Umsatz Abweichung (arbeitet mit Mio-Werten) - if crm_u_mio > 0 and wiki_u_mio > 0: # Beide müssen positive Werte haben für sinnvollen Vergleich - diff = abs(crm_u_mio - wiki_u_mio) - max_val = max(crm_u_mio, wiki_u_mio) - if (diff / max_val) > abweichung_prozent_config: - results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT" - results["begruendungen"].append(f"Umsatz CRM ({crm_u_mio:,.1f} Mio) vs. Wiki ({wiki_u_mio:,.1f} Mio) weicht >{abweichung_prozent_config*100:.0f}% ab.") + # Umsatz Abweichung (BJ) + if pd.notna(crm_u_abs) and pd.notna(wiki_u_abs) and crm_u_abs > 0 and wiki_u_abs > 0 : + if is_konzern_tochter: + results["abweichung_umsatz_flag"] = "INFO_KONZERN_LOGIK" + temp_begruendungen.append(f"INFO: Umsatz CRM vs. Wiki Abgleich bei Konzernzugehörigkeit nicht direkt bewertet (CRM: {crm_u_abs:,.0f}€, Wiki: {wiki_u_abs:,.0f}€).") else: - results["abweichung_umsatz_flag"] = "OK" - elif crm_u_mio > 0 and wiki_u_mio <= 0 : results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL" - elif crm_u_mio <= 0 and wiki_u_mio > 0 : results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL" - else: results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL" # Oder N/A + diff = abs(crm_u_abs - wiki_u_abs) + # Bezugswert für Prozentrechnung: der größere der beiden Werte oder Durchschnitt? + # Hier: Bezug auf den größeren Wert, um konservativer zu sein + bezugswert_umsatz = max(crm_u_abs, wiki_u_abs) + if bezugswert_umsatz > 0 and (diff / bezugswert_umsatz) > abweichung_prozent_config: + results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT" + temp_begruendungen.append(f"Umsatz CRM ({crm_u_abs:,.0f} €) vs. Wiki ({wiki_u_abs:,.0f} €) weicht >{abweichung_prozent_config*100:.0f}% ab.") + else: + results["abweichung_umsatz_flag"] = "OK" + elif pd.notna(crm_u_abs) and crm_u_abs > 0 and pd.isna(wiki_u_abs): results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL" + elif pd.isna(crm_u_abs) and pd.notna(wiki_u_abs) and wiki_u_abs > 0: results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL" + else: results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL" - # Mitarbeiter Abweichung (arbeitet mit absoluten Zahlen) - if crm_m_abs > 0 and wiki_m_abs > 0: - diff = abs(crm_m_abs - wiki_m_abs) - max_val = max(crm_m_abs, wiki_m_abs) - if (diff / max_val) > abweichung_prozent_config: - results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT" - results["begruendungen"].append(f"MA CRM ({crm_m_abs:.0f}) vs. Wiki ({wiki_m_abs:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.") + # Mitarbeiter Abweichung (BK) + if pd.notna(crm_m_abs_comp) and pd.notna(wiki_m_abs_comp) and crm_m_abs_comp > 0 and wiki_m_abs_comp > 0: + if is_konzern_tochter: + results["abweichung_ma_flag"] = "INFO_KONZERN_LOGIK" + temp_begruendungen.append(f"INFO: MA CRM vs. Wiki Abgleich bei Konzernzugehörigkeit nicht direkt bewertet (CRM: {crm_m_abs_comp:.0f}, Wiki: {wiki_m_abs_comp:.0f}).") else: - results["abweichung_ma_flag"] = "OK" - elif crm_m_abs > 0 and wiki_m_abs <= 0 : results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL" - elif crm_m_abs <= 0 and wiki_m_abs > 0 : results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL" + diff = abs(crm_m_abs_comp - wiki_m_abs_comp) + bezugswert_ma = max(crm_m_abs_comp, wiki_m_abs_comp) + if bezugswert_ma > 0 and (diff / bezugswert_ma) > abweichung_prozent_config: + results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT" + temp_begruendungen.append(f"MA CRM ({crm_m_abs_comp:.0f}) vs. Wiki ({wiki_m_abs_comp:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.") + else: + results["abweichung_ma_flag"] = "OK" + elif pd.notna(crm_m_abs_comp) and crm_m_abs_comp > 0 and pd.isna(wiki_m_abs_comp): results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL" + elif pd.isna(crm_m_abs_comp) and pd.notna(wiki_m_abs_comp) and wiki_m_abs_comp > 0: results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL" else: results["abweichung_ma_flag"] = "BEIDE_FEHLEN_ODER_NULL" - if not results["begruendungen"]: results["plausi_begruendung_final"] = "Plausibilität OK" - else: results["plausi_begruendung_final"] = "; ".join(results["begruendungen"]) + # Finale Begründung (BL) + if temp_begruendungen: + results["plausi_begruendung_final"] = "; ".join(temp_begruendungen) + # else: Default "Plausibilität OK" bleibt return results def run_plausibility_checks_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): - self.logger.info(f"Starte Modus 'Plausibilitäts-Checks mit Konsolidierung'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Datenstart'} bis {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}") + """ + Führt Konsolidierung und Plausibilitäts-Checks für einen Bereich von Zeilen durch. + - Konsolidiert Umsatz/MA (BD/BE) basierend auf CRM/Wiki und Parent-Account (D). + - Ruft _check_financial_plausibility für jede Zeile auf. + - Schreibt konsolidierte Werte und Plausi-Ergebnisse (BG-BM) ins Sheet. + - Setzt den Plausibilität Prüfdatum (BM). - plausi_ts_key = "Plausibilität Prüfdatum" - # Stellen Sie sicher, dass alle benötigten Spalten in COLUMN_MAP existieren - required_keys_for_mode = [ - "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", # Input für Konsolidierung - "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", # Output Konsolidierung / Input Plausi - "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", # Output Plausi - "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", # Output Plausi - plausi_ts_key # Output Timestamp - ] - if not all(key in COLUMN_MAP for key in required_keys_for_mode): - missing_k = [k for k in required_keys_for_mode if k not in COLUMN_MAP] - self.logger.error(f"Nicht alle benötigten Spalten ({missing_k}) für Plausi-Checks mit Konsolidierung in COLUMN_MAP. Abbruch.") - return + Args: + start_sheet_row (int, optional): Die 1-basierte Startzeile. Default: Ab erster Datenzeile. + end_sheet_row (int, optional): Die 1-basierte Endzeile. Default: Bis Ende des Sheets. + limit (int, optional): Maximale Anzahl zu verarbeitender Zeilen. Default: Unbegrenzt. + """ + self.logger.info(f"Starte Modus 'plausi_check_data' (Konsolidierung & Plausi-Checks). Bereich: {start_sheet_row if start_sheet_row is not None else 'Start'}-{end_sheet_row if end_sheet_row else 'Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}") + # --- Daten laden und Start/Ende bestimmen --- if not self.sheet_handler.load_data(): self.logger.error("Konnte Sheet-Daten nicht laden für Plausi-Checks. Abbruch.") return all_data = self.sheet_handler.get_all_data_with_headers() header_offset = self.sheet_handler._header_rows - - updates_fuer_sheet = [] - processed_rows_count = 0 - now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + total_sheet_rows = len(all_data) effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1 - effective_end_row = end_sheet_row if end_sheet_row is not None else len(all_data) + effective_end_row = end_sheet_row if end_sheet_row is not None else total_sheet_rows - self.logger.info(f"Konsolidiere und prüfe Plausibilität für Zeilen {effective_start_row} bis {effective_end_row}.") + if effective_start_row > effective_end_row or effective_start_row > total_sheet_rows: + self.logger.info("Start liegt nach Ende oder außerhalb des Sheets. Keine Zeilen zu verarbeiten.") + return + + self.logger.info(f"Verarbeite Zeilen {effective_start_row} bis {effective_end_row} für Konsolidierung und Plausi-Checks.") + # --- Notwendige Spalten prüfen --- + required_keys_for_plausi_mode = [ + "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", "Parent Account Name", + "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", + "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", + "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", + "Plausibilität Prüfdatum" + ] + if not all(key in COLUMN_MAP for key in required_keys_for_plausi_mode): + missing_k = [k for k in required_keys_for_plausi_mode if k not in COLUMN_MAP] + self.logger.error(f"Nicht alle benötigten Spalten ({missing_k}) für Modus 'plausi_check_data' in COLUMN_MAP. Abbruch.") + return + + # --- Verarbeitung --- + all_sheet_updates = [] + processed_rows_count = 0 + now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + update_batch_limit_config = getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) + # Jede Zeile generiert ca. 9 Updates (BD, BE, BG-BL, BM) + # Daher ist die Anzahl der Operationen ca. 9 * Anzahl Zeilen + # Wir lösen den Batch-Update aus, wenn die Anzahl der Operationen das Limit erreicht. + # update_batch_operations_trigger = update_batch_limit_config * 9 + # Sicherer: nach jeder `update_batch_limit_config`-ten Zeile updaten + for row_num_sheet in range(effective_start_row, effective_end_row + 1): if limit is not None and processed_rows_count >= limit: self.logger.info(f"Verarbeitungslimit von {limit} Zeilen erreicht.") break row_list_idx = row_num_sheet - 1 - if row_list_idx >= len(all_data): break - row_data = all_data[row_list_idx] - - # CRM Name für Logging - # crm_name_log = self._get_cell_value_safe(row_data, "CRM Name") - # self.logger.debug(f"Verarbeite Zeile {row_num_sheet} ({crm_name_log[:30]}...) für Plausi-Check & Konsolidierung.") + if row_list_idx >= total_sheet_rows: break # Ende der Daten erreicht + row_data = all_data[row_list_idx] # Rohdaten der aktuellen Zeile + # Überspringe leere Zeilen oder Zeilen ohne Firmennamen + crm_name_check = self._get_cell_value_safe(row_data, "CRM Name").strip() + if not crm_name_check: + # self.logger.debug(f"Zeile {row_num_sheet}: Übersprungen (kein Firmenname).") + continue + + self.logger.debug(f"Zeile {row_num_sheet} ({crm_name_check[:30]}...): Starte Konsolidierung und Plausi-Check.") current_row_updates = [] - # --- 1. Konsolidierung (Logik aus _process_single_row, Block 3e) --- + # 1. Konsolidierung (BD, BE) final_umsatz_str_konsolidiert = "k.A." - final_ma_str_konsolidiert = "k.A." - - crm_umsatz_val_str = self._get_cell_value_safe(row_data, "CRM Umsatz") - wiki_umsatz_val_str = self._get_cell_value_safe(row_data, "Wiki Umsatz") # Liest direkt aus dem Sheet - crm_ma_val_str = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") - wiki_ma_val_str = self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") # Liest direkt aus dem Sheet - + final_ma_str_konsolidiert = "k.A." + parent_account_name_d_val = self._get_cell_value_safe(row_data, "Parent Account Name").strip() + try: - # get_numeric_filter_value liefert Umsatz in Mio, MA absolut, oder 0 bei Fehler/unbekannt + crm_umsatz_val_str = self._get_cell_value_safe(row_data, "CRM Umsatz") + wiki_umsatz_val_str = self._get_cell_value_safe(row_data, "Wiki Umsatz") + crm_ma_val_str = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") + wiki_ma_val_str = self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") + num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val_str, is_umsatz=True) num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val_str, is_umsatz=True) num_crm_ma = get_numeric_filter_value(crm_ma_val_str, is_umsatz=False) num_wiki_ma = get_numeric_filter_value(wiki_ma_val_str, is_umsatz=False) - # Konsolidierung: Wiki > CRM. Priorisiere den Wert, der nicht 0 ist (was "unbekannt" von get_numeric_filter_value bedeutet). - final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz != 0.0 else num_crm_umsatz - final_num_ma = num_wiki_ma if num_wiki_ma != 0 else num_crm_ma - - # String-Konvertierung für Sheet: - # Ein numerischer Wert von 0 (der aus "0 Tsd" oder echter 0 kommen kann) wird als String "0" geschrieben. - # Wenn final_num_xxx 0 ist, weil *beide* Quellen 0 (oder leer/k.A.) waren, wird "k.A." geschrieben. - if final_num_umsatz == 0.0 and (num_crm_umsatz == 0.0 and num_wiki_umsatz == 0.0): # Beide Quellen waren "unbekannt" - final_umsatz_str_konsolidiert = 'k.A.' + if parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.': + self.logger.debug(f" -> Parent D ('{parent_account_name_d_val}') ist gesetzt. Konsolidiere primär mit CRM-Daten der Tochter.") + final_num_umsatz = num_crm_umsatz if num_crm_umsatz > 0 else num_wiki_umsatz + final_num_ma = num_crm_ma if num_crm_ma > 0 else num_wiki_ma else: - final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) - - if final_num_ma == 0 and (num_crm_ma == 0 and num_wiki_ma == 0): # Beide Quellen waren "unbekannt" - final_ma_str_konsolidiert = 'k.A.' - else: - final_ma_str_konsolidiert = str(int(round(final_num_ma))) + final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz + final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma - self.logger.debug(f" Zeile {row_num_sheet}: Konsolidiert -> U: '{final_umsatz_str_konsolidiert}', MA: '{final_ma_str_konsolidiert}'") - - except Exception as e_conso_direct: - self.logger.error(f"Fehler bei direkter Konsolidierung in Plausi-Check für Zeile {row_num_sheet}: {e_conso_direct}") - final_umsatz_str_konsolidiert = "FEHLER_KONSO_IN_PLAUSI" - final_ma_str_konsolidiert = "FEHLER_KONSO_IN_PLAUSI" + final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 else 'k.A.' + final_ma_str_konsolidiert = str(int(round(final_num_ma))) if final_num_ma > 0 else 'k.A.' + except Exception as e_conso_batch: + self.logger.error(f"Fehler bei Konsolidierung in Plausi-Batch für Zeile {row_num_sheet}: {e_conso_batch}") + final_umsatz_str_konsolidiert = "FEHLER_KONSO_PLAUSI" + final_ma_str_konsolidiert = "FEHLER_KONSO_PLAUSI" current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_umsatz_str_konsolidiert]]}) current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_ma_str_konsolidiert]]}) - # --- 2. Plausibilitäts-Checks (basierend auf den gerade konsolidierten Werten) --- - # Nur ausführen, wenn Konsolidierung keine FEHLER produziert hat + # 2. Plausibilitäts-Checks (BG-BM) if not final_umsatz_str_konsolidiert.startswith("FEHLER") and not final_ma_str_konsolidiert.startswith("FEHLER"): - processed_rows_count +=1 # Zähle nur Zeilen, für die Plausi tatsächlich versucht wird try: plausi_input_data = { "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, - "CRM Umsatz": crm_umsatz_val_str, - "Wiki Umsatz": wiki_umsatz_val_str, - "CRM Anzahl Mitarbeiter": crm_ma_val_str, - "Wiki Mitarbeiter": wiki_ma_val_str + "CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"), + "Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"), # Direkter Wert aus Sheet + "CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), + "Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter"), # Direkter Wert aus Sheet + "Parent Account Name": parent_account_name_d_val } plausi_results = self._check_financial_plausibility(plausi_input_data) @@ -8418,35 +8468,41 @@ class DataProcessor: current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) - except Exception as e_plausi_run: - self.logger.error(f"Fehler im Plausi-Check Aufruf für Zeile {row_num_sheet}: {e_plausi_run}") + except Exception as e_plausi_run_batch: + self.logger.error(f"Fehler im Plausi-Check Aufruf (Batch-Modus) für Zeile {row_num_sheet}: {e_plausi_run_batch}") + # Fehler-Flags für Plausi-Spalten setzen for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['FEHLER_PLAUSI_CALL']]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[f"Systemfehler Plausi-Call: {str(e_plausi_run)[:100]}"]]}) - else: # Konsolidierung hat Fehler produziert + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['FEHLER_CALL']]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[f"Systemfehler: {str(e_plausi_run_batch)[:100]}"]]}) + else: self.logger.warn(f"Zeile {row_num_sheet}: Überspringe Plausi-Checks wegen Fehler bei Konsolidierung.") - # Setze Plausi-Flags auf einen Fehlerstatus oder lasse sie leer (aktuell wird nichts explizit gesetzt) for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['INPUT_FEHLER_KONSO']]}) current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [["Konsolidierung fehlgeschlagen"]]}) + # Plausibilität Prüfdatum (BM) setzen + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Prüfdatum"] + 1)}{row_num_sheet}', 'values': [[now_timestamp_str]]}) + + all_sheet_updates.extend(current_row_updates) + processed_rows_count += 1 - # Immer den Plausi-Timestamp setzen, da die Zeile betrachtet wurde - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[plausi_ts_key] + 1)}{row_num_sheet}', 'values': [[now_timestamp_str]]}) - updates_fuer_sheet.extend(current_row_updates) + # Batch-Update auslösen, wenn Limit erreicht + if processed_rows_count % update_batch_limit_config == 0 and all_sheet_updates: + self.logger.info(f"Sende Plausi-Check & Konsolidierungs Batch-Update ({len(all_sheet_updates) / 9:.0f} Zeilen)...") # Ca. 9 Updates pro Zeile + success = self.sheet_handler.batch_update_cells(all_sheet_updates) + if success: + self.logger.info(f" Sheet-Update für Plausi-Batch erfolgreich.") + all_sheet_updates = [] # Liste leeren + time.sleep(0.5) # Kurze Pause - # Batch-Update Logik (Anzahl Spalten jetzt: 2 Konsolidierung + 6 Plausi-Flags/Begründung + 1 Plausi-TS = 9) - if len(updates_fuer_sheet) >= getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 9: - self.logger.info(f"Sende Plausi-Check & Konsolidierungs Batch-Update ({len(updates_fuer_sheet)//9} Zeilen)...") - self.sheet_handler.batch_update_cells(updates_fuer_sheet) - updates_fuer_sheet = [] - # time.sleep(0.5) # Kleine optionale Pause - - if updates_fuer_sheet: - self.logger.info(f"Sende finalen Plausi-Check & Konsolidierungs Batch-Update ({len(updates_fuer_sheet)//9} Zeilen)...") - self.sheet_handler.batch_update_cells(updates_fuer_sheet) + # Letzten Batch senden, falls noch Updates vorhanden sind + if all_sheet_updates: + self.logger.info(f"Sende finalen Plausi-Check & Konsolidierungs Batch-Update ({len(all_sheet_updates) / 9:.0f} Zeilen)...") + success = self.sheet_handler.batch_update_cells(all_sheet_updates) + if success: + self.logger.info(f" Finales Sheet-Update für Plausi-Batch erfolgreich.") - self.logger.info(f"Plausibilitäts-Check-Lauf (mit Konsolidierung) beendet. {processed_rows_count} Zeilen mit Plausi-Checks versehen, {skipped_count} Zeilen initial übersprungen.") + self.logger.info(f"Modus 'plausi_check_data' abgeschlossen. {processed_rows_count} Zeilen mit Konsolidierung und Plausi-Checks versehen.") # ========================================================================== # === Batch Processing Methods (Parent Account Suggestion) ============== @@ -11010,13 +11066,14 @@ def main(): limit=final_limit_to_use # Verwendet das global ermittelte Limit ) - elif selected_mode == "plausi_check_data": # <<< NEUER ELIF-BLOCK - data_processor.run_plausibility_checks_batch( # Methode aus vorherigem Vorschlag - start_sheet_row=args.start_sheet_row, # Nimmt CLI-Argumente für Bereich + elif selected_mode == "plausi_check_data": + data_processor.run_plausibility_checks_batch( + start_sheet_row=args.start_sheet_row, # Nimmt CLI-Argumente für Bereich end_sheet_row=args.end_sheet_row, - limit=final_limit_to_use # VERWENDE final_limit_to_use + limit=final_limit_to_use # VERWENDE das ermittelte Limit ) + elif selected_mode == "suggest_parents": # <<< NEUER ELIF-BLOCK data_processor.process_parent_suggestion_batch( start_sheet_row=args.start_sheet_row,