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

64 lines
2.6 KiB
Python

"""Шапка документа из полей `data_ocr` (§11.1 п. 8): ИНН — только цифры, единицы — очищенный текст, дата отчёта —
`parse_date` после замены двойников; «распознано» — `confidence == 100` и сырой текст без мусора."""
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import date
from src.normalizer.ocr.dates import is_date_sized, parse_date
from src.normalizer.ocr.text import clamp_confidence, clean_value, collapse_whitespace, digits_only, fix_lookalikes
__all__ = ["FileHeader", "parse_file_header"]
FULL_CONFIDENCE = 100.0
CAPPED_CONFIDENCE = 99.0
@dataclass(frozen=True)
class FileHeader:
organization_name: str | None
organization_confidence: float | None
inn: str | None
inn_confidence: float | None
units: str | None
report_date_text: str | None
report_date: date | None
report_date_recognized: bool
def _field(data_ocr: Mapping[str, object], name: str) -> tuple[str, float | None]:
raw = data_ocr.get(name)
if not isinstance(raw, Mapping):
return "", None
text = raw.get("form_name")
text_str = str(text) if isinstance(text, str) else ""
return (text_str, clamp_confidence(raw.get("confidence")))
def parse_file_header(data_ocr: Mapping[str, object], lookalikes: Mapping[str, str]) -> FileHeader:
organization, organization_confidence = _field(data_ocr, "organization")
inn_raw, inn_confidence = _field(data_ocr, "inn")
units_raw, _ = _field(data_ocr, "units")
date_raw, date_confidence = _field(data_ocr, "date")
organization_name = collapse_whitespace(organization) or None
inn = digits_only(inn_raw) or None
if inn is None:
inn_confidence = None
elif inn_confidence is not None and inn_raw.strip() != inn:
inn_confidence = min(inn_confidence, CAPPED_CONFIDENCE)
# решение №31: длинный текст не разбирается и не проходит `fix_lookalikes`
report_date = (
parse_date(fix_lookalikes(date_raw, lookalikes)) if date_raw.strip() and is_date_sized(date_raw) else None
)
return FileHeader(
organization_name=organization_name,
organization_confidence=organization_confidence if organization_name is not None else None,
inn=inn,
inn_confidence=inn_confidence,
units=clean_value(units_raw) or None,
report_date_text=date_raw.strip() or None,
report_date=report_date,
report_date_recognized=date_confidence == FULL_CONFIDENCE and report_date is not None,
)