Use masterdata project scope column for assignment disambiguation

This commit is contained in:
Rob committed 2026-04-21 16:41:59 +02:00
1 parent bd2c0d512a
commit 4dd320878d
5 files changed
+36 -16

No files matched your search

+4 -3
View File
@@ -41,9 +41,10 @@
"antragsnummer": "B", "antragsnummer": "B",
"name": "C", "name": "C",
"qualifikation": "D", "qualifikation": "D",
"stellenanteil": "E", "project_scope": "E",
"funktion": "F", "stellenanteil": "F",
"taetigkeit": "G" "funktion": "G",
"taetigkeit": "H"
}, },
"decimal": { "decimal": {
"decimal_separator": ",", "decimal_separator": ",",
+19 -8
View File
@@ -46,12 +46,23 @@ def _name_keys(name: str) -> set[str]:
def _disambiguate_by_project_key( def _disambiguate_by_project_key(
candidates: list, candidates: list[MasterDataRecord],
project_key: str, project_key: str,
) -> object: ) -> MasterDataRecord | None:
"""Return the single candidate whose Funktion contains the project key, or None.""" """Return the single candidate matching allocation project key via masterdata project_scope."""
key = project_key.strip().casefold() parts = [
matched = [c for c in candidates if key in c.funktion.casefold()] token.strip().casefold()
for token in re.split(r"[/,;|]", project_key)
if token.strip()
]
project_tokens = set(parts or [project_key.strip().casefold()])
matched = []
for candidate in candidates:
candidate_scope = (candidate.project_scope or "PS").strip().casefold()
if candidate_scope in project_tokens:
matched.append(candidate)
return matched[0] if len(matched) == 1 else None return matched[0] if len(matched) == 1 else None
@@ -105,7 +116,7 @@ def match_assignments(
if len(candidates) == 1: if len(candidates) == 1:
selected = candidates[0] selected = candidates[0]
else: else:
# Multiple candidates: disambiguate by project key in Funktion # Multiple candidates: disambiguate by explicit project scope column.
selected = _disambiguate_by_project_key(candidates, allocation.project_key) selected = _disambiguate_by_project_key(candidates, allocation.project_key)
if selected is None: if selected is None:
issues.append( issues.append(
@@ -114,8 +125,8 @@ def match_assignments(
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 Funktion. " f"none or multiple match project_scope. "
f"Funktionen: {[c.funktion for c in candidates]}" f"project_scope values: {[c.project_scope for c in candidates]}"
), ),
severity="error", severity="error",
context=allocation.employee_name, context=allocation.employee_name,
+1
View File
@@ -19,6 +19,7 @@ class MasterDataRecord:
antragsnummer: str antragsnummer: str
name: str name: str
qualifikation: str qualifikation: str
project_scope: str
stellenanteil_percent: float stellenanteil_percent: float
funktion: str funktion: str
taetigkeit: str taetigkeit: str
+11 -5
View File
@@ -243,6 +243,7 @@ def parse_masterdata_records(
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"])
project_scope_col = column_index_from_string(columns_cfg["project_scope"])
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"])
@@ -254,12 +255,16 @@ def parse_masterdata_records(
if not name: if not name:
continue continue
stellenanteil = _parse_german_decimal( stellenanteil_raw = worksheet.cell(row=row_index, column=stellenanteil_col).value
worksheet.cell(row=row_index, column=stellenanteil_col).value, stellenanteil = _to_float_or_none(stellenanteil_raw)
field_name=f"stellenanteil row {row_index}", if stellenanteil is None:
decimal_separator=decimal_cfg.get("decimal_separator", ","), stellenanteil = 0.0
thousands_separator=decimal_cfg.get("thousands_separator", "."),
project_scope = _to_text(
worksheet.cell(row=row_index, column=project_scope_col).value
) )
if not project_scope:
project_scope = "PS"
records.append( records.append(
MasterDataRecord( MasterDataRecord(
@@ -271,6 +276,7 @@ def parse_masterdata_records(
qualifikation=_to_text( qualifikation=_to_text(
worksheet.cell(row=row_index, column=qualifikation_col).value worksheet.cell(row=row_index, column=qualifikation_col).value
), ),
project_scope=project_scope,
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(
+1
View File
@@ -132,6 +132,7 @@ def validate_sources(
master_cols["antragsnummer"], master_cols["antragsnummer"],
master_cols["name"], master_cols["name"],
master_cols["qualifikation"], master_cols["qualifikation"],
master_cols["project_scope"],
master_cols["stellenanteil"], master_cols["stellenanteil"],
master_cols["funktion"], master_cols["funktion"],
master_cols["taetigkeit"], master_cols["taetigkeit"],