Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
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
|