"""Предупреждения ОСВ с кодами `OSV_*` (P4-12): коды по источнику, тексты — в `warnings` документа (контракт 2.0).""" import pytest from gridbuilder import L, M, sheet_from_rows from test_checks import row, total, tree from src.parser.checks import run_checks from src.parser.deadline import Deadline from src.parser.grid import Sheet from src.parser.osv import OsvParser from src.parser.osv_warnings import OsvNote, hierarchy_note from src.parser.rules import Rules, load_rules SUMS = (None, None, 100, 100, None, None) BROKEN = (None, None, 100, 100, None, 5) @pytest.fixture(scope="module") def rules() -> Rules: return load_rules() def _sheet(*rows: list[object]) -> Sheet: head: list[list[object]] = [ ["ООО Зерновая"], ["Оборотно-сальдовая ведомость по счету 60"], ["Период: 9 месяцев 2025 г."], ["Детализация по субсчетам, субконто: Контрагенты"], ["Выводимые данные: сумма"], [], ["Субконто", M("Сальдо на начало периода", 2), M("Оборот за период", 2), M("Сальдо на конец периода", 2)], [None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], ] return sheet_from_rows([*head, *rows]) def test_check_notes_have_codes() -> None: rows = tree() rows[2] = row(9, ("40", None, "30", "20", "51", None), level=1, parent=7) rows[-1] = total(10, ("100", None, "50", "30", "121", None)) _, notes = run_checks(rows) assert [note.code for note in notes] == ["OSV_BALANCE_IDENTITY", "OSV_PARENT_SUM", "OSV_TOTAL"] def test_hierarchy_note_code_by_strategy() -> None: assert hierarchy_note("sums") == OsvNote("OSV_HIERARCHY_BY_SUMS", "иерархия строк — по суммам (sums)") assert hierarchy_note("account_pattern").code == "OSV_HIERARCHY_BY_ACCOUNT_PATTERN" def test_hierarchy_note_text_names_the_strategy() -> None: assert hierarchy_note("outline").message == "иерархия строк — по группировке листа (outline)" assert hierarchy_note("indent").message == "иерархия строк — по отступам (indent)" assert hierarchy_note("account_pattern").message == "иерархия строк — по кодам счетов (account_pattern)" assert hierarchy_note("sums").message == "иерархия строк — по суммам (sums)" assert hierarchy_note("trivial").message == "иерархия строк — плоский список (trivial)" def test_parse_sheet_notes_start_with_hierarchy(rules: Rules) -> None: sheet = _sheet([L(0), "60.01", *BROKEN], [L(1), "ООО Альфа", *SUMS], [L(0), "Итого", *BROKEN]) result = OsvParser(rules).parse_sheet(sheet, Deadline(60)) assert result.notes[0].code.startswith("OSV_HIERARCHY_BY_") assert all(note.code.startswith("OSV_") for note in result.notes) assert not result.ok def test_document_warnings_are_the_notes(rules: Rules) -> None: sheet = _sheet([L(0), "60.01", *BROKEN], [L(1), "ООО Альфа", *SUMS], [L(0), "Итого", *BROKEN]) result = OsvParser(rules).parse_sheet(sheet, Deadline(60)) assert [(w.code, w.severity, w.message) for w in result.document.warnings] == [ (note.code, "info", note.message) for note in result.notes ] assert [note.message for note in result.notes] == [ "иерархия строк — по группировке листа (outline)", "валюта в шапке не найдена", "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 9, 11", "§1.4: суммы родителя ≠ сумме детей (±0.01): строки 9", ] assert result.document.status == "needs_check" def test_reader_notes_are_appended_last(rules: Rules) -> None: sheet = _sheet([L(0), "60.01", *SUMS], [L(1), "ООО Альфа", *SUMS], [L(0), "Итого", *SUMS]) note = OsvNote("OSV_XLSB_NO_OUTLINE", "xlsb: outline level недоступен") result = OsvParser(rules).parse_sheet(sheet, Deadline(60), [note]) assert result.notes[-1] == note assert result.document.warnings[-1].code == "OSV_XLSB_NO_OUTLINE"