diff --git a/input/Stellenbeschreibungen.xlsx b/input/Stellenbeschreibungen.xlsx index 3dea582..f073970 100644 Binary files a/input/Stellenbeschreibungen.xlsx and b/input/Stellenbeschreibungen.xlsx differ diff --git a/src/taetigkeitsnachweis/matcher.py b/src/taetigkeitsnachweis/matcher.py index 9748b92..36a089a 100644 --- a/src/taetigkeitsnachweis/matcher.py +++ b/src/taetigkeitsnachweis/matcher.py @@ -48,22 +48,28 @@ def _name_keys(name: str) -> set[str]: def _disambiguate_by_project_key( candidates: list[MasterDataRecord], project_key: str, -) -> MasterDataRecord | None: - """Return the single candidate matching allocation project key via masterdata project_scope.""" - parts = [ - token.strip().casefold() - for token in re.split(r"[/,;|]", project_key) - if token.strip() - ] - project_tokens = set(parts or [project_key.strip().casefold()]) +) -> tuple[MasterDataRecord | None, str]: + """Return the single candidate matching allocation project key via masterdata taetigkeit. + + Returns a tuple of (matched_record, status) where status is one of: + - "ok": exactly one match found + - "no_match": no candidate has a matching taetigkeit + - "multiple": more than one candidate matches + """ + normalized_key = project_key.strip().casefold() matched = [] for candidate in candidates: - candidate_scope = (candidate.project_scope or "PS").strip().casefold() - if candidate_scope in project_tokens: + candidate_taetigkeit = (candidate.taetigkeit or "").strip().casefold() + if candidate_taetigkeit == normalized_key: matched.append(candidate) - return matched[0] if len(matched) == 1 else None + if len(matched) == 1: + return matched[0], "ok" + elif len(matched) == 0: + return None, "no_match" + else: + return None, "multiple" def match_assignments( @@ -103,10 +109,11 @@ def match_assignments( ValidationIssue( code="MISSING_MASTERDATA", message=( - f"No master-data match for employee '{allocation.employee_name}' " - f"(project '{allocation.project_key}')." + f"In der Stellenbeschreibung ist kein Datensatz für " + f"'{allocation.employee_name}' hinterlegt " + f"(Projekt '{allocation.project_key}'). Keine Förderung möglich." ), - severity="error", + severity="info", context=allocation.employee_name, ) ) @@ -116,17 +123,32 @@ def match_assignments( if len(candidates) == 1: selected = candidates[0] else: - # Multiple candidates: disambiguate by explicit project scope column. - selected = _disambiguate_by_project_key(candidates, allocation.project_key) - if selected is None: + # Multiple candidates: disambiguate by Tätigkeitsschlüssel. + selected, match_status = _disambiguate_by_project_key(candidates, allocation.project_key) + if match_status == "no_match": + issues.append( + ValidationIssue( + code="NO_MATCHING_TAETIGKEIT", + message=( + f"No Tätigkeitsschlüssel '{allocation.project_key}' found in " + f"master-data for '{allocation.employee_name}'. " + f"Available: {[c.taetigkeit for c in candidates]}. " + f"Keine Förderung möglich." + ), + severity="warning", + context=allocation.employee_name, + ) + ) + continue + elif selected is None: issues.append( ValidationIssue( code="AMBIGUOUS_MASTERDATA", message=( f"Ambiguous master-data for '{allocation.employee_name}' / " f"'{allocation.project_key}': {len(candidates)} rows, " - f"none or multiple match project_scope. " - f"project_scope values: {[c.project_scope for c in candidates]}" + f"multiple match taetigkeit. " + f"taetigkeit values: {[c.taetigkeit for c in candidates]}" ), severity="error", context=allocation.employee_name,