This commit is contained in:
2025-05-27 06:59:23 +00:00
parent 335631152a
commit fbc9c242c4

View File

@@ -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,