"""Метки строк-показателей (§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) @pytest.mark.parametrize("label", ["Кол.", "Кол", "Кол. доп.", "Кол.доп."]) def test_quantity_labels_including_additional_quantity(rules: Rules, label: str) -> None: """«Кол. доп.» — дополнительное количество (сч. 11, животные на выращивании; AURORA-1709).""" result, _ = _read(label, rules) assert (result.indicator, result.data_type, result.text, result.warning) == (True, "QUANTITY", label, None) @pytest.mark.parametrize("label", ["Колдоп", "Кол. доп. ед.", "Колос"]) def test_quantity_label_needs_exact_form(rules: Rules, label: str) -> None: result, _ = _read(label, rules) assert (result.indicator, result.data_type) == (True, None) assert result.warning is not 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]