import json from dataclasses import replace from decimal import Decimal as D import pytest from arm_statements import ( DocStatus, DocumentType, document_status, make_warning, normalize_candidate, status_input, status_input_from_contract, to_contract, warning_to_contract, ) from arm_statements.loader import Rules from arm_statements.model import FailedRelation, StatementChecks, StatementDocument from arm_statements.tests.builders import hdr, ocr HEAD = ["Наименование показателя", "Код", "На 30.09.2025"] FULL_HEADER = hdr( organization_name="ООО «Пример»", inn=("7700000000", 80.0), report_date="30.09.2025", units="тыс. руб." ) HEADER_KEYS = {"form_name", "needs_review_fields"} | { f"{name}{suffix}" for name in ("organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "okei", "address") for suffix in ("", "_text", "_confidence") } def balance(rules: Rules, *rows: list[str]) -> StatementDocument: cand = ocr([HEAD, *rows], header=FULL_HEADER, type_hint=DocumentType.BALANCE, title="Бухгалтерский баланс") doc = normalize_candidate(cand, rules).document assert doc is not None return doc def test_document_in_contract_form(rules: Rules) -> None: doc = balance(rules, ["Запасы", "1210", "60"], ["Итого II", "1200", "60"], ["НИ", "", "7"]) data = json.loads(json.dumps(to_contract(doc), ensure_ascii=False)) assert set(data["header"]) == HEADER_KEYS assert (data["header"]["form_name"], data["header"]["inn"], data["header"]["inn_confidence"]) == ( "Бухгалтерский баланс", "7700000000", 80.0, ) assert data["header"]["report_date"] == "2025-09-30" and data["header"]["okei"] is None assert data["lines"][0]["values"][0]["amount"] == "60.00" assert {ln["line_status"] for ln in data["lines"]} >= {"ok"} assert "column_key" not in json.dumps(data) and "row:" not in json.dumps(data["header"]) assert "locator" not in json.dumps(data["warnings"]) assert set(data["checks"]) == {"relations_failed", "soft_relations_failed"} def test_empty_target_is_null_in_contract(rules: Rules) -> None: doc = balance(rules, ["Запасы", "1210", "60"], ["Итого II", "1200", "60"], ["Итого I", "1100", "40"]) failed = to_contract(doc)["checks"]["relations_failed"] target = next(f for f in failed if f["relation"] == "1600 = 1100 + 1200") assert (target["expected"], target["actual"], target["column_index"]) == (None, "100.00", 2) def test_no_lines_no_checks_key(rules: Rules) -> None: doc = normalize_candidate( ocr([["Итого", "15 000"]], type_hint=DocumentType.BALANCE, header=FULL_HEADER), rules ).document assert doc is not None and "checks" not in to_contract(doc) and to_contract(doc)["lines"] == [] def test_relation_without_column_index_is_error(rules: Rules) -> None: doc = balance(rules, ["Запасы", "1210", "60"], ["Итого II", "1200", "60"]) bad = FailedRelation(None, "c1", "1600 = 1700", ("1600", "1700"), D("1"), None) with pytest.raises(ValueError, match="column_index"): to_contract(replace(doc, checks=StatementChecks((bad,), ()))) def test_warning_keys(rules: Rules) -> None: w = make_warning("ROW_SPLIT", "разлеплено", rules, line_refs=("row:3#1", "row:3#2"), locator="file:a.json") assert warning_to_contract(w) == { "code": "ROW_SPLIT", "severity": "info", "message": "разлеплено", "line_refs": ["row:3#1", "row:3#2"], } lost = make_warning("TABLE_LOST", "таблица", rules, locator="file:a.json;table:1") assert warning_to_contract(lost, file_level=True) == { "code": "TABLE_LOST", "severity": "error", "message": "таблица", "locator": "file:a.json;table:1", } def test_status_input_roundtrip(rules: Rules) -> None: doc = balance(rules, ["Запасы", "1210", "60"], ["Итого II", "1200", "60"], ["НИ", "", "7"]) data = status_input_from_contract(json.loads(json.dumps(to_contract(doc), ensure_ascii=False))) assert data == status_input(doc) assert document_status(data) is doc.status is DocStatus.NEEDS_CHECK