This repository has been archived on 2026-10-26. You can view files and clone it, but cannot push or open issues or pull requests.
Andrey Epifancev 547f9efcdb feat(#5): шаг 2 — числа правее последней подписанной колонки не колонки сумм
Столбец без подписи правее последней колонки с подписью периода колонкой сумм
не считается; вместо 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>
2026-10-18 21:18:07 +04:00

190 lines
9.0 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 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)