arm-tabular-parser/api/tests/unit/parser/test_osv_warnings.py
Andrey Epifancev c915fd143c ОСВ: одно предупреждение на неизвестную метку «Показатели» вместо одного на строку (#72, AURORA-1724)
Нераспознанная метка в колонке «Показатели» давала OSV_ROWS на каждую строку (2 128 одинаковых
на 11.xlsx). Теперь построчные OSV_ROWS «строка N: <текст>» с одинаковым текстом сводятся в одно:
«метка «X» в колонке «Показатели» не распознана — K строк (первые 20 номеров, …) не участвуют в
проверках»; прочие одинаковые построчные — «<текст> — строк: K (…)». Одиночное предупреждение
не меняется. Коды и контракт не тронуты. Спека osv-parsing обновлена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 01:17:22 +04:00

160 lines
8.1 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, group_row_warnings, 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"
def _indicator_rows(label: str, count: int) -> list[list[object]]:
rows: list[list[object]] = [
[],
["Оборотно-сальдовая ведомость по счету: 76"],
["Контрагенты"],
["за 2025 г."],
['ООО "Тест"'],
[],
[
"Субконто",
"Показатели",
M("Сальдо на начало периода", 2),
M("Обороты за период", 2),
M("Сальдо на конец периода", 2),
],
[None, None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
]
for number in range(count):
rows.append([f"Контрагент {number}", "БУ", 10, None, None, None, 10, None])
rows.append([None, label, 5, None, None, None, 5, None])
rows.append(["Итого", "БУ", 10 * count, None, None, None, 10 * count, None])
return rows
def test_unknown_indicator_label_gives_one_warning_for_all_rows(rules: Rules) -> None:
"""AURORA-1724: неизвестная метка «Показатели» в 30 строках — одно предупреждение с числом и номерами строк."""
count = 30
result = OsvParser(rules).parse_sheet(sheet_from_rows(_indicator_rows("Неизв.", count)), Deadline(60))
texts = [note.message for note in result.notes if note.code == "OSV_ROWS" and "«Неизв.»" in note.message]
assert len(texts) == 1, texts
assert "30 строк (10, 12, 14," in texts[0]
assert "не участвуют в проверках" in texts[0]
assert "…" in texts[0] # номера строк ограничены, как у OSV_PARENT_SUM
def test_single_unknown_indicator_label_keeps_row_wording(rules: Rules) -> None:
result = OsvParser(rules).parse_sheet(sheet_from_rows(_indicator_rows("Неизв.", 1)), Deadline(60))
texts = [note.message for note in result.notes if "«Неизв.»" in note.message]
assert texts == ["строка 10: метка «Неизв.» в колонке «Показатели» не распознана — строка не участвует в проверках"]
def test_different_unknown_labels_are_grouped_separately() -> None:
messages = [
"строка 3: метка «А» в колонке «Показатели» не распознана — строка не участвует в проверках",
"строка 4: метка «Б» в колонке «Показатели» не распознана — строка не участвует в проверках",
"строка 5: метка «А» в колонке «Показатели» не распознана — строка не участвует в проверках",
]
assert group_row_warnings(messages) == [
"метка «А» в колонке «Показатели» не распознана — 2 строки (3, 5) не участвуют в проверках",
"строка 4: метка «Б» в колонке «Показатели» не распознана — строка не участвует в проверках",
]
def test_same_text_row_warnings_are_grouped() -> None:
messages = [
"строка 3: нечисловое значение в колонке сумм: «х»",
"строка 4: другое",
"строка 5: нечисловое значение в колонке сумм: «х»",
]
assert group_row_warnings(messages) == [
"нечисловое значение в колонке сумм: «х» — строк: 2 (3, 5)",
"строка 4: другое",
]