Andrey Epifancev c1288d08fb fix(normalizer): итоги финального ревью — Money, статус при ошибочном файле, пропущенные строки, длина дат, дубли yaml
- C-1 (№30): сумма с более чем двумя знаками после запятой не принимается (amount=null, string_value, is_recognized=false);
  confidence ячеек и шапки приводится к [0, 100], NaN и бесконечность дают 0
- F-A (№28): ошибочный файл набора при успешном понижает статус до needs_check
- F-B: «ИНН не распознан» только когда ИНН не извлечён, иначе «ИНН распознан неуверенно»
- №29: предупреждения «строк с нечисловым кодом пропущено: N» и «строк без кода с суммами пропущено: N»
- I-3 (№31): тексты дат длиннее 200 символов не разбираются и не проходят fix_lookalikes
- T1.3 (№33): загрузчик yaml отклоняет повторяющиеся ключи; тавтологичный тест дублей заменён настоящим
- гигиена: докстринг amounts.py описывает фактическое поведение (все точки удаляются), мёртвые noqa в ocr/__init__.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 03:35:10 +04:00

109 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.

"""Раскладка колонок таблицы OCR (§11.1 п. 3): шапка «Код» и вывод по данным.
Перенос `_build_layout`/`_infer_layout` из старого ядра."""
import pytest
from src.normalizer.ocr.layout import Cell, ColumnLayout, build_layout, cell_text, cells_by_index, infer_layout
KNOWN = frozenset({"1110", "1120", "1130", "1150", "2110", "2120"})
def _row(*cells: tuple[int, str, float]) -> dict:
return {"row_index": 0, "cells": [{"column_index": i, "value": v, "confidence": c} for i, v, c in cells]}
def test_cells_by_index_normalizes_types() -> None:
cells = cells_by_index(_row((0, "Выручка", 71.4), (1, None, None)))
assert cells == {0: Cell("Выручка", 71.4), 1: Cell("", 0.0)}
assert cell_text(cells, 0) == "Выручка"
assert cell_text(cells, 7) == ""
assert cell_text(cells, None) == ""
@pytest.mark.parametrize(
("raw", "expected"),
[(float("nan"), 0.0), (150, 100.0), (-5, 0.0), (float("inf"), 0.0), (10**400, 0.0), ("90", 0.0), (None, 0.0)],
)
def test_cells_by_index_clamps_confidence(raw: object, expected: float) -> None:
"""Решение №30: `json.loads` принимает NaN/Infinity — уверенность ячейки приводится к [0, 100], NaN → 0."""
row = {"cells": [{"column_index": 0, "value": "x", "confidence": raw}]}
assert cells_by_index(row)[0].confidence == expected
class TestBuildLayout:
def test_header_with_code_and_years(self) -> None:
cells = cells_by_index(
_row(
(0, "Наименование показателя", 100),
(1, "Код", 100),
(2, "На 30 июня\n2025 г", 100),
(4, "На 31 декабря\n2024 г.", 100),
(5, "На 31 декабря\n2023 г.", 100),
)
)
assert build_layout(cells) == ColumnLayout(label_idx=0, code_idx=1, value_idxs=(2, 4, 5))
def test_label_defaults_to_last_filled_left_of_code(self) -> None:
cells = cells_by_index(_row((0, "Пояснения", 100), (1, "Показатель", 100), (2, "Код", 100), (3, "2024", 100)))
assert build_layout(cells) == ColumnLayout(label_idx=1, code_idx=2, value_idxs=(3,))
def test_no_code_cell_is_not_a_header(self) -> None:
assert build_layout(cells_by_index(_row((0, "Выручка", 100), (1, "2110", 100), (2, "13 070", 100)))) is None
class TestInferLayout:
def test_aep3_like_rows_without_header(self) -> None:
rows = [
cells_by_index(
_row(
(1, "НИ", 0),
(0, "Нематериальные активы", 100),
(2, "30127", 60),
(4, "| | г", 16),
(5, "", 100),
)
),
cells_by_index(
_row(
(1, "1120", 50),
(0, "Результаты исследований и", 94),
(2, ":", 0),
(4, "--", 0),
(5, ":", 0),
)
),
cells_by_index(
_row(
(1, "1130", 80),
(0, "Нематериальные поисковые", 87),
(2, "не ****", 5),
(4, "3", 0),
(5, "+ :", 0),
)
),
cells_by_index(
_row(
(1, "1150", 100),
(0, "Основные средства", 100),
(2, "1 234", 100),
(4, "5 678", 100),
(5, "", 100),
)
),
]
layout = infer_layout(rows, KNOWN)
assert layout is not None
assert layout.code_idx == 1
assert layout.label_idx == 0
# R-1: в колонке 5 только мусор (пусто, ':', '+ :'), настоящих сумм нет
# в колонке 4 и мусор, и суммы ('--', '3', '5 678'), но сумм не меньше, чем мусора
assert layout.value_idxs == (2, 4)
def test_no_known_codes_is_none(self) -> None:
rows = [cells_by_index(_row((0, "текст", 100), (1, "мусор", 100)))]
assert infer_layout(rows, KNOWN) is None
def test_empty_rows_is_none(self) -> None:
assert infer_layout([], KNOWN) is None