Implement business validation rules for output generation
This commit is contained in:
1 parent
70873f3726
commit
7501499080
2 files changed
+146
-8
No files matched your search
@@ -59,12 +59,12 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon
|
||||
- [x] Monatsmarker parsebar
|
||||
- [x] Gewuenschter Monat exakt einmal vorhanden
|
||||
- [x] Stunden-/Vertragswerte gueltig
|
||||
- [ ] T10: Business-Validierung implementieren
|
||||
- [ ] Mindestens ein aktiver Fall vorhanden
|
||||
- [ ] Jeder aktive Fall hat eindeutigen Match oder klaren Fehler
|
||||
- [ ] Template enthaelt genau ein Quellblatt
|
||||
- [ ] Dateinamenkollisionen vermeiden
|
||||
- [ ] Pflicht-Zielzellen vollstaendig konfiguriert
|
||||
- [x] T10: Business-Validierung implementieren
|
||||
- [x] Mindestens ein aktiver Fall vorhanden
|
||||
- [x] Jeder aktive Fall hat eindeutigen Match oder klaren Fehler
|
||||
- [x] Template enthaelt genau ein Quellblatt
|
||||
- [x] Dateinamenkollisionen vermeiden
|
||||
- [x] Pflicht-Zielzellen vollstaendig konfiguriert
|
||||
|
||||
## Phase 6 - Ausgabeerzeugung
|
||||
- [ ] T11: Template-Verarbeitung bauen
|
||||
@@ -111,4 +111,4 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon
|
||||
8. T15 -> T16 -> T17
|
||||
|
||||
## Aktueller Startpunkt
|
||||
- [ ] Naechste aktive Aufgabe: T10 (Business-Validierung implementieren)
|
||||
- [ ] Naechste aktive Aufgabe: T11 (Template-Verarbeitung bauen)
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
from openpyxl import load_workbook
|
||||
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:
|
||||
@@ -232,3 +232,141 @@ def validate_sources(
|
||||
allocation_wb.close()
|
||||
master_wb.close()
|
||||
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
|
||||
Reference in new issue
Block a user