from typing import Any from arm_statements.header import parse_header from arm_statements.loader import Rules from arm_statements.model import LineStatus, StatementWarning from arm_statements.rows import RawLine, collect_raw_lines from arm_statements.segments import find_layout from arm_statements.tests.builders import excel, hdr, ocr HEAD = ["Наименование показателя", "Код", "На 30.09.2025", "На 31.12.2024"] OK, UNREADABLE, DETAIL, MISSING = LineStatus.OK, LineStatus.UNREADABLE, LineStatus.DETAIL, LineStatus.MISSING def lines_of(rows: Any, rules: Rules, source: str = "ocr") -> tuple[list[RawLine], list[StatementWarning]]: cand = ocr(rows) if source == "ocr" else excel([("Баланс", rows)]) layout = find_layout(cand, parse_header(hdr(), rules), rules) return collect_raw_lines(cand, layout, rules) def test_junk_code_lines_survive(rules: Rules) -> None: lines, _ = lines_of( [ HEAD, ["НМА", "1110", "5", "4"], ["Запасы", "1150", "5", "4"], ["Мусор", "ИЧ,250", "7", "6"], ["в том числе: прочее", "", "1", ""], ["", "44 30\nI |", "", ""], ["Итого", "1100", "10", "8"], ], rules, ) assert [(ln.line_status, ln.code) for ln in lines] == [ (OK, "1110"), (OK, "1150"), (UNREADABLE, None), (DETAIL, None), (UNREADABLE, None), (OK, "1100"), ] assert lines[3].parent_ref == lines[1].ref == "row:2" assert [ln.row_index for ln in lines] == [0, 1, 2, 3, 4, 5] def test_lookalike_only_code_cells_are_empty(rules: Rules) -> None: lines, _ = lines_of( [ HEAD, ["НМА", "1110", "5", "4"], ["Запасы", "1150", "5", "4"], ["", "изо :", ("3", 0.0), ""], ["", "НИ", "I", ""], ["", "| 140", "2", "1"], ["Итого", "1100", "12", "9"], ], rules, ) assert [(ln.line_status, ln.code, ln.code_fixed) for ln in lines] == [ (OK, "1110", False), (OK, "1150", False), (DETAIL, None, False), (DETAIL, None, False), (OK, "1140", True), (OK, "1100", False), ] assert [ln.parent_ref for ln in lines[2:4]] == ["row:2", "row:2"] assert len({ln.ref for ln in lines}) == len(lines) def test_missing_before_first_code_and_name_only_row(rules: Rules) -> None: lines, _ = lines_of( [HEAD, ["Выручка", "", "100", "90"], ["Раздел I. Активы", "", "", ""], ["Запасы", "1150", "5", "4"]], rules ) assert [ln.line_status for ln in lines] == [MISSING, OK] def test_duplicate_code_both_ok(rules: Rules) -> None: lines, _ = lines_of([HEAD, ["Налог", "2410", "100", "1"], ["Налог УСН", "2410", "649", "2"]], rules) assert [(ln.line_status, ln.code) for ln in lines] == [(OK, "2410"), (OK, "2410")] def test_split_glued_rows(rules: Rules) -> None: lines, warnings = lines_of( [HEAD, ["Запасы", "1150", "5", "4"], ["Займы\nКредиторы", "1500\n1700", "917 775\n2 371 333", "1\n2"]], rules ) split = lines[1:] assert [(ln.ref, ln.code, ln.split) for ln in split] == [("row:2#1", "1500", True), ("row:2#2", "1700", True)] assert [ln.cells[0][1].text for ln in split if ln.cells[0][1] is not None] == ["917 775", "2 371 333"] assert [ln.name.text for ln in split if ln.name is not None] == ["Займы", "Кредиторы"] assert [(w.code, w.line_refs) for w in warnings] == [("ROW_SPLIT", ("row:2#1", "row:2#2"))] def test_not_split_when_no_part_in_column_range(rules: Rules) -> None: lines, warnings = lines_of( [ HEAD, ["НМА", "1110", "5", "4"], ["Запасы", "1150", "5", "4"], ["", "44 30\nI |", "1\n2", ""], ["Итого", "1100", "10", "8"], ], rules, ) assert [ln.line_status for ln in lines] == [OK, OK, UNREADABLE, OK] and warnings == [] def test_excel_signature_text_is_not_content(rules: Rules) -> None: rows = [ ["Наименование", "", "Код", "На 30.09.2025"], ["Запасы", "", "1150", "5"], ["Руководитель", "", "", "Иванов"], ["", "", "", "(расшифровка подписи)"], ] lines, _ = lines_of(rows, rules, source="excel") assert [ln.ref for ln in lines] == ["Баланс!C2"]