arm-tabular-parser/api/tests/unit/parser/test_indicators.py
Andrey Epifancev d6dd341003 Правки итогового ревью: спека, предупреждение, README, design (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 12:47:49 +04:00

139 lines
5.4 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.

"""Метки строк-показателей (§1.3, #58): колонка «Показатели» и текст строки."""
from decimal import Decimal
import pytest
from gridbuilder import L, M, at, sheet_from_rows
from src.parser.deadline import Deadline
from src.parser.detect import TableHeader, find_table_header
from src.parser.grid import Sheet
from src.parser.hierarchy import BodyRow
from src.parser.indicators import RowLabel, is_indicator_total, levels_with_indicators, read_label
from src.parser.rules import Rules, load_rules
TITLE = "Оборотно-сальдовая ведомость по счету 60 за 1 квартал 2025 г."
LABEL_COL = 2
FIRST_ROW = 4
@pytest.fixture(scope="module")
def rules() -> Rules:
return load_rules()
def _sheet(*labels: str, text: str = "60") -> tuple[Sheet, TableHeader]:
rows: list[list[object]] = [
[TITLE],
["Счет", None, M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2)],
["Счет", "Показатели", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
]
rows.extend([L(0), *at({1: text, LABEL_COL: label, 3: 10, 8: 10})] for label in labels)
sheet = sheet_from_rows(rows)
return sheet, find_table_header(sheet, load_rules())
def _read(label: str, rules: Rules, text: str = "60") -> tuple[RowLabel, int]:
sheet, header = _sheet(label, text=text)
return read_label(sheet, FIRST_ROW, text, header, rules), FIRST_ROW
def test_bu_label_is_data_row(rules: Rules) -> None:
result, _ = _read("БУ", rules)
assert (result.indicator, result.data_type, result.text) == (False, "BU", None)
def test_nu_label_is_indicator_with_label_text(rules: Rules) -> None:
result, _ = _read("НУ", rules)
assert (result.indicator, result.data_type, result.text, result.warning) == (True, "NU", "НУ", None)
def test_untyped_label_has_no_data_type_and_no_warning(rules: Rules) -> None:
result, _ = _read("ПР", rules)
assert (result.indicator, result.data_type, result.text, result.warning) == (True, None, "ПР", None)
def test_unknown_label_warns_with_row_number(rules: Rules) -> None:
result, row = _read("Ед.", rules)
assert (result.indicator, result.data_type) == (True, None)
assert result.warning is not None
assert f"строка {row}" in result.warning
assert "Ед." in result.warning
assert "не участвует в проверках" in result.warning
def test_text_currency_label_without_column(rules: Rules) -> None:
sheet = sheet_from_rows(
[
[TITLE],
["Счет", M("Сальдо на начало периода", 2), M("Обороты за период", 2), M("Сальдо на конец периода", 2)],
["Счет", "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
[L(0), *at({1: "В валюте", 2: 10})],
[L(0), *at({1: "Валюта USD", 2: 10})],
]
)
header = find_table_header(sheet, rules)
assert header.indicator_col is None
in_currency = read_label(sheet, 4, "В валюте", header, rules)
assert (in_currency.indicator, in_currency.data_type, in_currency.text) == (True, "CURRENCY", None)
plain = read_label(sheet, 5, "Валюта USD", header, rules)
assert (plain.indicator, plain.data_type) == (False, None)
def test_is_indicator_total(rules: Rules) -> None:
sheet, header = _sheet("НУ", "БУ", "")
assert is_indicator_total(sheet, FIRST_ROW, header, rules)
assert not is_indicator_total(sheet, FIRST_ROW + 1, header, rules)
assert not is_indicator_total(sheet, FIRST_ROW + 2, header, rules)
# --- Иерархия: показатели вне каскада ---
def _row(row: int, text: str, outline: int, *, indicator: bool = False) -> BodyRow:
return BodyRow(
row=row,
text=text,
is_account=text.isdigit(),
account_rank=1 if text.isdigit() else None,
bold=False,
indent=0,
outline=outline,
sums=(Decimal(1),) * 6,
indicator=indicator,
)
def test_indicators_are_children_of_previous_data_row_outside_cascade() -> None:
rows = [
_row(10, "60", 0),
_row(11, "НУ", 0, indicator=True),
_row(12, "Объект", 1),
_row(13, "НУ", 1, indicator=True),
_row(14, "Итого", 0),
_row(15, "НУ", 0, indicator=True),
]
levels = levels_with_indicators(rows, 14, Deadline(3600))
assert levels.strategy == "outline"
assert levels.by_row == {10: 0, 11: 1, 12: 1, 13: 2, 14: 0, 15: 0}
assert levels.parent == {10: None, 11: 10, 12: 10, 13: 12, 14: None, 15: None}
def test_indicator_first_in_table_is_root() -> None:
rows = [_row(10, "НУ", 0, indicator=True), _row(11, "60", 0)]
levels = levels_with_indicators(rows, None, Deadline(3600))
assert levels.by_row[10] == 0
assert levels.parent[10] is None
def test_levels_keep_sheet_row_order() -> None:
rows = [
_row(10, "60", 0),
_row(11, "НУ", 0, indicator=True),
_row(12, "Объект", 1),
_row(13, "НУ", 1, indicator=True),
]
levels = levels_with_indicators(rows, None, Deadline(3600))
assert list(levels.by_row) == [10, 11, 12, 13]
assert list(levels.parent) == [10, 11, 12, 13]