diff --git a/README.md b/README.md index 76101d9..25a5692 100644 --- a/README.md +++ b/README.md @@ -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 (ставит конвейер) diff --git a/arm_statements/pipeline.py b/arm_statements/pipeline.py index aadcd0a..8e34d1a 100644 --- a/arm_statements/pipeline.py +++ b/arm_statements/pipeline.py @@ -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) diff --git a/arm_statements/tests/test_rows.py b/arm_statements/tests/test_rows.py index 2c33bff..87ae28d 100644 --- a/arm_statements/tests/test_rows.py +++ b/arm_statements/tests/test_rows.py @@ -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") diff --git a/arm_statements/tests/test_segments.py b/arm_statements/tests/test_segments.py index 83c536d..d636409 100644 --- a/arm_statements/tests/test_segments.py +++ b/arm_statements/tests/test_segments.py @@ -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)) diff --git a/arm_statements/tests/test_text_codes.py b/arm_statements/tests/test_text_codes.py index 17567c7..77c94d3 100644 --- a/arm_statements/tests/test_text_codes.py +++ b/arm_statements/tests/test_text_codes.py @@ -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) diff --git a/arm_statements/text.py b/arm_statements/text.py index 70ac37d..e4dd601 100644 --- a/arm_statements/text.py +++ b/arm_statements/text.py @@ -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)