pylib_ai_arm_statements/tests/test_columns.py

214 lines
10 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 collections.abc import Sequence
from datetime import date
from decimal import Decimal as D
from typing import Any
from arm_statements.columns import amount_columns, document_dates, finalize_columns, segment_columns
from arm_statements.header import parse_header
from arm_statements.loader import Rules
from arm_statements.model import HeaderCells, PeriodKind, Severity, Source, StatementColumn, StatementWarning
from tests.builders import excel, hdr, ocr
OCR, EXCEL = Source.OCR, Source.EXCEL
def run( # noqa: PLR0913, PLR0917 - вспомогательная функция тестов, сигнатура задана планом (Task 9)
rows: Sequence[Sequence[Any]],
source: Source,
rules: Rules,
header: HeaderCells | None = None,
header_rows: tuple[int, ...] = (0,),
data_rows: tuple[int, ...] = (1, 2),
) -> tuple[tuple[StatementColumn, ...], list[StatementWarning]]:
cand = ocr(rows) if source is OCR else excel([("Лист1", rows)])
drafts, warnings = segment_columns(cand.rows, header_rows, data_rows, 1 if source is OCR else 2, source, rules)
head = parse_header(header or hdr(), rules)
columns, more = finalize_columns(drafts, document_dates(drafts, head), rules)
return columns, [*warnings, *more]
def test_blank_66n_relative_labels(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", "На отчетную дату", "На 31 декабря предыдущего года",
"На 31 декабря года, предшествующего предыдущему"],
["Запасы", "1210", "5", "4", "3"],
["Итого", "1200", "5", "4", "3"],
] # fmt: skip
columns, warnings = run(rows, OCR, rules, header=hdr(report_date="30.09.2025"))
assert [c.period_date for c in columns] == [date(2025, 9, 30), date(2024, 12, 31), date(2023, 12, 31)]
assert [c.period_kind for c in columns] == [
PeriodKind.CURRENT,
PeriodKind.PREV_YEAR_END,
PeriodKind.PREV_PREV_YEAR_END,
]
assert warnings == []
def test_excel_skipped_change_column(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", "На 31.12.2024", "Изменение, %"],
["Запасы", "", "1210", D("5"), D("4"), D("25")],
["Итого", "", "1200", D("5"), D("4"), D("25")],
]
columns, warnings = run(rows, EXCEL, rules)
assert [c.column_index for c in columns] == [3, 4]
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("COLUMN_SKIPPED", Severity.INFO, 5)]
assert {c.label_confidence for c in columns} == {100.0}
def test_junk_label_over_numbers_is_kept(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", "На 30.09.2025", ("| | г", 17.0)],
["Запасы", "1210", "5", "4"],
["Итого", "1200", "5", "4"],
]
columns, warnings = run(rows, OCR, rules, header=hdr(report_date="30.09.2025"))
junk = columns[1]
assert (junk.label, junk.period_date, junk.period_kind) == ("| | г", None, PeriodKind.NONE)
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", Severity.ERROR, 3)]
def test_empty_label_right_of_dated_columns_is_outside_table(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", "На 30.09.2025", ""],
["Запасы", "1210", "5", "4"],
["Итого", "1200", "5", "4"],
]
columns, warnings = run(rows, OCR, rules)
assert [(c.column_index, c.period_kind) for c in columns] == [(2, PeriodKind.ABSOLUTE)]
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("NUMBERS_OUTSIDE_TABLE", Severity.INFO, None)]
assert warnings[0].locator == "row:1\nrow:2"
def test_numbers_right_of_last_dated_column_are_not_amounts(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", "На 31.12.2024", None, None],
["Запасы", "", "1210", D("5"), D("4"), None, D("9")],
["Баланс", "", "1600", D("5"), D("4"), D("7"), None],
["Итого", "", "1700", D("5"), D("4"), None, None],
]
columns, warnings = run(rows, EXCEL, rules, data_rows=(1, 2, 3))
assert [c.column_index for c in columns] == [3, 4]
assert [(w.code, w.severity) for w in warnings] == [("NUMBERS_OUTSIDE_TABLE", Severity.INFO)]
assert warnings[0].locator == "Лист1!G2\nЛист1!F3"
assert "G2" in warnings[0].message and "F3" in warnings[0].message
def test_numbers_outside_table_do_not_depend_on_cell_count(rules: Rules) -> None:
rows: list[list[Any]] = [["Наименование", "", "Код", "На 30.09.2025", None]]
rows += [["Строка", "", str(1200 + i), D("1"), D("2")] for i in range(12)]
cand = excel([("Лист1", rows)])
_, warnings = segment_columns(cand.rows, (0,), tuple(range(1, 13)), 2, EXCEL, rules)
assert len(warnings) == 1
assert warnings[0].message.endswith("и ещё 2")
assert warnings[0].message.count("Лист1!E") == 10 # noqa: PLR2004
assert len((warnings[0].locator or "").split("\n")) == 12 # noqa: PLR2004
def test_no_dated_columns_keeps_old_behaviour(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "", ""],
["Запасы", "", "1210", D("5"), D("4")],
["Итого", "", "1200", D("5"), D("4")],
]
columns, warnings = run(rows, EXCEL, rules)
assert [c.column_index for c in columns] == [3, 4]
assert [(w.code, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", 3), ("COLUMN_WITHOUT_DATE", 4)]
def test_undated_column_between_dated_keeps_old_behaviour(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", None, "На 31.12.2024"],
["Запасы", "", "1210", D("5"), D("9"), D("4")],
["Итого", "", "1200", D("5"), D("9"), D("4")],
]
columns, warnings = run(rows, EXCEL, rules)
assert [c.column_index for c in columns] == [3, 4, 5]
assert [(w.code, w.severity, w.column_index) for w in warnings] == [("COLUMN_WITHOUT_DATE", Severity.ERROR, 4)]
def test_two_row_header_is_joined_with_min_confidence(rules: Rules) -> None:
rows: list[list[Any]] = [
["", "", ("На 31 декабря", 90.0)],
["Наименование", "Код", ("2024 г.", 80.0)],
["Запасы", "1210", "5"],
]
cand = ocr(rows)
drafts, _ = segment_columns(cand.rows, (0, 1), (2,), 1, OCR, rules)
assert [(d.grid_col, d.label, d.label_confidence, d.date) for d in drafts] == [
(2, "На 31 декабря 2024 г.", 80.0, date(2024, 12, 31))
]
def test_sparse_ocr_columns(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", "На 30.09.2025", None, "На 31.12.2024", "На 31.12.2023"],
["Запасы", "1210", "5", None, "4", "3"],
["Итого", "1200", "5", None, "4", "3"],
]
columns, _ = run(rows, OCR, rules)
assert [c.column_index for c in columns] == [2, 4, 5]
def test_uncertain_label_does_not_confirm_itself(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", ("На 30.09.2О25", 60.0), ""],
["Запасы", "1210", "5", "4"],
["Итого", "1200", "5", "4"],
]
cand = ocr(rows)
drafts, _ = segment_columns(cand.rows, (0,), (1, 2), 1, OCR, rules)
dates = document_dates(drafts, parse_header(hdr(), rules))
assert (dates.effective, dates.confident_effective) == (date(2025, 9, 30), None)
_, warnings = finalize_columns(drafts, dates, rules)
assert [(w.code, w.column_index) for w in warnings] == [("COLUMN_DATE_UNCERTAIN", 2)]
def test_uncertain_label_confirmed_by_report_date(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "Код", "На 30.09.2025", ("На 31.12.2024", 60.0), ("На 31.03.2025", 60.0)],
["Запасы", "1210", "5", "4", "3"],
["Итого", "1200", "5", "4", "3"],
]
_, warnings = run(rows, OCR, rules, header=hdr(report_date="30.09.2025"))
assert [(w.code, w.column_index) for w in warnings] == [("COLUMN_DATE_UNCERTAIN", 4)]
def test_segment_without_header(rules: Rules) -> None:
cand = ocr([["Запасы", "1210", "5", "", "4"], ["Итого", "1200", "5", "", "4"]])
drafts, warnings = segment_columns(cand.rows, (), (0, 1), 1, OCR, rules)
assert [(d.grid_col, d.label, d.kind) for d in drafts] == [(2, "", PeriodKind.NONE), (4, "", PeriodKind.NONE)]
assert warnings == []
def test_public_amount_columns(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", "На 31.12.2024", "Изменение"],
["Запасы", "", "1210", D("5"), D("4"), "рост"],
["Итого", "", "1200", D("5"), "", "рост"],
]
assert amount_columns(excel([("Лист1", rows)]).rows, 2, rules) == (3, 4)
assert amount_columns(ocr([["Запасы", "1210", "5", "", "4"]]).rows, 1, rules, source=OCR) == (2, 4)
def test_dated_column_without_numbers_is_not_the_table_edge(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", "", "На 31.12.2024", ""],
["Запасы", "", "1210", None, D("5"), None, D("4")],
["Итого", "", "1200", None, D("5"), None, D("4")],
] # подпись объединённой ячейки стоит левее значений: границы таблицы нет, колонки остаются
columns, warnings = run(rows, EXCEL, rules)
assert [c.column_index for c in columns] == [3, 4, 5, 6][:0] or [w.code for w in warnings].count(
"NUMBERS_OUTSIDE_TABLE"
) == 0
def test_sheet_name_with_comma_keeps_addresses(rules: Rules) -> None:
rows: list[list[Any]] = [
["Наименование", "", "Код", "На 30.09.2025", None],
["Запасы", "", "1210", D("5"), D("9")],
]
cand = excel([("Баланс, 2025", rows)])
_, warnings = segment_columns(cand.rows, (0,), (1,), 2, EXCEL, rules)
assert (warnings[0].locator or "").split("\n") == ["'Баланс, 2025'!E2"]