Andrey Epifancev 065bcba25a AURORA-1864: группа альтернатив мягких соотношений, 0.1.4 (#3)
statement_checks.yaml: необязательный ключ group у мягких соотношений одной
формы и варианта. В колонке сошлась хоть одна альтернатива — нарушений группы
нет; не сошлась ни одна — одно нарушение, ближайшая к цели. group у жёсткого —
ошибка загрузки.

Соотношения 2400 ОФР — группа net_profit; в упрощённую форму добавлен вариант
с +2410. Положительный 2410 (доход по налогу, ФСБУ 4/2023) больше не даёт
ложного «2400 = 2300 − 2410 + 2420». Прогон ОФР Фарватера — без нарушений.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:55:22 +04:00

164 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 == ()
# группа альтернатив 2400 (AURORA-1864): одно нарушение — ближайшая к цели (450 против 350)
assert [f.relation for f in checks.soft_relations_failed] == ["2400 = 2110 - 2120 - 2330 + 2340 - 2350 + 2410"]
def _net_profit_soft(checks: StatementChecks) -> list[str]:
return [f.relation for f in checks.soft_relations_failed if f.relation.startswith("2400 =")]
def test_net_profit_group_full_any_sign_of_2410(rules: Rules) -> None:
"""AURORA-1864: 2410 доходом (плюс), расходом с минусом и расходом без знака — 2400 без мягких нарушений."""
for tax, net in (("20", "120"), ("-20", "80"), ("20", "80")):
lines = [line("2300", "100"), line("2410", tax), line("2400", net)]
checks = failed(lines, INC, FormVariant.FULL, rules)
assert _net_profit_soft(checks) == [], (tax, net)
def test_net_profit_group_simplified_any_sign_of_2410(rules: Rules) -> None:
for tax, net in (("50", "450"), ("-50", "350"), ("50", "350")):
lines = [line("2110", "1000"), line("2120", "-600"), line("2410", tax), line("2400", net)]
assert _net_profit_soft(failed(lines, INC, FormVariant.SIMPLIFIED, rules)) == [], (tax, net)
def test_net_profit_group_wrong_target_is_one_violation(rules: Rules) -> None:
lines = [line("2300", "100"), line("2410", "20"), line("2400", "150")]
soft = failed(lines, INC, FormVariant.FULL, rules).soft_relations_failed
net = [f for f in soft if f.relation.startswith("2400 =")]
# ближайшая к цели: 100 + 20 = 120 (расхождение 30) против 100 − 20 = 80 (70)
assert [(f.relation, f.expected, f.actual) for f in net] == [("2400 = 2300 + 2410 + 2460", D("150"), D("120"))]
def test_net_profit_group_empty_target_is_one_violation(rules: Rules) -> None:
lines = [line("2300", "100"), line("2410", "20")]
net = _net_profit_soft(failed(lines, INC, FormVariant.FULL, rules))
assert net == ["2400 = 2300 - 2410 + 2420"]
def test_net_profit_group_is_per_column(rules: Rules) -> None:
def two(code: str, a: str, b: str) -> EvalLine:
return EvalLine(
f"k{code}",
0,
LineStatus.OK,
code,
100.0,
False,
(
EvalValue("0", D(a), a, 100.0, True),
EvalValue("1", D(b), b, 100.0, True),
),
)
lines = [two("2300", "100", "100"), two("2410", "20", "20"), two("2400", "120", "150")]
outcomes = evaluate_relations(lines, ["0", "1"], INC, FormVariant.FULL, rules)
soft = checks_from(outcomes, {"0": 0, "1": 1}).soft_relations_failed
assert [(f.column_key, f.relation) for f in soft if f.relation.startswith("2400 =")] == [
("1", "2400 = 2300 + 2410 + 2460")
]
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)