From f2da062c49a92ece91efc5ff7bca823a528879c3 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 21 Apr 2026 15:28:13 +0200 Subject: [PATCH] Add master data parser with German decimal conversion --- .github/copilot-tasks.md | 10 ++-- src/taetigkeitsnachweis/parser.py | 93 ++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/.github/copilot-tasks.md b/.github/copilot-tasks.md index cf19780..fa502a1 100644 --- a/.github/copilot-tasks.md +++ b/.github/copilot-tasks.md @@ -36,10 +36,10 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon - [x] Mitarbeiterkontext ueber Mehrzeilen-Eintraege vererben - [x] Werte normalisieren (`-`, `0`, leer, numerisch) - [x] Normalisierte Records erzeugen -- [ ] T6: Parser fuer Stellenbeschreibungen.xlsx implementieren - - [ ] Felder ID, Antragsnummer, Name, Qualifikation, Stellenanteil, Funktion, Taetigkeit einlesen - - [ ] Prozentwerte (deutsches Dezimalformat) robust in Zahlen umwandeln - - [ ] Mehrfache Eintraege je Name korrekt behalten +- [x] T6: Parser fuer Stellenbeschreibungen.xlsx implementieren + - [x] Felder ID, Antragsnummer, Name, Qualifikation, Stellenanteil, Funktion, Taetigkeit einlesen + - [x] Prozentwerte (deutsches Dezimalformat) robust in Zahlen umwandeln + - [x] Mehrfache Eintraege je Name korrekt behalten ## Phase 4 - Matching und Fachlogik - [ ] T7: Foerderquote berechnen @@ -111,4 +111,4 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon 8. T15 -> T16 -> T17 ## Aktueller Startpunkt -- [ ] Naechste aktive Aufgabe: T6 (Parser fuer Stellenbeschreibungen.xlsx implementieren) +- [ ] Naechste aktive Aufgabe: T7 (Foerderquote berechnen) diff --git a/src/taetigkeitsnachweis/parser.py b/src/taetigkeitsnachweis/parser.py index dfd8b32..45cc403 100644 --- a/src/taetigkeitsnachweis/parser.py +++ b/src/taetigkeitsnachweis/parser.py @@ -10,7 +10,7 @@ from openpyxl import load_workbook from openpyxl.utils.cell import column_index_from_string from .errors import ValidationError -from .models import AllocationRecord +from .models import AllocationRecord, MasterDataRecord def _to_text(value: Any) -> str: @@ -30,6 +30,27 @@ def _to_float(value: Any, field_name: str) -> float: raise ValidationError(f"Invalid numeric value for {field_name}: {value}") from exc +def _parse_german_decimal( + value: Any, + field_name: str, + decimal_separator: str, + thousands_separator: str, +) -> float: + if value is None or str(value).strip() == "": + raise ValidationError(f"Missing numeric value for {field_name}.") + + text = str(value).strip() + if thousands_separator: + text = text.replace(thousands_separator, "") + if decimal_separator: + text = text.replace(decimal_separator, ".") + + try: + return float(text) + except ValueError as exc: + raise ValidationError(f"Invalid numeric value for {field_name}: {value}") from exc + + def _parse_month_marker(value: Any, prefix: str, date_format: str) -> datetime | None: marker = _to_text(value) if not marker: @@ -182,3 +203,73 @@ def parse_allocation_records( workbook.close() return normalized_records + + +def parse_masterdata_records( + config: dict[str, Any], + project_root: Path, +) -> list[MasterDataRecord]: + files_cfg = config["files"] + sheets_cfg = config["sheets"] + paths_cfg = config["paths"] + master_cfg = config["masterdata"] + columns_cfg = master_cfg["columns"] + decimal_cfg = master_cfg["decimal"] + + workbook_path = ( + project_root + / paths_cfg["input_dir"] + / files_cfg["masterdata_workbook"] + ) + + if not workbook_path.exists(): + raise ValidationError(f"Master data workbook not found: {workbook_path}") + + workbook = load_workbook(workbook_path, data_only=True) + sheet_name = sheets_cfg["masterdata"] + if sheet_name not in workbook.sheetnames: + raise ValidationError(f"Sheet '{sheet_name}' not found in {workbook_path.name}") + worksheet = workbook[sheet_name] + + id_col = column_index_from_string(columns_cfg["id"]) + antragsnummer_col = column_index_from_string(columns_cfg["antragsnummer"]) + name_col = column_index_from_string(columns_cfg["name"]) + qualifikation_col = column_index_from_string(columns_cfg["qualifikation"]) + stellenanteil_col = column_index_from_string(columns_cfg["stellenanteil"]) + funktion_col = column_index_from_string(columns_cfg["funktion"]) + taetigkeit_col = column_index_from_string(columns_cfg["taetigkeit"]) + + records: list[MasterDataRecord] = [] + + for row_index in range(2, worksheet.max_row + 1): + name = _to_text(worksheet.cell(row=row_index, column=name_col).value) + if not name: + continue + + stellenanteil = _parse_german_decimal( + worksheet.cell(row=row_index, column=stellenanteil_col).value, + field_name=f"stellenanteil row {row_index}", + decimal_separator=decimal_cfg.get("decimal_separator", ","), + thousands_separator=decimal_cfg.get("thousands_separator", "."), + ) + + records.append( + MasterDataRecord( + record_id=_to_text(worksheet.cell(row=row_index, column=id_col).value), + antragsnummer=_to_text( + worksheet.cell(row=row_index, column=antragsnummer_col).value + ), + name=name, + qualifikation=_to_text( + worksheet.cell(row=row_index, column=qualifikation_col).value + ), + stellenanteil_percent=stellenanteil, + funktion=_to_text(worksheet.cell(row=row_index, column=funktion_col).value), + taetigkeit=_to_text( + worksheet.cell(row=row_index, column=taetigkeit_col).value + ), + ) + ) + + workbook.close() + return records