Andrey Epifancev 412a322b9d chore(arm_statements): копия пакета arm-statements@6bce80b (#52)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:38:43 +04:00

174 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from decimal import Decimal as D
import pytest
from arm_statements.layout import (
code_column,
find_code_column,
find_header_rows,
find_name_column,
first_code_row,
is_numbering_row,
name_cell,
segment_roles,
)
from arm_statements.loader import CodeSection, Rules
from arm_statements.model import Cell, DocumentType, RowRole
from arm_statements.tests.builders import excel, ocr, place
BALANCE_ROWS = [
["Наименование показателя", "Код", "На 30.09.2025", "На 31.12.2024"],
["1", "2", "3", "4"],
["Нематериальные активы", "1110", "10", "9"],
["Основные средства", "1150", "20", "19"],
["Итого по разделу I", "1100", "30", "28"],
]
def test_code_column_by_caption(rules: Rules) -> None:
cand = ocr(BALANCE_ROWS)
assert (find_code_column(cand.rows, 0, 4, rules), first_code_row(cand.rows, 0, 4, 1, rules)) == (1, 2)
def test_title_block_kody_is_not_code_column(rules: Rules) -> None:
title = [
place({5: v}) for v in ["Коды", "0710001", "30 09 2025", "12345678", "7700000000", "12267", "16", "384", "", ""]
]
table = [
place({0: n, 2: c, 3: D(a)})
for n, c, a in [
("Запасы", "1210", "5"),
("НДС", "1220", "1"),
("Дебиторы", "1230", "4"),
("Итого", "1200", "10"),
]
]
cand = excel([("Баланс", [*title, *table])])
assert [find_code_column(cand.rows, 0, len(cand.rows) - 1, rules)] == [2]
def test_two_line_document(rules: Rules) -> None:
cand = ocr([["Наименование", "Код", "2025"], ["Выручка", "2110", "5"], ["Прибыль", "2400", "1"]])
assert find_code_column(cand.rows, 0, 2, rules) == 1
def test_relaxed_search_finds_single_code(rules: Rules) -> None:
cand = ocr([["Чистая прибыль", "", "2400", "7", "6"]])
assert find_code_column(cand.rows, 0, 0, rules) is None
assert [find_code_column(cand.rows, 0, 0, rules, relaxed_section=CodeSection.INCOME)] == [2]
def test_public_code_column(rules: Rules) -> None:
assert code_column(ocr(BALANCE_ROWS).rows, rules) == 1
single = ocr([["Чистая прибыль", "", "2400", "7", "6"]]).rows
assert (code_column(single, rules), code_column(single, rules, relaxed=DocumentType.INCOME)) == (None, 2)
assert code_column(single, rules, relaxed=DocumentType.BALANCE) is None and code_column((), rules) is None
with pytest.raises(ValueError, match="DISCLOSURE"):
code_column(single, rules, relaxed=DocumentType.DISCLOSURE)
def test_numbering_rows(rules: Rules) -> None:
def row(*texts: str) -> tuple[Cell | None, ...]:
return ocr([list(texts)]).rows[0]
assert is_numbering_row(row("1", "2", "3", "4", "5"), rules)
assert is_numbering_row(row("1", "2", "З", "4"), rules)
assert is_numbering_row(row("2", "3", "4", "5"), rules)
assert is_numbering_row(row("1", "2", "4", "5"), rules)
assert not is_numbering_row(row("1", "3", "5"), rules)
assert not is_numbering_row(row("1", "2"), rules)
def test_header_row_roles_and_title(rules: Rules) -> None:
rows = [["Бухгалтерский баланс", "", "", ""], *BALANCE_ROWS]
cand = ocr(rows)
header = find_header_rows(cand.rows, 0, 3, 1, rules)
assert header == (1,)
roles = segment_roles(cand.rows, 0, 5, header, rules)
assert roles == [RowRole.TITLE, RowRole.HEADER, RowRole.NUMBERING, RowRole.DATA, RowRole.DATA, RowRole.DATA]
assert RowRole.TITLE not in segment_roles(cand.rows, 3, 5, (), rules)
def test_two_row_header_is_glued(rules: Rules) -> None:
cand = ocr(
[
["", "", "На 30 сентября", "На 31 декабря"],
["Наименование", "Код", "2025 г.", "2024 г."],
["Запасы", "1210", "5", "4"],
]
)
assert find_header_rows(cand.rows, 0, 2, 1, rules) == (0, 1)
single = ocr(
[
["Раздел", "", "", ""],
["Наименование", "Код", "На 30.09.2025", "На 31.12.2024"],
["Запасы", "1210", "5", "4"],
]
)
assert find_header_rows(single.rows, 0, 2, 1, rules) == (1,)
def test_name_column_without_caption(rules: Rules) -> None:
cand = ocr(
[
["5.1", "Запасы", "1210", "5"],
["5.2", "Дебиторская задолженность", "1230", "4"],
["", "Итого", "1200", "9"],
]
)
assert find_name_column(cand.rows, (), 0, 2, 2, rules) == (1, 0)
def test_header_without_code_caption_skips_numbering(rules: Rules) -> None:
cand = ocr(
[
["Титул 2025", "", "", ""],
["Показатель", "", "На 30.09.2025", "На 31.12.2024"],
["1", "2", "3", "4"],
["Запасы", "1210", "5", "4"],
]
)
assert find_header_rows(cand.rows, 0, 3, 1, rules) == (1,)
assert find_header_rows(cand.rows, 3, 3, 1, rules) == ()
def test_name_cell_falls_back_to_left_text(rules: Rules) -> None:
row = ocr([["5.1", "Запасы", "", "1210"]]).rows[0]
cell = name_cell(row, 2, 3, 0)
assert cell is not None and cell.text == "Запасы"
assert name_cell(ocr([["5.1", "", "", "1210"]]).rows[0], 2, 3, 0) is None
def test_two_row_header_with_caption_in_upper_row_is_glued_with_lower(rules: Rules) -> None:
"""I-1: «Код» в верхней строке, годы — в нижней (шаблон банка)."""
cand = ocr(
[
["Наименование", "Код", "На 30 сентября", "На 31 декабря", "На 31 декабря"],
["", "", "2025 г.", "2024 г.", "2023 г."],
["Запасы", "1210", "5", "4", "3"],
]
)
header = find_header_rows(cand.rows, 0, 2, 1, rules)
assert header == (0, 1)
assert segment_roles(cand.rows, 0, 2, header, rules) == [RowRole.HEADER, RowRole.HEADER, RowRole.DATA]
def test_two_row_header_lower_skips_numbering_and_ignores_unrelated_row(rules: Rules) -> None:
glued = ocr(
[
["Наименование", "Код", "На 30 сентября", "На 31 декабря"],
["", "", "2025 г.", "2024 г."],
["1", "2", "3", "4"],
["Запасы", "1210", "5", "4"],
]
)
assert find_header_rows(glued.rows, 0, 3, 1, rules) == (0, 1)
plain = ocr(
[
["Наименование", "Код", "На 30.09.2025", "На 31.12.2024"],
["Справочно", "", "", ""],
["Запасы", "1210", "5", "4"],
]
)
assert find_header_rows(plain.rows, 0, 2, 1, rules) == (0,)