This repository has been archived on 2026-10-26. You can view files and clone it, but cannot push or open issues or pull requests.
Andrey Epifancev 3df179b7a3 feat: шаг 7 — посчитанные соотношения §7, abs_terms, допуск и округление, упрощённый набор, подстроки
Добавлены evaluation.py (EvalLine/EvalValue/is_check_line) и checks.py
(form_variant, subline_parents, evaluate_relations, checks_from).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 13:50:17 +04:00

108 lines
5.3 KiB
Python

from decimal import Decimal as D
from arm_statements.checks import checks_from, evaluate_relations, form_variant, subline_parents
from arm_statements.evaluation import EvalLine, EvalValue, is_check_line
from arm_statements.loader import Rules
from arm_statements.model import DocumentType, FormVariant, LineStatus, StatementChecks
BAL, INC = DocumentType.BALANCE, DocumentType.INCOME
def line(code: str | None, *amounts: str | None, status: LineStatus = LineStatus.OK, order: int = 0) -> EvalLine:
values = tuple(EvalValue(str(i), None if a is None else D(a), a, 100.0, True) for i, a in enumerate(amounts))
return EvalLine(f"k{order}{code}", order, status, code, 100.0, False, values)
def failed(lines: list[EvalLine], doc: DocumentType, variant: FormVariant | None, rules: Rules) -> StatementChecks:
outcomes = evaluate_relations(lines, ["0"], doc, variant, rules)
return checks_from(outcomes, {"0": 0})
def test_form_variant() -> None:
assert form_variant(BAL, ["1150", "1210", "1600", "1310", "1700"]) is FormVariant.SIMPLIFIED
assert form_variant(BAL, ["1100", "1600"]) is FormVariant.FULL
assert form_variant(INC, ["2110", "2400"]) is FormVariant.SIMPLIFIED
assert form_variant(INC, ["2110", "2100", "2400"]) is FormVariant.FULL
assert form_variant(DocumentType.DISCLOSURE, ["5101"]) is None
def test_empty_target_is_violation(rules: Rules) -> None:
lines = [
line("1110", "30"),
line("1100", "30"),
line("1210", "70"),
line("1200", "70"),
line(None, "100", status=LineStatus.DETAIL),
]
checks = failed(lines, BAL, FormVariant.FULL, rules)
assert [f.relation for f in checks.relations_failed] == ["1600 = 1100 + 1200"]
assert (checks.relations_failed[0].expected, checks.relations_failed[0].actual) == (None, D("100"))
def test_target_without_members_is_violation(rules: Rules) -> None:
lines = [line("1110", "30"), line("1100", "30"), line("1210", "70"), line("1200", "70"), line("1600", "100")]
checks = failed(lines, BAL, FormVariant.FULL, rules)
assert [f.relation for f in checks.relations_failed] == ["1600 = 1700"]
assert checks.relations_failed[0].codes == ("1600", "1700")
assert (checks.relations_failed[0].expected, checks.relations_failed[0].actual) == (D("100"), None)
def test_zero_target_with_empty_members_is_violation(rules: Rules) -> None:
lines = [line("1600", "0"), line("1110", "0"), line("1100", "0")]
hard = failed(lines, BAL, FormVariant.FULL, rules).relations_failed
assert [(f.relation, f.expected, f.actual) for f in hard] == [("1600 = 1700", D("0"), None)]
def test_duplicate_code_is_summed(rules: Rules) -> None:
lines = [line("2300", "849"), line("2410", "100"), line("2410", "649"), line("2400", "100")]
outcomes = evaluate_relations(lines, ["0"], INC, FormVariant.FULL, rules)
target = next(o for o in outcomes if o.rule.relation == "2400 = 2300 - 2410 + 2420")
assert target.within_tolerance
def test_simplified_income_soft(rules: Rules) -> None:
lines = [line("2110", "1000"), line("2120", "-600"), line("2410", "50"), line("2400", "500")]
checks = failed(lines, INC, FormVariant.SIMPLIFIED, rules)
assert checks.relations_failed == ()
assert [f.relation for f in checks.soft_relations_failed] == ["2400 = 2110 - 2120 - 2330 + 2340 - 2350 - 2410"]
def test_rounding_goes_soft(rules: Rules) -> None:
lines = [
line("1600", "17000000009.02"),
line("1700", "17000000000"),
line("1100", "17000000009.02"),
line("1300", "17000000000"),
]
checks = failed(lines, BAL, FormVariant.FULL, rules)
assert "1600 = 1700" in [f.relation for f in checks.soft_relations_failed]
assert "1600 = 1700" not in [f.relation for f in checks.relations_failed]
def test_abs_terms_hold_with_minus(rules: Rules) -> None:
lines = [line("2110", "100"), line("2120", "-60"), line("2100", "40")]
hard = failed(lines, INC, FormVariant.FULL, rules).relations_failed
assert "2100 = 2110 - 2120" not in [f.relation for f in hard]
def test_sublines(rules: Rules) -> None:
assert subline_parents(["1230", "12301", "12302", "1240", "11101"]) == [None, "1230", "1230", None, "1110"]
# расхождение 2 (>1): в плане было 10 против 4 + 5 = 9, это в пределах допуска ±1 — спека «Шаг 7» (T13.1)
lines = [line("1230", "10"), line("12301", "4"), line("12302", "4")]
soft = failed(lines, BAL, FormVariant.FULL, rules).soft_relations_failed
assert [f.relation for f in soft] == ["1230 = 12301 + 12302"]
within = [line("1230", "10"), line("12301", "4"), line("12302", "5")]
assert failed(within, BAL, FormVariant.FULL, rules).soft_relations_failed == ()
empty = [line("1230", "10"), line("12301", None), line("12302", None)]
assert failed(empty, BAL, FormVariant.FULL, rules).soft_relations_failed == ()
def test_is_check_line() -> None:
assert is_check_line(line("1600", "1"))
assert not is_check_line(line("1600", "1", status=LineStatus.DETAIL))
assert not is_check_line(line(None, "1"))
correction = EvalLine("c", None, LineStatus.OK, "1600", 100.0, False, line("1600", None).values)
assert not is_check_line(correction)
with_amount = EvalLine("c", None, LineStatus.OK, "1600", 100.0, False, line("1600", "5").values)
assert is_check_line(with_amount)