Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa6ef61d4 | ||
|
|
e5bb23a666 | ||
|
|
4165b9fdbc | ||
|
|
716b8b5567 | ||
|
|
decc86c279 |
No files matched your search
+3
-1
@@ -1 +1,3 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
|
*.xlsx\#
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# SAB Taetigkeitsnachweise Tool
|
# SAB Tätigkeitsnachweise Tool
|
||||||
|
|
||||||
|
Automatisierte Generierung monatlicher Tätigkeitsnachweise (SAB Stellenförderung). Das Tool liest eine zentrale Stundenzuordnung und Stammdaten aus Excel-Dateien, ordnet Mitarbeiter zu Projekten zu und erzeugt pro Mitarbeiter eine vorausgefüllte Excel-Datei auf Basis der SAB-Vorlage.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -9,12 +11,107 @@
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local run command
|
## Ausführung
|
||||||
|
|
||||||
Use module execution from the project root:
|
Haupteinstiegspunkt ist `generate.py` (Click-basierte CLI):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m src.taetigkeitsnachweis.cli --version
|
python generate.py --month 5 --year 2026
|
||||||
```
|
```
|
||||||
|
|
||||||
Current CLI is a scaffold. Full generation pipeline is implemented in later tasks.
|
### CLI-Optionen
|
||||||
|
|
||||||
|
| Option | Typ | Pflicht | Beschreibung |
|
||||||
|
|--------|-----|---------|--------------|
|
||||||
|
| `--month` | 1–12 | Ja | Berichtsmonat |
|
||||||
|
| `--year` | 2000–2100 | Ja | Berichtsjahr |
|
||||||
|
| `--yes` | Flag | Nein | Interaktive Bestätigung überspringen |
|
||||||
|
| `--skip-errors` | Flag | Nein | Generierung trotz nicht-zuordenbarer Einträge fortsetzen |
|
||||||
|
|
||||||
|
## Implementierte Features
|
||||||
|
|
||||||
|
### Pipeline (generate.py)
|
||||||
|
|
||||||
|
Vollständige Generierungs-Pipeline mit folgenden Schritten:
|
||||||
|
|
||||||
|
1. **Konfiguration laden** – `config.json` wird eingelesen
|
||||||
|
2. **Quellvalidierung** – Prüfung der Eingabedateien auf Existenz, Sheets, Spalten, Monatsmarker
|
||||||
|
3. **Parsing** – Stundenzuordnung und Stammdaten aus Excel einlesen
|
||||||
|
4. **Matching** – Zuordnung der Mitarbeiter über normalisierte Namensabgleiche
|
||||||
|
5. **Geschäftsregel-Validierung** – Duplikate, fehlende Template-Sheets, Output-Kollisionen
|
||||||
|
6. **Vorschau** – Zusammenfassung der geplanten Ausgabe (Dry-Run-Ansicht)
|
||||||
|
7. **Bestätigung** – Interaktive Abfrage (überspringbar mit `--yes`)
|
||||||
|
8. **Generierung** – Excel-Workbooks erstellen und in `output/` speichern
|
||||||
|
|
||||||
|
### Parser (src/taetigkeitsnachweis/parser.py)
|
||||||
|
|
||||||
|
- **Stundenzuordnung parsen** (`parse_allocation_records`): Liest `PS_PW.xlsx`, erkennt Monatsspalte über Datumsmarker (`ab DD.MM.YYYY`), vererbt Mitarbeiter-/Vertragsstunden-Kontext über Zeilen
|
||||||
|
- **Stammdaten parsen** (`parse_masterdata_records`): Liest `Stellenbeschreibungen.xlsx` mit Antragsnummer, Qualifikation, Funktion, Tätigkeiten etc.
|
||||||
|
- Erkennung inaktiver Werte (Elternzeit, Kündigung, etc.)
|
||||||
|
|
||||||
|
### Matcher (src/taetigkeitsnachweis/matcher.py)
|
||||||
|
|
||||||
|
- **Namens-Normalisierung**: Trimming, Akzente entfernen, Casefolding, Leerzeichenbereinigung
|
||||||
|
- **Flexible Namens-Zuordnung**: Unterstützt „Nachname, Vorname" ↔ „Vorname Nachname"
|
||||||
|
- **Disambiguierung**: Bei mehreren Stammdaten-Treffern wird über `project_scope` aufgelöst
|
||||||
|
- **Förderquote-Berechnung**: `(geförderte Stunden / Vertragsstunden) × 100`
|
||||||
|
- Erzeugt Validierungsmeldungen: `MISSING_MASTERDATA`, `AMBIGUOUS_MASTERDATA`, `ALLOCATION_NOTE`
|
||||||
|
|
||||||
|
### Validator (src/taetigkeitsnachweis/validator.py)
|
||||||
|
|
||||||
|
Zweistufige Validierung:
|
||||||
|
|
||||||
|
- **Quellvalidierung** (`validate_sources`): Datei-Existenz, Sheet-Existenz, Spaltenprüfung, Monatsmarker-Parsebarkeit, Vertragsstunden-Prüfung
|
||||||
|
- **Geschäftsregel-Validierung** (`validate_business_rules`): Keine aktiven Zuordnungen, doppelte Matches, Template-Sheet-Existenz, Template-Targets-Vollständigkeit, Dateinamen-Kollisionen
|
||||||
|
|
||||||
|
### Writer (src/taetigkeitsnachweis/writer.py)
|
||||||
|
|
||||||
|
- **Workbook-Generierung** (`build_employee_workbooks`): Lädt SAB-Vorlage, erstellt pro Zuordnung ein Sheet, befüllt konfigurierte Zielzellen (Antragsnummer, Name, Qualifikation, Funktion, Tätigkeiten, Berichtsmonat, Förderquote, Erstelldatum)
|
||||||
|
- **Dateinamen-Generierung** (`build_output_filename`): Pattern-basiert mit `{year}`, `{month}`, `{first_name}`, `{last_name}`
|
||||||
|
- **Speicherung** (`save_employee_workbooks`): Schreibt in `output/`, erkennt Dateinamen-Kollisionen
|
||||||
|
|
||||||
|
### Datenmodelle (src/taetigkeitsnachweis/models.py)
|
||||||
|
|
||||||
|
- `AllocationRecord` – Mitarbeitername, Vertragsstunden, Projektkürzel, geförderte Stunden, Anmerkungen
|
||||||
|
- `MasterDataRecord` – Antragsnummer, Name, Qualifikation, Projektumfang, Stellenanteil, Funktion, Tätigkeit
|
||||||
|
- `MatchedAssignment` – Verknüpfung Zuordnung + Stammdaten + berechnete Förderquote
|
||||||
|
- `ValidationIssue` – Code, Meldung, Schweregrad, Kontext
|
||||||
|
|
||||||
|
### Fehlerbehandlung (src/taetigkeitsnachweis/errors.py)
|
||||||
|
|
||||||
|
Exception-Hierarchie: `TaetigkeitsnachweisError` → `ConfigurationError`, `ValidationError`, `MatchingError`
|
||||||
|
|
||||||
|
### Konfiguration (config.json)
|
||||||
|
|
||||||
|
Zentrale Konfigurationsdatei mit:
|
||||||
|
|
||||||
|
- **Pfade**: Input-, Template- und Output-Verzeichnisse
|
||||||
|
- **Dateien**: Zuordnungs-Workbook, Stammdaten-Workbook, Template-Workbook
|
||||||
|
- **Sheet-Namen**: Pro Workbook konfigurierbar
|
||||||
|
- **Parsing-Regeln**: Spalten-Mappings, Monatsmarker-Format, inaktive Werte
|
||||||
|
- **Matching-Einstellungen**: Normalisierung, Toleranz
|
||||||
|
- **Output-Einstellungen**: Dateinamen-Pattern, ein Workbook pro Mitarbeiter, ein Sheet pro Zuordnung
|
||||||
|
- **Template-Targets**: Zielzellen-Adressen in der Vorlage
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
Tool/
|
||||||
|
├── generate.py # Haupteinstiegspunkt (Click CLI)
|
||||||
|
├── config.json # Zentrale Konfiguration
|
||||||
|
├── start.sh # Shell-Wrapper
|
||||||
|
├── requirements.txt # Python-Abhängigkeiten
|
||||||
|
├── input/ # Eingabe-Excel-Dateien (PS_PW.xlsx, Stellenbeschreibungen.xlsx)
|
||||||
|
├── output/ # Generierte Tätigkeitsnachweise
|
||||||
|
├── template/ # SAB-Vorlage (Tätigkeitsnachweis Stellenförderung.xlsx)
|
||||||
|
└── src/taetigkeitsnachweis/
|
||||||
|
├── cli.py # Legacy-Scaffold (argparse)
|
||||||
|
├── errors.py # Exception-Klassen
|
||||||
|
├── logging_utils.py # Logging-Konfiguration
|
||||||
|
├── matcher.py # Namenszuordnung & Förderquote
|
||||||
|
├── models.py # Datenklassen
|
||||||
|
├── parser.py # Excel-Parsing
|
||||||
|
├── util.py # Utilities (noch leer)
|
||||||
|
├── validator.py # Zwei-Phasen-Validierung
|
||||||
|
└── writer.py # Excel-Ausgabe
|
||||||
|
```
|
||||||
Binary file not shown.
@@ -1,34 +0,0 @@
|
|||||||
"""CLI entrypoint and orchestration layer."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog="taetigkeitsnachweis",
|
|
||||||
description="SAB Taetigkeitsnachweise generator",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--version",
|
|
||||||
action="store_true",
|
|
||||||
help="Show tool version and exit.",
|
|
||||||
)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = build_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.version:
|
|
||||||
print("taetigkeitsnachweis 0.1.0")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print("CLI scaffold ready. Full pipeline will be added in Task T13.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -48,22 +48,28 @@ def _name_keys(name: str) -> set[str]:
|
|||||||
def _disambiguate_by_project_key(
|
def _disambiguate_by_project_key(
|
||||||
candidates: list[MasterDataRecord],
|
candidates: list[MasterDataRecord],
|
||||||
project_key: str,
|
project_key: str,
|
||||||
) -> MasterDataRecord | None:
|
) -> tuple[MasterDataRecord | None, str]:
|
||||||
"""Return the single candidate matching allocation project key via masterdata project_scope."""
|
"""Return the single candidate matching allocation project key via masterdata taetigkeit.
|
||||||
parts = [
|
|
||||||
token.strip().casefold()
|
Returns a tuple of (matched_record, status) where status is one of:
|
||||||
for token in re.split(r"[/,;|]", project_key)
|
- "ok": exactly one match found
|
||||||
if token.strip()
|
- "no_match": no candidate has a matching taetigkeit
|
||||||
]
|
- "multiple": more than one candidate matches
|
||||||
project_tokens = set(parts or [project_key.strip().casefold()])
|
"""
|
||||||
|
normalized_key = project_key.strip().casefold()
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
candidate_scope = (candidate.project_scope or "PS").strip().casefold()
|
candidate_taetigkeit = (candidate.taetigkeit or "").strip().casefold()
|
||||||
if candidate_scope in project_tokens:
|
if candidate_taetigkeit == normalized_key:
|
||||||
matched.append(candidate)
|
matched.append(candidate)
|
||||||
|
|
||||||
return matched[0] if len(matched) == 1 else None
|
if len(matched) == 1:
|
||||||
|
return matched[0], "ok"
|
||||||
|
elif len(matched) == 0:
|
||||||
|
return None, "no_match"
|
||||||
|
else:
|
||||||
|
return None, "multiple"
|
||||||
|
|
||||||
|
|
||||||
def match_assignments(
|
def match_assignments(
|
||||||
@@ -103,10 +109,11 @@ def match_assignments(
|
|||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
code="MISSING_MASTERDATA",
|
code="MISSING_MASTERDATA",
|
||||||
message=(
|
message=(
|
||||||
f"No master-data match for employee '{allocation.employee_name}' "
|
f"In der Stellenbeschreibung ist kein Datensatz für "
|
||||||
f"(project '{allocation.project_key}')."
|
f"'{allocation.employee_name}' hinterlegt "
|
||||||
|
f"(Projekt '{allocation.project_key}'). Keine Förderung möglich."
|
||||||
),
|
),
|
||||||
severity="error",
|
severity="info",
|
||||||
context=allocation.employee_name,
|
context=allocation.employee_name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -116,17 +123,32 @@ def match_assignments(
|
|||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
selected = candidates[0]
|
selected = candidates[0]
|
||||||
else:
|
else:
|
||||||
# Multiple candidates: disambiguate by explicit project scope column.
|
# Multiple candidates: disambiguate by Tätigkeitsschlüssel.
|
||||||
selected = _disambiguate_by_project_key(candidates, allocation.project_key)
|
selected, match_status = _disambiguate_by_project_key(candidates, allocation.project_key)
|
||||||
if selected is None:
|
if match_status == "no_match":
|
||||||
|
issues.append(
|
||||||
|
ValidationIssue(
|
||||||
|
code="NO_MATCHING_TAETIGKEIT",
|
||||||
|
message=(
|
||||||
|
f"No Tätigkeitsschlüssel '{allocation.project_key}' found in "
|
||||||
|
f"master-data for '{allocation.employee_name}'. "
|
||||||
|
f"Available: {[c.taetigkeit for c in candidates]}. "
|
||||||
|
f"Keine Förderung möglich."
|
||||||
|
),
|
||||||
|
severity="warning",
|
||||||
|
context=allocation.employee_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
elif selected is None:
|
||||||
issues.append(
|
issues.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
code="AMBIGUOUS_MASTERDATA",
|
code="AMBIGUOUS_MASTERDATA",
|
||||||
message=(
|
message=(
|
||||||
f"Ambiguous master-data for '{allocation.employee_name}' / "
|
f"Ambiguous master-data for '{allocation.employee_name}' / "
|
||||||
f"'{allocation.project_key}': {len(candidates)} rows, "
|
f"'{allocation.project_key}': {len(candidates)} rows, "
|
||||||
f"none or multiple match project_scope. "
|
f"multiple match taetigkeit. "
|
||||||
f"project_scope values: {[c.project_scope for c in candidates]}"
|
f"taetigkeit values: {[c.taetigkeit for c in candidates]}"
|
||||||
),
|
),
|
||||||
severity="error",
|
severity="error",
|
||||||
context=allocation.employee_name,
|
context=allocation.employee_name,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Always run from this script's directory.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
PYTHON_BIN="python3"
|
||||||
|
elif command -v python >/dev/null 2>&1; then
|
||||||
|
PYTHON_BIN="python"
|
||||||
|
else
|
||||||
|
echo "Error: Python was not found on PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using Python: $PYTHON_BIN"
|
||||||
|
|
||||||
|
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
|
||||||
|
if [[ -d "$VENV_DIR" ]]; then
|
||||||
|
echo "Existing virtual environment is not compatible with this OS. Recreating $VENV_DIR ..."
|
||||||
|
rm -rf "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
echo "Creating virtual environment in $VENV_DIR ..."
|
||||||
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VENV_PYTHON="$VENV_DIR/bin/python"
|
||||||
|
|
||||||
|
echo "Upgrading pip and packaging tools ..."
|
||||||
|
"$VENV_PYTHON" -m pip install --upgrade pip
|
||||||
|
|
||||||
|
echo "Installing requirements ..."
|
||||||
|
"$VENV_PYTHON" -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
echo "Starting program ..."
|
||||||
|
"$VENV_PYTHON" "generate.py" --month 5 --year 2026
|
||||||
Reference in new issue
Block a user