Столбец без подписи правее последней колонки с подписью периода колонкой сумм не считается; вместо COLUMN_WITHOUT_DATE — info NUMBERS_OUTSIDE_TABLE с адресами ячеек. Решение владельца 18.10.2026 (arm-ka-docs#116, Р-31). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
9.0 KiB
Python
190 lines
9.0 KiB
Python
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 arm_statements.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, row: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, Лист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")
|
||
|
||
|
||
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)
|