Fix inherited contract handling and document smoke test blockers
This commit is contained in:
1 parent
0be2646952
commit
418d8336f5
4 files changed
+258
-210
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
|
## Phase 8 - Tests und Abnahme
|
||||||
- [ ] T15: Smoke-Test fuer End-to-End-Lauf
|
- [ ] T15: Smoke-Test fuer End-to-End-Lauf
|
||||||
- [ ] Beispielmonat ausfuehren
|
- [x] Beispielmonat ausfuehren
|
||||||
- [ ] Dateien in `output/` entstehen erwartungsgemaess
|
- [ ] 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
|
- [ ] T16: Fachtests definieren und ausfuehren
|
||||||
- [ ] Mehrfachzuordnung eines Mitarbeiters
|
- [ ] Mehrfachzuordnung eines Mitarbeiters
|
||||||
- [ ] Fehlender Match
|
- [ ] Fehlender Match
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import unicodedata
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .errors import ValidationError
|
from .errors import ValidationError
|
||||||
@@ -21,7 +22,30 @@ def calculate_foerderquote_percent(allocation: AllocationRecord, precision: int
|
|||||||
|
|
||||||
def _normalize_name(name: str) -> str:
|
def _normalize_name(name: str) -> str:
|
||||||
compact = re.sub(r"\s+", " ", name.strip())
|
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(
|
def _with_tolerance(
|
||||||
@@ -43,14 +67,19 @@ def match_assignments(
|
|||||||
|
|
||||||
master_by_name: dict[str, list[MasterDataRecord]] = {}
|
master_by_name: dict[str, list[MasterDataRecord]] = {}
|
||||||
for record in masterdata_records:
|
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] = []
|
matched: list[MatchedAssignment] = []
|
||||||
issues: list[ValidationIssue] = []
|
issues: list[ValidationIssue] = []
|
||||||
|
|
||||||
for allocation in allocation_records:
|
for allocation in allocation_records:
|
||||||
normalized_name = _normalize_name(allocation.employee_name)
|
candidate_map: dict[str, MasterDataRecord] = {}
|
||||||
candidates = master_by_name.get(normalized_name, [])
|
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:
|
if not candidates:
|
||||||
issues.append(
|
issues.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
|
|||||||
+208
-200
@@ -1,4 +1,4 @@
|
|||||||
"""Parsing and normalization for source workbooks."""
|
"""Parsing and normalization for source workbooks."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -14,262 +14,270 @@ from .models import AllocationRecord, MasterDataRecord
|
|||||||
|
|
||||||
|
|
||||||
def _to_text(value: Any) -> str:
|
def _to_text(value: Any) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
def _to_float(value: Any, field_name: str) -> float:
|
def _to_float(value: Any, field_name: str) -> float:
|
||||||
if value is None or str(value).strip() == "":
|
if value is None or str(value).strip() == "":
|
||||||
raise ValidationError(f"Missing numeric value for {field_name}.")
|
raise ValidationError(f"Missing numeric value for {field_name}.")
|
||||||
|
|
||||||
text = str(value).strip().replace(",", ".")
|
text = str(value).strip().replace(",", ".")
|
||||||
try:
|
try:
|
||||||
return float(text)
|
return float(text)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValidationError(f"Invalid numeric value for {field_name}: {value}") from exc
|
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(
|
def _parse_german_decimal(
|
||||||
value: Any,
|
value: Any,
|
||||||
field_name: str,
|
field_name: str,
|
||||||
decimal_separator: str,
|
decimal_separator: str,
|
||||||
thousands_separator: str,
|
thousands_separator: str,
|
||||||
) -> float:
|
) -> float:
|
||||||
if value is None or str(value).strip() == "":
|
if value is None or str(value).strip() == "":
|
||||||
raise ValidationError(f"Missing numeric value for {field_name}.")
|
raise ValidationError(f"Missing numeric value for {field_name}.")
|
||||||
|
|
||||||
text = str(value).strip()
|
text = str(value).strip()
|
||||||
if thousands_separator:
|
if thousands_separator:
|
||||||
text = text.replace(thousands_separator, "")
|
text = text.replace(thousands_separator, "")
|
||||||
if decimal_separator:
|
if decimal_separator:
|
||||||
text = text.replace(decimal_separator, ".")
|
text = text.replace(decimal_separator, ".")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return float(text)
|
return float(text)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValidationError(f"Invalid numeric value for {field_name}: {value}") from 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:
|
def _parse_month_marker(value: Any, prefix: str, date_format: str) -> datetime | None:
|
||||||
marker = _to_text(value)
|
marker = _to_text(value)
|
||||||
if not marker:
|
if not marker:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not marker.lower().startswith(prefix.lower()):
|
if not marker.lower().startswith(prefix.lower()):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
date_text = marker[len(prefix) :].strip()
|
date_text = marker[len(prefix) :].strip()
|
||||||
return datetime.strptime(date_text, date_format)
|
return datetime.strptime(date_text, date_format)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_month_column_index(
|
def _resolve_month_column_index(
|
||||||
worksheet: Any,
|
worksheet: Any,
|
||||||
header_row: int,
|
header_row: int,
|
||||||
first_month_col: str,
|
first_month_col: str,
|
||||||
target_month: int,
|
target_month: int,
|
||||||
target_year: int,
|
target_year: int,
|
||||||
month_prefix: str,
|
month_prefix: str,
|
||||||
month_date_format: str,
|
month_date_format: str,
|
||||||
) -> int:
|
) -> int:
|
||||||
start_index = column_index_from_string(first_month_col)
|
start_index = column_index_from_string(first_month_col)
|
||||||
matched_columns: list[int] = []
|
matched_columns: list[int] = []
|
||||||
|
|
||||||
for column_index in range(start_index, worksheet.max_column + 1):
|
for column_index in range(start_index, worksheet.max_column + 1):
|
||||||
value = worksheet.cell(row=header_row, column=column_index).value
|
value = worksheet.cell(row=header_row, column=column_index).value
|
||||||
parsed = _parse_month_marker(value, month_prefix, month_date_format)
|
parsed = _parse_month_marker(value, month_prefix, month_date_format)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
continue
|
continue
|
||||||
if parsed.month == target_month and parsed.year == target_year:
|
if parsed.month == target_month and parsed.year == target_year:
|
||||||
matched_columns.append(column_index)
|
matched_columns.append(column_index)
|
||||||
|
|
||||||
if len(matched_columns) == 0:
|
if len(matched_columns) == 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"No month column found for {target_month:02d}/{target_year} in allocation workbook."
|
f"No month column found for {target_month:02d}/{target_year} in allocation workbook."
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(matched_columns) > 1:
|
if len(matched_columns) > 1:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Multiple month columns found for {target_month:02d}/{target_year} in allocation workbook."
|
f"Multiple month columns found for {target_month:02d}/{target_year} in allocation workbook."
|
||||||
)
|
)
|
||||||
|
|
||||||
return matched_columns[0]
|
return matched_columns[0]
|
||||||
|
|
||||||
|
|
||||||
def _is_inactive_value(value: Any, inactive_values: list[Any]) -> bool:
|
def _is_inactive_value(value: Any, inactive_values: list[Any]) -> bool:
|
||||||
normalized = _to_text(value)
|
normalized = _to_text(value)
|
||||||
if normalized in {"", "-"}:
|
if normalized in {"", "-"}:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for inactive in inactive_values:
|
for inactive in inactive_values:
|
||||||
if str(inactive).strip() == normalized:
|
if str(inactive).strip() == normalized:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def parse_allocation_records(
|
def parse_allocation_records(
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
report_month: int,
|
report_month: int,
|
||||||
report_year: int,
|
report_year: int,
|
||||||
project_root: Path,
|
project_root: Path,
|
||||||
) -> list[AllocationRecord]:
|
) -> list[AllocationRecord]:
|
||||||
allocation_cfg = config["allocation"]
|
allocation_cfg = config["allocation"]
|
||||||
columns_cfg = allocation_cfg["columns"]
|
columns_cfg = allocation_cfg["columns"]
|
||||||
files_cfg = config["files"]
|
files_cfg = config["files"]
|
||||||
sheets_cfg = config["sheets"]
|
sheets_cfg = config["sheets"]
|
||||||
paths_cfg = config["paths"]
|
paths_cfg = config["paths"]
|
||||||
|
|
||||||
workbook_path = (
|
workbook_path = (
|
||||||
project_root
|
project_root
|
||||||
/ paths_cfg["input_dir"]
|
/ paths_cfg["input_dir"]
|
||||||
/ files_cfg["allocation_workbook"]
|
/ files_cfg["allocation_workbook"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not workbook_path.exists():
|
if not workbook_path.exists():
|
||||||
raise ValidationError(f"Allocation workbook not found: {workbook_path}")
|
raise ValidationError(f"Allocation workbook not found: {workbook_path}")
|
||||||
|
|
||||||
workbook = load_workbook(workbook_path, data_only=True)
|
workbook = load_workbook(workbook_path, data_only=True)
|
||||||
sheet_name = sheets_cfg["allocation"]
|
sheet_name = sheets_cfg["allocation"]
|
||||||
if sheet_name not in workbook.sheetnames:
|
if sheet_name not in workbook.sheetnames:
|
||||||
raise ValidationError(f"Sheet '{sheet_name}' not found in {workbook_path.name}")
|
raise ValidationError(f"Sheet '{sheet_name}' not found in {workbook_path.name}")
|
||||||
worksheet = workbook[sheet_name]
|
worksheet = workbook[sheet_name]
|
||||||
|
|
||||||
header_row = allocation_cfg["header_month_marker_row"]
|
header_row = allocation_cfg["header_month_marker_row"]
|
||||||
month_marker_cfg = allocation_cfg["month_marker"]
|
month_marker_cfg = allocation_cfg["month_marker"]
|
||||||
month_column = _resolve_month_column_index(
|
month_column = _resolve_month_column_index(
|
||||||
worksheet=worksheet,
|
worksheet=worksheet,
|
||||||
header_row=header_row,
|
header_row=header_row,
|
||||||
first_month_col=columns_cfg["first_month_column"],
|
first_month_col=columns_cfg["first_month_column"],
|
||||||
target_month=report_month,
|
target_month=report_month,
|
||||||
target_year=report_year,
|
target_year=report_year,
|
||||||
month_prefix=month_marker_cfg["prefix"],
|
month_prefix=month_marker_cfg["prefix"],
|
||||||
month_date_format=month_marker_cfg["date_format"],
|
month_date_format=month_marker_cfg["date_format"],
|
||||||
)
|
)
|
||||||
|
|
||||||
employee_col = column_index_from_string(columns_cfg["employee_name"])
|
employee_col = column_index_from_string(columns_cfg["employee_name"])
|
||||||
contract_col = column_index_from_string(columns_cfg["contract_hours_weekly"])
|
contract_col = column_index_from_string(columns_cfg["contract_hours_weekly"])
|
||||||
project_col = column_index_from_string(columns_cfg["project_key"])
|
project_col = column_index_from_string(columns_cfg["project_key"])
|
||||||
remarks_col = column_index_from_string(columns_cfg["remarks"])
|
remarks_col = column_index_from_string(columns_cfg["remarks"])
|
||||||
inactive_values = allocation_cfg.get("inactive_values", ["", "-", "0", 0])
|
inactive_values = allocation_cfg.get("inactive_values", ["", "-", "0", 0])
|
||||||
|
|
||||||
current_employee = ""
|
current_employee = ""
|
||||||
current_contract_hours: float | None = None
|
current_contract_hours: float | None = None
|
||||||
normalized_records: list[AllocationRecord] = []
|
normalized_records: list[AllocationRecord] = []
|
||||||
|
|
||||||
for row_index in range(header_row + 1, worksheet.max_row + 1):
|
for row_index in range(header_row + 1, worksheet.max_row + 1):
|
||||||
row_employee = _to_text(worksheet.cell(row=row_index, column=employee_col).value)
|
row_employee = _to_text(worksheet.cell(row=row_index, column=employee_col).value)
|
||||||
row_contract = worksheet.cell(row=row_index, column=contract_col).value
|
row_contract = worksheet.cell(row=row_index, column=contract_col).value
|
||||||
row_project = _to_text(worksheet.cell(row=row_index, column=project_col).value)
|
row_project = _to_text(worksheet.cell(row=row_index, column=project_col).value)
|
||||||
|
|
||||||
if row_employee:
|
if row_employee:
|
||||||
current_employee = row_employee
|
current_employee = row_employee
|
||||||
current_contract_hours = _to_float(
|
current_contract_hours = _to_float_or_none(row_contract)
|
||||||
row_contract,
|
|
||||||
f"contract_hours_weekly row {row_index}",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not row_project:
|
if not row_project:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not current_employee or current_contract_hours is None:
|
if not current_employee or current_contract_hours is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Project row {row_index} has no inherited employee context."
|
f"Project row {row_index} has no inherited employee context."
|
||||||
)
|
)
|
||||||
|
|
||||||
month_value = worksheet.cell(row=row_index, column=month_column).value
|
month_value = worksheet.cell(row=row_index, column=month_column).value
|
||||||
if _is_inactive_value(month_value, inactive_values):
|
if _is_inactive_value(month_value, inactive_values):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
funded_hours = _to_float(month_value, f"funded_hours row {row_index}")
|
funded_hours = _to_float(month_value, f"funded_hours row {row_index}")
|
||||||
if funded_hours <= 0:
|
if funded_hours <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if current_contract_hours <= 0:
|
if current_contract_hours <= 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Contract hours must be > 0 for employee '{current_employee}'"
|
f"Contract hours must be > 0 for employee '{current_employee}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
notes = _to_text(worksheet.cell(row=row_index, column=remarks_col).value)
|
notes = _to_text(worksheet.cell(row=row_index, column=remarks_col).value)
|
||||||
normalized_records.append(
|
normalized_records.append(
|
||||||
AllocationRecord(
|
AllocationRecord(
|
||||||
employee_name=current_employee,
|
employee_name=current_employee,
|
||||||
contract_hours_weekly=current_contract_hours,
|
contract_hours_weekly=current_contract_hours,
|
||||||
project_key=row_project,
|
project_key=row_project,
|
||||||
funded_hours_weekly=funded_hours,
|
funded_hours_weekly=funded_hours,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
workbook.close()
|
workbook.close()
|
||||||
return normalized_records
|
return normalized_records
|
||||||
|
|
||||||
|
|
||||||
def parse_masterdata_records(
|
def parse_masterdata_records(
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
project_root: Path,
|
project_root: Path,
|
||||||
) -> list[MasterDataRecord]:
|
) -> list[MasterDataRecord]:
|
||||||
files_cfg = config["files"]
|
files_cfg = config["files"]
|
||||||
sheets_cfg = config["sheets"]
|
sheets_cfg = config["sheets"]
|
||||||
paths_cfg = config["paths"]
|
paths_cfg = config["paths"]
|
||||||
master_cfg = config["masterdata"]
|
master_cfg = config["masterdata"]
|
||||||
columns_cfg = master_cfg["columns"]
|
columns_cfg = master_cfg["columns"]
|
||||||
decimal_cfg = master_cfg["decimal"]
|
decimal_cfg = master_cfg["decimal"]
|
||||||
|
|
||||||
workbook_path = (
|
workbook_path = (
|
||||||
project_root
|
project_root
|
||||||
/ paths_cfg["input_dir"]
|
/ paths_cfg["input_dir"]
|
||||||
/ files_cfg["masterdata_workbook"]
|
/ files_cfg["masterdata_workbook"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not workbook_path.exists():
|
if not workbook_path.exists():
|
||||||
raise ValidationError(f"Master data workbook not found: {workbook_path}")
|
raise ValidationError(f"Master data workbook not found: {workbook_path}")
|
||||||
|
|
||||||
workbook = load_workbook(workbook_path, data_only=True)
|
workbook = load_workbook(workbook_path, data_only=True)
|
||||||
sheet_name = sheets_cfg["masterdata"]
|
sheet_name = sheets_cfg["masterdata"]
|
||||||
if sheet_name not in workbook.sheetnames:
|
if sheet_name not in workbook.sheetnames:
|
||||||
raise ValidationError(f"Sheet '{sheet_name}' not found in {workbook_path.name}")
|
raise ValidationError(f"Sheet '{sheet_name}' not found in {workbook_path.name}")
|
||||||
worksheet = workbook[sheet_name]
|
worksheet = workbook[sheet_name]
|
||||||
|
|
||||||
id_col = column_index_from_string(columns_cfg["id"])
|
id_col = column_index_from_string(columns_cfg["id"])
|
||||||
antragsnummer_col = column_index_from_string(columns_cfg["antragsnummer"])
|
antragsnummer_col = column_index_from_string(columns_cfg["antragsnummer"])
|
||||||
name_col = column_index_from_string(columns_cfg["name"])
|
name_col = column_index_from_string(columns_cfg["name"])
|
||||||
qualifikation_col = column_index_from_string(columns_cfg["qualifikation"])
|
qualifikation_col = column_index_from_string(columns_cfg["qualifikation"])
|
||||||
stellenanteil_col = column_index_from_string(columns_cfg["stellenanteil"])
|
stellenanteil_col = column_index_from_string(columns_cfg["stellenanteil"])
|
||||||
funktion_col = column_index_from_string(columns_cfg["funktion"])
|
funktion_col = column_index_from_string(columns_cfg["funktion"])
|
||||||
taetigkeit_col = column_index_from_string(columns_cfg["taetigkeit"])
|
taetigkeit_col = column_index_from_string(columns_cfg["taetigkeit"])
|
||||||
|
|
||||||
records: list[MasterDataRecord] = []
|
records: list[MasterDataRecord] = []
|
||||||
|
|
||||||
for row_index in range(2, worksheet.max_row + 1):
|
for row_index in range(2, worksheet.max_row + 1):
|
||||||
name = _to_text(worksheet.cell(row=row_index, column=name_col).value)
|
name = _to_text(worksheet.cell(row=row_index, column=name_col).value)
|
||||||
if not name:
|
if not name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stellenanteil = _parse_german_decimal(
|
stellenanteil = _parse_german_decimal(
|
||||||
worksheet.cell(row=row_index, column=stellenanteil_col).value,
|
worksheet.cell(row=row_index, column=stellenanteil_col).value,
|
||||||
field_name=f"stellenanteil row {row_index}",
|
field_name=f"stellenanteil row {row_index}",
|
||||||
decimal_separator=decimal_cfg.get("decimal_separator", ","),
|
decimal_separator=decimal_cfg.get("decimal_separator", ","),
|
||||||
thousands_separator=decimal_cfg.get("thousands_separator", "."),
|
thousands_separator=decimal_cfg.get("thousands_separator", "."),
|
||||||
)
|
)
|
||||||
|
|
||||||
records.append(
|
records.append(
|
||||||
MasterDataRecord(
|
MasterDataRecord(
|
||||||
record_id=_to_text(worksheet.cell(row=row_index, column=id_col).value),
|
record_id=_to_text(worksheet.cell(row=row_index, column=id_col).value),
|
||||||
antragsnummer=_to_text(
|
antragsnummer=_to_text(
|
||||||
worksheet.cell(row=row_index, column=antragsnummer_col).value
|
worksheet.cell(row=row_index, column=antragsnummer_col).value
|
||||||
),
|
),
|
||||||
name=name,
|
name=name,
|
||||||
qualifikation=_to_text(
|
qualifikation=_to_text(
|
||||||
worksheet.cell(row=row_index, column=qualifikation_col).value
|
worksheet.cell(row=row_index, column=qualifikation_col).value
|
||||||
),
|
),
|
||||||
stellenanteil_percent=stellenanteil,
|
stellenanteil_percent=stellenanteil,
|
||||||
funktion=_to_text(worksheet.cell(row=row_index, column=funktion_col).value),
|
funktion=_to_text(worksheet.cell(row=row_index, column=funktion_col).value),
|
||||||
taetigkeit=_to_text(
|
taetigkeit=_to_text(
|
||||||
worksheet.cell(row=row_index, column=taetigkeit_col).value
|
worksheet.cell(row=row_index, column=taetigkeit_col).value
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
workbook.close()
|
workbook.close()
|
||||||
return records
|
return records
|
||||||
@@ -194,22 +194,31 @@ def validate_sources(
|
|||||||
|
|
||||||
if len(month_matches) == 1:
|
if len(month_matches) == 1:
|
||||||
month_column = month_matches[0]
|
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"])
|
contract_col = column_index_from_string(allocation_cols["contract_hours_weekly"])
|
||||||
project_col = column_index_from_string(allocation_cols["project_key"])
|
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):
|
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)
|
project_key = _to_text(allocation_ws.cell(row=row_idx, column=project_col).value)
|
||||||
if not project_key:
|
if not project_key:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
contract_value = _to_float_or_none(
|
if current_contract_value is None or current_contract_value <= 0:
|
||||||
allocation_ws.cell(row=row_idx, column=contract_col).value
|
|
||||||
)
|
|
||||||
if contract_value is None or contract_value <= 0:
|
|
||||||
issues.append(
|
issues.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
code="INVALID_CONTRACT_HOURS",
|
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",
|
severity="error",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user