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)