Документирование 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
|
```python
|
||||||
normalize_candidate(candidate: FormCandidate, rules: Rules | None = None) -> CandidateResult
|
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
|
normalize(candidates: Sequence[FormCandidate], rules: Rules | None = None) -> NormalizationResult
|
||||||
# по порядку; исключение кандидата не перехватывается — хост, которому нужна изоляция кандидатов
|
# по порядку; у кандидата без документа candidate.warnings не попадают в результат (см. выше);
|
||||||
|
# исключение кандидата не перехватывается — хост, которому нужна изоляция кандидатов
|
||||||
# (TABLE_PARSE_ERROR), зовёт normalize_candidate в цикле
|
# (TABLE_PARSE_ERROR), зовёт normalize_candidate в цикле
|
||||||
recompute(form: StoredForm, rules: Rules | None = None) -> RecomputeResult
|
recompute(form: StoredForm, rules: Rules | None = None) -> RecomputeResult
|
||||||
parse_header(cells: HeaderCells, rules: Rules) -> StatementHeader # form_name = None (ставит конвейер)
|
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:
|
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()
|
rules = rules if rules is not None else default_rules()
|
||||||
header = replace(parse_header(candidate.header, rules), form_name=_form_name(candidate))
|
header = replace(parse_header(candidate.header, rules), form_name=_form_name(candidate))
|
||||||
layout = find_layout(candidate, header, rules)
|
layout = find_layout(candidate, header, rules)
|
||||||
|
|||||||
@ -108,7 +108,7 @@ def test_excel_signature_text_is_not_content(rules: Rules) -> None:
|
|||||||
rows = [
|
rows = [
|
||||||
["Наименование", "", "Код", "На 30.09.2025"],
|
["Наименование", "", "Код", "На 30.09.2025"],
|
||||||
["Запасы", "", "1150", "5"],
|
["Запасы", "", "1150", "5"],
|
||||||
["Руководитель", "", "", "Косарев"],
|
["Руководитель", "", "", "Иванов"],
|
||||||
["", "", "", "(расшифровка подписи)"],
|
["", "", "", "(расшифровка подписи)"],
|
||||||
]
|
]
|
||||||
lines, _ = lines_of(rows, rules, source="excel")
|
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 [s.first_row for s in layout.segments] == [0, 3]
|
||||||
assert layout.roles[4] is RowRole.HEADER and layout.roles[5] is RowRole.DATA
|
assert layout.roles[4] is RowRole.HEADER and layout.roles[5] is RowRole.DATA
|
||||||
_no_date_noise(layout)
|
_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:
|
def test_disguised_digit(rules: Rules) -> None:
|
||||||
assert is_disguised_digit("I", rules.lookalikes) and is_disguised_digit("| О", rules.lookalikes)
|
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)
|
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:
|
def is_disguised_digit(text: str, lookalikes: Mapping[str, str]) -> bool:
|
||||||
"""После замены двойников везде есть цифра и нет других букв («I», «| О» — да; «Косарев» — нет)."""
|
"""После замены двойников везде есть цифра и нет других букв («I», «| О» — да; «Иванов» — нет)."""
|
||||||
fixed = fix_lookalikes(text, lookalikes, anywhere=True)
|
fixed = fix_lookalikes(text, lookalikes, anywhere=True)
|
||||||
return has_digit(fixed) and not any(char.isalpha() for char in fixed)
|
return has_digit(fixed) and not any(char.isalpha() for char in fixed)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user