"""Golden на обезличенных входах Review Focus 1–5 (план 1, задача 20). Входы `golden/*.candidate.json` собраны из `builders` один раз и лежат файлами; мусорный текст ячеек — дословно из реальных распознаваний, названия, ИНН и суммы — обезличенные и синтетические. Эталон `*.expected.json` — `to_jsonable(normalize_candidate(...).document)`; пересборка — `make golden-update` (`GOLDEN_UPDATE=1`). """ import json import os from collections.abc import Callable from decimal import Decimal as D from pathlib import Path import pytest from arm_statements import ( document_status, normalize_candidate, status_input, status_input_from_contract, to_contract, ) from arm_statements.loader import Rules from arm_statements.model import DocStatus, DocumentType, FormCandidate, LineStatus, StatementDocument from arm_statements.serialize import candidate_from_dict, to_jsonable from arm_statements.tests.builders import hdr, ocr GOLDEN = Path(__file__).parent / "golden" FULL_CONFIDENCE = 100.0 NAMES = ["aep3_balance", "aep3_income", "glued_balance", "kody_title_balance", "npz_income"] HEAD = ["Наименование показателя", "Код", "На 30.09.2025"] FULL_HEADER = hdr(organization_name="ООО «Пример»", inn="7700000000", report_date="30.09.2025", units="тыс. руб.") def run(name: str, rules: Rules) -> StatementDocument: data = json.loads((GOLDEN / f"{name}.candidate.json").read_text(encoding="utf-8")) result = normalize_candidate(candidate_from_dict(data), rules) assert result.document is not None return result.document @pytest.mark.parametrize("name", NAMES) def test_golden_snapshot(name: str, rules: Rules) -> None: actual = to_jsonable(run(name, rules)) target = GOLDEN / f"{name}.expected.json" if os.environ.get("GOLDEN_UPDATE") == "1": target.write_text(json.dumps(actual, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") pytest.fail(f"эталон {target.name} пересобран — просмотрите diff и запустите без GOLDEN_UPDATE") assert actual == json.loads(target.read_text(encoding="utf-8")) def assert_status_is_function_of_contract(doc: StatementDocument) -> None: contract = json.loads(json.dumps(to_contract(doc), ensure_ascii=False)) assert status_input_from_contract(contract) == status_input(doc) assert document_status(status_input(doc)) is doc.status @pytest.mark.parametrize("name", NAMES) def test_status_is_function_of_contract_document(name: str, rules: Rules) -> None: assert_status_is_function_of_contract(run(name, rules)) def _disclosure() -> FormCandidate: return ocr([["Текст"]], type_hint=DocumentType.DISCLOSURE) def _without_lines() -> FormCandidate: return ocr([["Итого", "15 000"]], type_hint=DocumentType.BALANCE, header=FULL_HEADER) def _absolute_column_without_report_date() -> FormCandidate: rows = [ HEAD, ["Запасы", "1210", "60"], ["Дебиторы", "1230", "40"], ["Итого II", "1200", "100"], ] header = hdr(organization_name="ООО «Пример»", inn="7700000000", units="тыс. руб.") return ocr(rows, type_hint=DocumentType.BALANCE, header=header) EXTRA: dict[str, Callable[[], FormCandidate]] = { "disclosure": _disclosure, "without_lines": _without_lines, "absolute_column_without_report_date": _absolute_column_without_report_date, } @pytest.mark.parametrize("case", sorted(EXTRA)) def test_status_is_function_of_contract_document_edge_cases(case: str, rules: Rules) -> None: doc = normalize_candidate(EXTRA[case](), rules).document assert doc is not None assert_status_is_function_of_contract(doc) def test_edge_cases_cover_their_branch(rules: Rules) -> None: disclosure = normalize_candidate(_disclosure(), rules).document assert disclosure is not None and disclosure.document_type is DocumentType.DISCLOSURE assert disclosure.status is DocStatus.DONE empty = normalize_candidate(_without_lines(), rules).document assert empty is not None and empty.lines == () assert [w.code for w in empty.warnings if w.code == "CODE_COLUMN_NOT_FOUND"] == ["CODE_COLUMN_NOT_FOUND"] assert status_input(empty).layout_found is False and empty.status is DocStatus.NEEDS_CHECK absolute = normalize_candidate(_absolute_column_without_report_date(), rules).document assert absolute is not None and absolute.header.report_date.value is None assert status_input(absolute).period_known is True def test_aep3_junk_codes(rules: Rules) -> None: # Review Focus 1 doc = run("aep3_balance", rules) by_text = {ln.line_code_text: ln for ln in doc.lines} assert (by_text["| 140"].line_status, by_text["| 140"].line_code) == (LineStatus.OK, "1140") confidence = by_text["| 140"].line_code_confidence assert confidence is not None and confidence < FULL_CONFIDENCE for junk in ("НИ", "изо :"): assert by_text[junk].line_status is LineStatus.DETAIL and by_text[junk].parent_ref is not None refs = [ln.ref for ln in doc.lines] assert len(refs) == len(set(refs)) and [ln.row_index for ln in doc.lines] == list(range(len(doc.lines))) assert by_text["НИ"].parent_ref == by_text["1110"].ref and by_text["изо :"].parent_ref == by_text["| 140"].ref assert len(doc.lines) == len(json.loads((GOLDEN / "aep3_balance.candidate.json").read_text("utf-8"))["rows"]) - 1 def test_aep3_dirty_income_amounts(rules: Rules) -> None: # Review Focus 2 doc = run("aep3_income", rules) values = {v.text: v for ln in doc.lines for v in ln.values if v.text} assert (values["(7744536}"].amount, values["(7744536}"].needs_review) == (D("-7744536"), False) assert (values["211(1 |"].amount, values["211(1 |"].needs_review) == (None, True) assert (values["42831 1"].amount, values["42831 1"].needs_review) == (D("428311"), False) assert doc.checks is not None and doc.checks.relations_failed == () assert doc.status is DocStatus.NEEDS_CHECK def test_glued_rows(rules: Rules) -> None: # Review Focus 3 doc = run("glued_balance", rules) split = [ln for ln in doc.lines if "#" in ln.ref] assert [(ln.line_code, ln.values[0].amount) for ln in split] == [("1500", D("917775")), ("1700", D("2371333"))] assert [ln.ref for ln in split] == ["row:13#1", "row:13#2"] assert len({ln.ref for ln in doc.lines}) == len(doc.lines) assert [w.code for w in doc.warnings if w.code == "ROW_SPLIT"] == ["ROW_SPLIT"] assert not [v for ln in doc.lines for v in ln.values if v.needs_review] assert not [ln for ln in doc.lines if ln.needs_review] def test_kody_title_block(rules: Rules) -> None: # Review Focus 4 doc = run("kody_title_balance", rules) assert "12267" not in [ln.line_code for ln in doc.lines] assert doc.header.okopf.value == "12267" and doc.status is DocStatus.DONE assert not [w for w in doc.warnings if w.code == "CODE_COLUMN_NOT_FOUND"] assert all(ln.ref.startswith("Баланс!C") for ln in doc.lines) def test_npz_continuation_single_code(rules: Rules) -> None: # Review Focus 5 doc = run("npz_income", rules) (line_2400,) = [ln for ln in doc.lines if ln.line_code == "2400"] assert [v.column_index for v in line_2400.values] == [c.column_index for c in doc.columns] assert all(v.amount is not None for v in line_2400.values) assert [v.amount for v in line_2400.values] == [D("264"), D("238")] assert "," in doc.locator and not [w for w in doc.warnings if w.code.startswith("SEGMENT_")] assert doc.status is DocStatus.DONE @pytest.mark.parametrize("name", NAMES) def test_document_warnings_reference_document_parts(name: str, rules: Rules) -> None: """I-2: line_refs предупреждения — ref строк документа; locator нет; column_index — из колонок.""" doc = run(name, rules) line_refs = {ln.ref for ln in doc.lines} column_indexes = {c.column_index for c in doc.columns} for warning in doc.warnings: assert set(warning.line_refs) <= line_refs, warning assert warning.locator is None, warning assert warning.column_index is None or warning.column_index in column_indexes, warning