arm-tabular-parser/api/tests/unit/parser/test_osv_warnings.py
Andrey Epifancev 71f596cdb7 feat: формы через arm_statements, контракт результата и конверта v2.0, статус и OSV_* у ОСВ (#54)
- копия схем и примеров контракта v2.0 (запрос остаётся v1.0), модели result/common/message/request;
  ResultWarning/FileWarning, статус и предупреждения у документов, строки forms v2.0, service.statements_version
- statements/pipeline.py: кандидаты адаптера -> normalize_candidate -> to_contract / warning_to_contract
- BookParser: статус книги через envelope_status, SHEET_OTHER / FILE_TYPE_MISMATCH / XLS_NO_CODEPAGE как предупреждения
  файла, OSV_* и OSV_XLSB_NO_OUTLINE в документах ОСВ, тексты иерархии по спеке
- воркер, app и CLI получают statements_version из require_statements_version()
- старые модели документа формы вынесены в statements/legacy_models.py до удаления старого конвейера (задача 9)
- OpenSpec: MODIFIED «Парсер — заменяемый интерфейс» в дельте tabular-recognition-service

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 20:52:52 +04:00

95 lines
4.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.

"""Предупреждения ОСВ с кодами `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"