Документирование warnings адаптера, тест листа-продолжения, обезличенная фамилия (M-2, M-3, M-4)
M-2: README и docstring normalize_candidate — при отсутствии документа candidate.warnings хост обрабатывает сам. M-3: тесты сопоставления по дате (переставленные даты) и по порядку (дубль даты). M-4: «Косарев» заменён на «Иванов» в тестах и docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
5cc4522e4a
commit
2973cc7966
@ -96,9 +96,11 @@ NormalizationResult(documents: tuple[StatementDocument, ...], file_warnings: tup
|
||||
|
||||
```python
|
||||
normalize_candidate(candidate: FormCandidate, rules: Rules | None = None) -> CandidateResult
|
||||
# шаги 2–8 одного кандидата; документа нет — file_warnings (TABLE_LOST | TABLE_SKIPPED | SHEET_OTHER)
|
||||
# шаги 2–8 одного кандидата; документа нет — file_warnings (TABLE_LOST | TABLE_SKIPPED | SHEET_OTHER);
|
||||
# при этом candidate.warnings адаптера пакет не переносит — хост обрабатывает их сам
|
||||
normalize(candidates: Sequence[FormCandidate], rules: Rules | None = None) -> NormalizationResult
|
||||
# по порядку; исключение кандидата не перехватывается — хост, которому нужна изоляция кандидатов
|
||||
# по порядку; у кандидата без документа candidate.warnings не попадают в результат (см. выше);
|
||||
# исключение кандидата не перехватывается — хост, которому нужна изоляция кандидатов
|
||||
# (TABLE_PARSE_ERROR), зовёт normalize_candidate в цикле
|
||||
recompute(form: StoredForm, rules: Rules | None = None) -> RecomputeResult
|
||||
parse_header(cells: HeaderCells, rules: Rules) -> StatementHeader # form_name = None (ставит конвейер)
|
||||
|
||||
@ -160,7 +160,10 @@ def _lines_document( # noqa: PLR0913 -- сборка документа со с
|
||||
|
||||
|
||||
def normalize_candidate(candidate: FormCandidate, rules: Rules | None = None) -> CandidateResult:
|
||||
"""Шаги 2–8 кандидата; документа нет — предупреждение файла (`TABLE_LOST`, `TABLE_SKIPPED`, `SHEET_OTHER`)."""
|
||||
"""Шаги 2–8 кандидата; документа нет — предупреждение файла (`TABLE_LOST`, `TABLE_SKIPPED`, `SHEET_OTHER`).
|
||||
|
||||
Когда документа нет, `candidate.warnings` (предупреждения адаптера) никуда не попадают: хост обрабатывает их сам.
|
||||
"""
|
||||
rules = rules if rules is not None else default_rules()
|
||||
header = replace(parse_header(candidate.header, rules), form_name=_form_name(candidate))
|
||||
layout = find_layout(candidate, header, rules)
|
||||
|
||||
@ -108,7 +108,7 @@ def test_excel_signature_text_is_not_content(rules: Rules) -> None:
|
||||
rows = [
|
||||
["Наименование", "", "Код", "На 30.09.2025"],
|
||||
["Запасы", "", "1150", "5"],
|
||||
["Руководитель", "", "", "Косарев"],
|
||||
["Руководитель", "", "", "Иванов"],
|
||||
["", "", "", "(расшифровка подписи)"],
|
||||
]
|
||||
lines, _ = lines_of(rows, rules, source="excel")
|
||||
|
||||
@ -158,3 +158,32 @@ def test_repeat_header_in_two_rows(rules: Rules) -> None:
|
||||
assert [s.first_row for s in layout.segments] == [0, 3]
|
||||
assert layout.roles[4] is RowRole.HEADER and layout.roles[5] is RowRole.DATA
|
||||
_no_date_noise(layout)
|
||||
|
||||
|
||||
def _npz_sheets(second_dates: list[str]) -> list[tuple[str, list[list[str | D | None]]]]:
|
||||
first = [
|
||||
place({1: "Наименование показателя", 12: "Код", 16: DATES3[0], 21: DATES3[1], 24: DATES3[2]}),
|
||||
place({1: "Нематериальные активы", 12: "1110", 16: D("10"), 21: D("9"), 24: D("8")}),
|
||||
]
|
||||
second = [
|
||||
place({0: "Наименование показателя", 6: "Код", 8: second_dates[0], 9: second_dates[1], 10: second_dates[2]}),
|
||||
place({0: "Заемные средства", 6: "1510", 8: D("3"), 9: D("2"), 10: D("1")}),
|
||||
]
|
||||
return [("Бухгалтерский баланс", first), ("Бухгалтерский баланс. Лист 4", second)]
|
||||
|
||||
|
||||
def test_continuation_sheet_with_permuted_dates_maps_by_date_not_order(rules: Rules) -> None:
|
||||
"""M-3: даты листа-продолжения переставлены — колонки ищутся по дате, а не по порядку."""
|
||||
permuted = [DATES3[2], DATES3[0], DATES3[1]] # 31.12.2023, 30.09.2025, 31.12.2024
|
||||
cand = excel(_npz_sheets(permuted))
|
||||
layout = find_layout(cand, parse_header(hdr(report_date="30.09.2025"), rules), rules)
|
||||
assert layout.segments[1].value_cols == ((8, 24), (9, 16), (10, 21))
|
||||
assert not [w for w in layout.warnings if w.code.startswith("SEGMENT_")]
|
||||
|
||||
|
||||
def test_continuation_sheet_with_duplicate_date_maps_by_order(rules: Rules) -> None:
|
||||
"""M-3: две колонки на одну дату — сопоставление по порядку."""
|
||||
duplicate = [DATES3[0], DATES3[0], DATES3[2]]
|
||||
cand = excel(_npz_sheets(duplicate))
|
||||
layout = find_layout(cand, parse_header(hdr(report_date="30.09.2025"), rules), rules)
|
||||
assert layout.segments[1].value_cols == ((8, 16), (9, 21), (10, 24))
|
||||
|
||||
@ -44,7 +44,7 @@ def test_lookalikes_only_near_digit(rules: Rules) -> None:
|
||||
|
||||
def test_disguised_digit(rules: Rules) -> None:
|
||||
assert is_disguised_digit("I", rules.lookalikes) and is_disguised_digit("| О", rules.lookalikes)
|
||||
assert not is_disguised_digit("Косарев", rules.lookalikes)
|
||||
assert not is_disguised_digit("Иванов", rules.lookalikes)
|
||||
assert not is_disguised_digit("(расшифровка подписи)", rules.lookalikes)
|
||||
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ def has_digit(text: str | None) -> bool:
|
||||
|
||||
|
||||
def is_disguised_digit(text: str, lookalikes: Mapping[str, str]) -> bool:
|
||||
"""После замены двойников везде есть цифра и нет других букв («I», «| О» — да; «Косарев» — нет)."""
|
||||
"""После замены двойников везде есть цифра и нет других букв («I», «| О» — да; «Иванов» — нет)."""
|
||||
fixed = fix_lookalikes(text, lookalikes, anywhere=True)
|
||||
return has_digit(fixed) and not any(char.isalpha() for char in fixed)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user