Fix inherited contract handling and document smoke test blockers
This commit is contained in:
1 parent
0be2646952
commit
418d8336f5
4 files changed
+63
-15
No files matched your search
@@ -88,8 +88,10 @@ Projekt vollstaendig umsetzen: aus PS_PW.xlsx und Stellenbeschreibungen.xlsx mon
|
||||
|
||||
## Phase 8 - Tests und Abnahme
|
||||
- [ ] T15: Smoke-Test fuer End-to-End-Lauf
|
||||
- [ ] Beispielmonat ausfuehren
|
||||
- [x] Beispielmonat ausfuehren
|
||||
- [ ] Dateien in `output/` entstehen erwartungsgemaess
|
||||
- [ ] Blocker: Masterdata-Matches fehlen fuer mehrere aktive Namen.
|
||||
- [ ] Blocker: Template-Regel verletzt (aktuell 5 Sheets statt 1) und template_targets sind noch nicht konfiguriert.
|
||||
- [ ] T16: Fachtests definieren und ausfuehren
|
||||
- [ ] Mehrfachzuordnung eines Mitarbeiters
|
||||
- [ ] Fehlender Match
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Any
|
||||
|
||||
from .errors import ValidationError
|
||||
@@ -21,7 +22,30 @@ def calculate_foerderquote_percent(allocation: AllocationRecord, precision: int
|
||||
|
||||
def _normalize_name(name: str) -> str:
|
||||
compact = re.sub(r"\s+", " ", name.strip())
|
||||
return compact.casefold()
|
||||
no_accents = "".join(
|
||||
ch for ch in unicodedata.normalize("NFKD", compact) if not unicodedata.combining(ch)
|
||||
)
|
||||
return no_accents.casefold()
|
||||
|
||||
|
||||
def _name_keys(name: str) -> set[str]:
|
||||
normalized = _normalize_name(name)
|
||||
keys = {normalized}
|
||||
|
||||
if "," in normalized:
|
||||
last_name, first_name = [part.strip() for part in normalized.split(",", 1)]
|
||||
if first_name and last_name:
|
||||
keys.add(f"{first_name} {last_name}")
|
||||
keys.add(f"{last_name}, {first_name}")
|
||||
else:
|
||||
tokens = normalized.split()
|
||||
if len(tokens) >= 2:
|
||||
first_name = tokens[0]
|
||||
last_name = tokens[-1]
|
||||
keys.add(f"{first_name} {last_name}")
|
||||
keys.add(f"{last_name}, {first_name}")
|
||||
|
||||
return {re.sub(r"\s+", " ", key).strip() for key in keys if key.strip()}
|
||||
|
||||
|
||||
def _with_tolerance(
|
||||
@@ -43,14 +67,19 @@ def match_assignments(
|
||||
|
||||
master_by_name: dict[str, list[MasterDataRecord]] = {}
|
||||
for record in masterdata_records:
|
||||
master_by_name.setdefault(_normalize_name(record.name), []).append(record)
|
||||
for key in _name_keys(record.name):
|
||||
master_by_name.setdefault(key, []).append(record)
|
||||
|
||||
matched: list[MatchedAssignment] = []
|
||||
issues: list[ValidationIssue] = []
|
||||
|
||||
for allocation in allocation_records:
|
||||
normalized_name = _normalize_name(allocation.employee_name)
|
||||
candidates = master_by_name.get(normalized_name, [])
|
||||
candidate_map: dict[str, MasterDataRecord] = {}
|
||||
for key in _name_keys(allocation.employee_name):
|
||||
for candidate in master_by_name.get(key, []):
|
||||
candidate_map[candidate.record_id] = candidate
|
||||
|
||||
candidates = list(candidate_map.values())
|
||||
if not candidates:
|
||||
issues.append(
|
||||
ValidationIssue(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Parsing and normalization for source workbooks."""
|
||||
"""Parsing and normalization for source workbooks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -30,6 +30,17 @@ def _to_float(value: Any, field_name: str) -> float:
|
||||
raise ValidationError(f"Invalid numeric value for {field_name}: {value}") from exc
|
||||
|
||||
|
||||
def _to_float_or_none(value: Any) -> float | None:
|
||||
if value is None or str(value).strip() == "":
|
||||
return None
|
||||
|
||||
text = str(value).strip().replace(",", ".")
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_german_decimal(
|
||||
value: Any,
|
||||
field_name: str,
|
||||
@@ -164,10 +175,7 @@ def parse_allocation_records(
|
||||
|
||||
if row_employee:
|
||||
current_employee = row_employee
|
||||
current_contract_hours = _to_float(
|
||||
row_contract,
|
||||
f"contract_hours_weekly row {row_index}",
|
||||
)
|
||||
current_contract_hours = _to_float_or_none(row_contract)
|
||||
|
||||
if not row_project:
|
||||
continue
|
||||
|
||||
@@ -194,22 +194,31 @@ def validate_sources(
|
||||
|
||||
if len(month_matches) == 1:
|
||||
month_column = month_matches[0]
|
||||
employee_col = column_index_from_string(allocation_cols["employee_name"])
|
||||
contract_col = column_index_from_string(allocation_cols["contract_hours_weekly"])
|
||||
project_col = column_index_from_string(allocation_cols["project_key"])
|
||||
|
||||
current_contract_value: float | None = None
|
||||
|
||||
for row_idx in range(header_row + 1, allocation_ws.max_row + 1):
|
||||
employee_value = _to_text(allocation_ws.cell(row=row_idx, column=employee_col).value)
|
||||
contract_raw = allocation_ws.cell(row=row_idx, column=contract_col).value
|
||||
parsed_contract = _to_float_or_none(contract_raw)
|
||||
|
||||
if employee_value:
|
||||
current_contract_value = parsed_contract
|
||||
|
||||
project_key = _to_text(allocation_ws.cell(row=row_idx, column=project_col).value)
|
||||
if not project_key:
|
||||
continue
|
||||
|
||||
contract_value = _to_float_or_none(
|
||||
allocation_ws.cell(row=row_idx, column=contract_col).value
|
||||
)
|
||||
if contract_value is None or contract_value <= 0:
|
||||
if current_contract_value is None or current_contract_value <= 0:
|
||||
issues.append(
|
||||
ValidationIssue(
|
||||
code="INVALID_CONTRACT_HOURS",
|
||||
message=f"Invalid contract weekly hours at row {row_idx}.",
|
||||
message=(
|
||||
f"Invalid inherited contract weekly hours at row {row_idx}."
|
||||
),
|
||||
severity="error",
|
||||
)
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user