Files
sufw-taetigkeitsnachweise/src/parser.py
T

135 lines
5.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Input-Datei-Parser: liest monatliche Sheets und erzeugt EmployeeRecords."""
import re
from pathlib import Path
from typing import Any, Optional
from openpyxl import load_workbook
from openpyxl.utils import column_index_from_string
from .config import is_month_sheet
from .models import EmployeeRecord, FieldSchema
def _to_text(value: Any) -> str:
if value is None:
return ""
return str(value).strip()
def _to_int_or_none(value: Any) -> Optional[int]:
if value is None or str(value).strip() == "":
return None
try:
return int(value)
except (ValueError, TypeError):
return None
def _to_percent_string(value: Any) -> str:
"""Wandelt einen Prozentwert in einen String ohne %-Zeichen um.
Excel speichert Prozentwerte als Dezimalbruch (50 % = 0.5).
Falls der Rohwert eine Zahl ≤ 1 ist, wird er als Excel-Prozentwert
interpretiert und mit 100 multipliziert.
"""
if value is None or str(value).strip() == "":
return ""
# Falls es bereits ein String mit %-Zeichen ist
if isinstance(value, str):
return value.strip().rstrip("% ")
# Numerischer Wert: Excel-Prozentwert (0.5 = 50 %) oder Ganzzahl (50 = 50 %)
if isinstance(value, (int, float)):
if 0 < value <= 1:
return str(int(value * 100))
if value == 0:
return "0"
# Wert > 1: bereits als Ganzzahl gemeint (z. B. 50)
return str(int(value))
return str(value).strip()
def _build_column_map(schema: list[FieldSchema]) -> dict[str, int]:
"""Baut ein Mapping von field_name → 0-basierter Spaltenindex."""
col_map: dict[str, int] = {}
for field in schema:
col_map[field.field_name] = column_index_from_string(field.column) - 1
return col_map
def parse_input_file(
filepath: Path,
schema: list[FieldSchema],
month_sheet_pattern: str = "* 20??",
skip_empty_name: bool = True,
skip_empty_antragsnummer: bool = True,
) -> list[EmployeeRecord]:
"""
Öffnet die Input-Datei, iteriert über alle Monats-Sheets
und gibt eine Liste von EmployeeRecords zurück.
"""
wb = load_workbook(filepath, data_only=True)
col_map = _build_column_map(schema)
records: list[EmployeeRecord] = []
for sheet_name in wb.sheetnames:
if not is_month_sheet(sheet_name, month_sheet_pattern):
continue
ws = wb[sheet_name]
# Header ist Zeile 1, Daten ab Zeile 2
# min_col=1 explizit setzen für korrekte Spaltenzuordnung
schema_col_count = len(schema)
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=1, max_col=schema_col_count, values_only=True):
name = _to_text(row[col_map.get("Name", 0)]) if len(row) > col_map.get("Name", 0) else ""
antragsnummer = _to_text(row[col_map.get("Antragsnummer", 0)]) if len(row) > col_map.get("Antragsnummer", 0) else ""
if skip_empty_name and not name:
continue
if skip_empty_antragsnummer and not antragsnummer:
continue
qualifikation = _to_text(row[col_map.get("Qualifikation", 0)]) if len(row) > col_map.get("Qualifikation", 0) else ""
funktion = _to_text(row[col_map.get("Funktion", 0)]) if len(row) > col_map.get("Funktion", 0) else ""
stellenanteil_raw = row[col_map.get("Stellenanteil in %", 0)] if len(row) > col_map.get("Stellenanteil in %", 0) else None
stellenanteil = _to_percent_string(stellenanteil_raw)
monat = _to_text(row[col_map.get("Monat (MM.JJ)", 0)]) if len(row) > col_map.get("Monat (MM.JJ)", 0) else ""
taetigkeit = _to_text(row[col_map.get("Tätigkeit", 0)]) if len(row) > col_map.get("Tätigkeit", 0) else ""
record_id = _to_int_or_none(row[col_map.get("ID", 0)]) if len(row) > col_map.get("ID", 0) else None
kommentar = _to_text(row[col_map.get("Kommentar", 1)]) if "Kommentar" in col_map and len(row) > col_map["Kommentar"] else None
records.append(EmployeeRecord(
id=record_id,
name=name,
antragsnummer=antragsnummer,
qualifikation=qualifikation,
funktion=funktion,
stellenanteil=stellenanteil,
monat=monat,
taetigkeit=taetigkeit,
kommentar=kommentar,
))
wb.close()
return records
def group_by_employee(records: list[EmployeeRecord]) -> dict[str, list[EmployeeRecord]]:
"""Gruppiert Records nach Mitarbeitername."""
groups: dict[str, list[EmployeeRecord]] = {}
for record in records:
if record.name not in groups:
groups[record.name] = []
groups[record.name].append(record)
return groups
def get_unique_antragsnummern(records: list[EmployeeRecord]) -> list[str]:
"""Gibt eine sortierte Liste aller eindeutigen Antragsnummern zurück."""
return sorted(set(r.antragsnummer for r in records if r.antragsnummer))
def get_unique_employees(records: list[EmployeeRecord]) -> list[str]:
"""Gibt eine sortierte Liste aller eindeutigen Mitarbeiternamen zurück."""
return sorted(set(r.name for r in records if r.name))