Implement business validation rules for output generation

This commit is contained in:
Rob committed 2026-04-21 15:35:26 +02:00
1 parent 70873f3726
commit 7501499080
2 files changed
+146 -8

No files matched your search

+7 -7
View File
@@ -59,12 +59,12 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon
- [x] Monatsmarker parsebar - [x] Monatsmarker parsebar
- [x] Gewuenschter Monat exakt einmal vorhanden - [x] Gewuenschter Monat exakt einmal vorhanden
- [x] Stunden-/Vertragswerte gueltig - [x] Stunden-/Vertragswerte gueltig
- [ ] T10: Business-Validierung implementieren - [x] T10: Business-Validierung implementieren
- [ ] Mindestens ein aktiver Fall vorhanden - [x] Mindestens ein aktiver Fall vorhanden
- [ ] Jeder aktive Fall hat eindeutigen Match oder klaren Fehler - [x] Jeder aktive Fall hat eindeutigen Match oder klaren Fehler
- [ ] Template enthaelt genau ein Quellblatt - [x] Template enthaelt genau ein Quellblatt
- [ ] Dateinamenkollisionen vermeiden - [x] Dateinamenkollisionen vermeiden
- [ ] Pflicht-Zielzellen vollstaendig konfiguriert - [x] Pflicht-Zielzellen vollstaendig konfiguriert
## Phase 6 - Ausgabeerzeugung ## Phase 6 - Ausgabeerzeugung
- [ ] T11: Template-Verarbeitung bauen - [ ] T11: Template-Verarbeitung bauen
@@ -111,4 +111,4 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon
8. T15 -> T16 -> T17 8. T15 -> T16 -> T17
## Aktueller Startpunkt ## Aktueller Startpunkt
- [ ] Naechste aktive Aufgabe: T10 (Business-Validierung implementieren) - [ ] Naechste aktive Aufgabe: T11 (Template-Verarbeitung bauen)
+139 -1
View File
@@ -9,7 +9,7 @@ from typing import Any
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.utils.cell import column_index_from_string from openpyxl.utils.cell import column_index_from_string
from .models import ValidationIssue from .models import AllocationRecord, MatchedAssignment, ValidationIssue
def _to_text(value: Any) -> str: def _to_text(value: Any) -> str:
@@ -232,3 +232,141 @@ def validate_sources(
allocation_wb.close() allocation_wb.close()
master_wb.close() master_wb.close()
return issues return issues
def _split_name_for_filename(name: str) -> tuple[str, str]:
cleaned = _to_text(name)
if "," in cleaned:
last_name, first_name = [part.strip() for part in cleaned.split(",", 1)]
return first_name or "Unknown", last_name or "Unknown"
parts = cleaned.split()
if len(parts) >= 2:
return parts[0], parts[-1]
if len(parts) == 1:
return parts[0], "Unknown"
return "Unknown", "Unknown"
def _build_output_filename(pattern: str, year: int, month: int, employee_name: str) -> str:
first_name, last_name = _split_name_for_filename(employee_name)
safe_first = first_name.replace(" ", "_")
safe_last = last_name.replace(" ", "_")
return pattern.format(
year=year,
month=f"{month:02d}",
first_name=safe_first,
last_name=safe_last,
)
def validate_business_rules(
config: dict[str, Any],
report_month: int,
report_year: int,
allocation_records: list[AllocationRecord],
matched_assignments: list[MatchedAssignment],
) -> list[ValidationIssue]:
issues: list[ValidationIssue] = []
if len(allocation_records) == 0:
issues.append(
ValidationIssue(
code="NO_ACTIVE_ASSIGNMENT",
message="No active allocation record found for the requested month.",
severity="error",
)
)
matched_keys: set[tuple[str, str, float]] = set()
for item in matched_assignments:
key = (
item.allocation.employee_name,
item.allocation.project_key,
item.allocation.funded_hours_weekly,
)
if key in matched_keys:
issues.append(
ValidationIssue(
code="DUPLICATE_MATCH",
message=(
f"Duplicate matched assignment for '{item.allocation.employee_name}' "
f"and project '{item.allocation.project_key}'."
),
severity="error",
context=item.allocation.employee_name,
)
)
matched_keys.add(key)
paths_cfg = config["paths"]
files_cfg = config["files"]
project_root = Path.cwd()
template_path = (
project_root
/ paths_cfg["template_dir"]
/ files_cfg["template_workbook"]
)
if template_path.exists():
template_wb = load_workbook(template_path, data_only=True)
if len(template_wb.sheetnames) != 1:
issues.append(
ValidationIssue(
code="TEMPLATE_SHEET_COUNT",
message=(
f"Template workbook must contain exactly one sheet, found {len(template_wb.sheetnames)}."
),
severity="error",
)
)
template_wb.close()
else:
issues.append(
ValidationIssue(
code="MISSING_TEMPLATE",
message=f"Template workbook not found: {template_path}",
severity="error",
)
)
template_targets = config.get("template_targets", {})
missing_targets = [name for name, cell in template_targets.items() if not _to_text(cell)]
if missing_targets:
issues.append(
ValidationIssue(
code="MISSING_TEMPLATE_TARGETS",
message=(
"Mandatory template target cells not configured: "
+ ", ".join(missing_targets)
),
severity="error",
)
)
filename_pattern = config["output"]["filename_pattern"]
filename_to_employee: dict[str, str] = {}
for assignment in matched_assignments:
filename = _build_output_filename(
filename_pattern,
report_year,
report_month,
assignment.allocation.employee_name,
)
previous_owner = filename_to_employee.get(filename)
if previous_owner is None:
filename_to_employee[filename] = assignment.allocation.employee_name
continue
if previous_owner != assignment.allocation.employee_name:
issues.append(
ValidationIssue(
code="FILENAME_COLLISION",
message=(
f"Output filename collision between '{previous_owner}' and "
f"'{assignment.allocation.employee_name}' -> {filename}"
),
severity="error",
)
)
return issues