from datetime import date from typing import Any import pytest import src.normalizer.ocr.answer_set as answer_set_module from arm_statements import DocStatus, DocumentType, EnvelopeStatus, PeriodKind, Severity, default_rules from src.contract import ErrorCode from src.normalizer.ocr.answer_set import NO_TABLES_MESSAGE, normalize_answer_set from tests.unit.normalizer.ocr_builders import BAD, BALANCE, HEAD, INCOME, OK, merged, ocr_table, response RULES = default_rules() def test_one_json_two_forms_copy_header() -> None: res = normalize_answer_set( merged( ( "j.json", response( ocr_table("Бухгалтерский баланс", BALANCE), ocr_table("Отчет о финансовых результатах", INCOME) ), ) ), RULES, ) assert [(d.locator, d.document_type, d.header.form_name) for d in res.documents] == [ ("file:j.json;table:0", DocumentType.BALANCE, "Бухгалтерский баланс"), ("file:j.json;table:1", DocumentType.INCOME, "Отчет о финансовых результатах"), ] assert {d.header.inn.value for d in res.documents} == {"7700000000"} assert res.documents[0].status is DocStatus.DONE and res.error is None def test_lost_ofr_json_is_file_error() -> None: res = normalize_answer_set( merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE))), ("j_ofr.json", response(error=BAD))), RULES, ) assert res.status is EnvelopeStatus.NEEDS_CHECK and len(res.documents) == 1 assert [(w.code, w.severity, w.locator, w.message) for w in res.file_warnings] == [ ("JSON_FAILED", Severity.ERROR, "file:j_ofr.json", "INVALID_DOCUMENT: Не удалось определить тип документа.") ] def test_table_exception_is_file_warning(monkeypatch: pytest.MonkeyPatch) -> None: real = answer_set_module.normalize_candidate def flaky(candidate: Any, rules: Any = None) -> Any: if candidate.locator.endswith("table:1"): raise RuntimeError("сломалось") return real(candidate, rules) monkeypatch.setattr(answer_set_module, "normalize_candidate", flaky) res = normalize_answer_set( merged( ( "j.json", response( ocr_table("Бухгалтерский баланс", BALANCE), ocr_table("Отчет о финансовых результатах", INCOME) ), ) ), RULES, ) assert [d.document_type for d in res.documents] == [DocumentType.BALANCE] [w] = res.file_warnings assert (w.code, w.severity, w.locator) == ("TABLE_PARSE_ERROR", Severity.ERROR, "file:j.json;table:1") assert w.message == "BACKEND_PARSING_ERROR: RuntimeError: сломалось" assert res.status is EnvelopeStatus.NEEDS_CHECK def test_broken_table_structure_is_table_parse_error() -> None: res = normalize_answer_set( merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE), {"label": {}, "rows": "x"}))), RULES ) assert [w.code for w in res.file_warnings] == ["TABLE_PARSE_ERROR"] and len(res.documents) == 1 def test_text_only_table_skipped_and_no_documents() -> None: res = normalize_answer_set(merged(("j.json", response(ocr_table("", [("Руководитель", "Иванов")])))), RULES) assert [(w.code, w.severity, w.locator) for w in res.file_warnings] == [ ("TABLE_SKIPPED", Severity.INFO, "file:j.json;table:0") ] assert res.status is EnvelopeStatus.ERROR assert (res.error.code, res.error.message) == (ErrorCode.RECOGNITION_FAILED, NO_TABLES_MESSAGE) def test_lost_table_with_numbers() -> None: res = normalize_answer_set( merged( ("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE), ocr_table("", [("Итого", "", "15 000")]))) ), RULES, ) assert [(w.code, w.severity, w.locator) for w in res.file_warnings] == [ ("TABLE_LOST", Severity.ERROR, "file:j.json;table:1") ] assert res.status is EnvelopeStatus.NEEDS_CHECK def test_all_json_failed_first_code_and_all_listed() -> None: res = normalize_answer_set(merged(("j.json", response(error=BAD)), ("j_ofr.json", "не объект")), RULES) assert res.status is EnvelopeStatus.ERROR and res.documents == () assert (res.error.code, res.error.message) == (ErrorCode.INVALID_DOCUMENT, BAD["message"]) assert [(w.code, w.locator) for w in res.file_warnings] == [ ("JSON_FAILED", "file:j.json"), ("JSON_FAILED", "file:j_ofr.json"), ] @pytest.mark.parametrize( ("data_ocr", "message"), [ (None, "ответ ИС без data_ocr"), ({**HEAD, "table_structure": None}, "ответ ИС без table_structure"), ({**HEAD, "table_structure": []}, "в ответе ИС нет таблиц"), ], ) def test_successful_json_without_tables(data_ocr: object, message: str) -> None: res = normalize_answer_set(merged(("j.json", {"error": OK, "data_ocr": data_ocr})), RULES) assert [(w.code, w.severity, w.message) for w in res.file_warnings] == [("JSON_FAILED", Severity.ERROR, message)] assert res.error is not None and res.error.code is ErrorCode.RECOGNITION_FAILED @pytest.mark.parametrize("raw", [None, {"files": "x"}, {"job_id": "j"}]) def test_not_merged(raw: object) -> None: res = normalize_answer_set(raw, RULES) assert res.status is EnvelopeStatus.ERROR and res.error is not None assert res.error.code is ErrorCode.INVALID_FORMAT # Сквозной тест дат шлюза: строки OCR из test_parses_real_forms (правило — в пакете, план 1, задача 5) @pytest.mark.parametrize( ("label", "expected"), [ ("На 30 июня\n2025 г", date(2025, 6, 30)), ("За январь - июнь\n2025 г.", date(2025, 6, 30)), ("30 сентября 2025 r.", date(2025, 9, 30)), ("3l.12.2О24", date(2024, 12, 31)), ("9 мес. 2025", date(2025, 9, 30)), ("30.09.2025 0:00:00", date(2025, 9, 30)), ], ) def test_ocr_column_dates_end_to_end(label: str, expected: date) -> None: rows = [("Наименование показателя", "Код", label), *BALANCE[1:]] head = {**HEAD, "date": {"form_name": "2|0|2|4|", "confidence": 75.0}} res = normalize_answer_set(merged(("j.json", response(ocr_table("Бухгалтерский баланс", rows), head=head))), RULES) doc = res.documents[0] assert [(c.period_kind, c.period_date) for c in doc.columns] == [(PeriodKind.ABSOLUTE, expected)] assert doc.header.report_date.value == date(2024, 12, 31)