- 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
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
"""Раскладка колонок таблицы 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
|