Andrey Epifancev d73836e30a test(task-11): T11.I2 — цель отсутствует, члены=0; real abs_terms values
T11.I2: + test_target_absent_no_violations (INCOME: нет 2200; BALANCE: нет 1100)
T11.I2: + test_target_present_members_absent_as_zero (1100=100 без подстатей → нарушение)
test_relation_holds_with_abs_terms: Добавлены ненулевые значения 2210/2220/2330/2350
  1С: 2210=-50, 2220=-30, 2330=-20, 2350=-10
  66н: 2210=50, 2220=30, 2330=20, 2350=10

Refs #14

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 17:49:50 +04:00

190 lines
7.9 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.

"""Соотношения §7 как данные (design D-7; spec «Соотношения §7 — данные, проверка по колонкам»)."""
from decimal import Decimal
import pytest
from src.contract.result import StatementLine, StatementValue
from src.parser.rules import Rules, load_rules
from src.parser.statements.checks import run_statement_checks
@pytest.fixture(scope="module")
def rules() -> Rules:
return load_rules()
def line(code: str, *amounts: str | None) -> StatementLine:
values = [
StatementValue(
column_index=index,
amount=None if amount is None else Decimal(amount),
confidence=100,
is_recognized=True,
)
for index, amount in enumerate(amounts)
]
return StatementLine(
line_code=code,
line_code_confidence=100,
line_name=code,
line_name_confidence=100,
values=values,
)
def test_balance_relations_hold_with_missing_codes_as_zero(rules: Rules) -> None:
lines = [
line("1150", "100", "90"),
line("1100", "100", "90"),
line("1210", "50", "40"),
line("1200", "50", "40"),
line("1600", "150", "130"),
line("1310", "150", "130"),
line("1300", "150", "130"),
line("1700", "150", "130"),
]
checks, warnings = run_statement_checks(lines, 2, "BALANCE", rules)
assert checks.relations_failed == []
assert checks.soft_relations_failed == []
assert warnings == []
def test_broken_total_reported_per_column(rules: Rules) -> None:
lines = [
line("1100", "100", "100"),
line("1200", "50", "50"),
line("1600", "155", "150"),
]
checks, warnings = run_statement_checks(lines, 2, "BALANCE", rules)
failed = [(item.column_index, item.relation, item.expected, item.actual) for item in checks.relations_failed]
assert (0, "1600 = 1100 + 1200", Decimal("155"), Decimal("150")) in failed
assert not any(item.column_index == 1 and item.relation == "1600 = 1100 + 1200" for item in checks.relations_failed)
assert any("1600 = 1100 + 1200" in warning and "колонка 0" in warning for warning in warnings)
def test_tolerance_one_unit(rules: Rules) -> None:
"""Допуск ±1: 1600 = 151 (ожидание), 1100 + 1200 = 100 + 50 = 150 (расчёт), разница = 1."""
lines = [
line("1150", "100"), # компонента 1100
line("1100", "100"), # 1100 = 1105 + … + 1150 + … = 100
line("1210", "50"), # компонента 1200
line("1200", "50"), # 1200 = 1210 + … = 50
line("1600", "151"), # 1600 = 1100 + 1200 = 150 (разница 1 в допуске)
line("1310", "151"), # компонента 1300
line("1300", "151"), # 1300 = 1310 + … = 151
line("1700", "151"), # 1700 = 1300 + … = 151
]
checks, _ = run_statement_checks(lines, 1, "BALANCE", rules)
assert checks.relations_failed == []
def test_relation_holds_with_abs_terms(rules: Rules) -> None:
"""Review Focus 3: 1С пишет расходы со знаком минус, 66н — в скобках; оба варианта сходятся.
Цепочка: 2100 = 2110 - 2120; 2200 = 2100 - 2210 - 2220; 2300 = 2200 + …
В обоих вариантах (1С: минус; 66н: скобки) chain сходится с abs_terms.
"""
one_c = [
line("2110", "1000"),
line("2120", "-600"),
line("2100", "400"), # 1000 - (-600) = 1600 plain | 1000 - abs(-600) = 400 (abs) ✓
line("2210", "-50"),
line("2220", "-30"),
line("2200", "320"), # 400 - (-50) - (-30) = 480 plain | 400 - 50 - 30 = 320 (abs) ✓
line("2330", "-20"),
line("2350", "-10"),
line("2300", "290"), # 320 - (-20) - (-10) = 350 plain | 320 - 20 - 10 = 290 (abs) ✓
]
brackets = [
line("2110", "1000"),
line("2120", "600"),
line("2100", "400"), # 1000 - 600 = 400 ✓
line("2210", "50"),
line("2220", "30"),
line("2200", "320"), # 400 - 50 - 30 = 320 ✓
line("2330", "20"),
line("2350", "10"),
line("2300", "290"), # 320 - 20 - 10 = 290 ✓
]
assert run_statement_checks(one_c, 1, "INCOME", rules)[0].relations_failed == []
assert run_statement_checks(brackets, 1, "INCOME", rules)[0].relations_failed == []
def test_soft_relation_and_sublines(rules: Rules) -> None:
"""Мягкие соотношения и генерируемые подстроки.
Для INCOME: 2410 = 2411 + 2412 мягкое (strict=false) → -100 ≠ -110 (разница 10 > допуска 1) → нарушение.
Для BALANCE: две проверки подстрок:
- 1110 = 11101 + 11102 с разницей 2 (10 vs 8) → нарушение
- 1110 = 11101 + 11102 с разницей 1 (10 vs 9) → без нарушения (в допуске)
"""
# Case 1: нарушение подстроки (разница > допуска)
lines_case1 = [
line("2400", "-100"),
line("2410", "-100"),
line("2411", "-80"),
line("2412", "-30"), # -80 + -30 = -110, но 2410 = -100, разница 10 > допуска 1
line("1110", "10"),
line("11101", "6"),
line("11102", "2"), # 6 + 2 = 8, но 1110 = 10, разница 2 > допуска 1
]
checks, warnings = run_statement_checks(lines_case1, 1, "INCOME", rules)
balance, _ = run_statement_checks(lines_case1, 1, "BALANCE", rules)
assert [item.relation for item in checks.soft_relations_failed] == ["2410 = 2411 + 2412"]
assert checks.relations_failed == []
assert [item.relation for item in balance.soft_relations_failed] == ["1110 = 11101 + 11102"]
assert any("мягкое" in warning for warning in warnings)
# Case 2: подстрока в пределах допуска (разница = 1)
lines_case2 = [
line("1110", "10"),
line("11101", "6"),
line("11102", "3"), # 6 + 3 = 9, но 1110 = 10, разница 1 = допуска → нет нарушения
]
balance2, _ = run_statement_checks(lines_case2, 1, "BALANCE", rules)
assert balance2.soft_relations_failed == []
def test_null_values_count_as_zero_and_empty_form_passes(rules: Rules) -> None:
lines = [line("1100", None), line("1200", None), line("1600", None)]
checks, _ = run_statement_checks(lines, 1, "BALANCE", rules)
assert checks.relations_failed == []
def test_target_absent_no_violations(rules: Rules) -> None:
"""R-6: Если цели правила нет в данных, проверка пропускается → нарушений нет."""
# INCOME: 2100, 2110, 2120 есть; 2200 НЕТУ → правило 2200 не проверяется
lines_income = [line("2110", "1000"), line("2120", "-600"), line("2100", "400")]
checks_income, _ = run_statement_checks(lines_income, 1, "INCOME", rules)
assert checks_income.relations_failed == []
assert checks_income.soft_relations_failed == []
# BALANCE: только 1150 БЕЗ 1100 → правило 1100 не проверяется (1100 это цель правила)
lines_balance = [line("1150", "100")]
checks_balance, _ = run_statement_checks(lines_balance, 1, "BALANCE", rules)
assert checks_balance.relations_failed == []
def test_target_present_members_absent_as_zero(rules: Rules) -> None:
"""R-6: Цель есть, члены отсутствуют (= 0) → соотношение с нарушением."""
# BALANCE: 1100 = 100 БЕЗ подстатей → 1100 = 0 + 0 + … = 0 (не совпадает с 100)
lines = [line("1100", "100")]
checks, _ = run_statement_checks(lines, 1, "BALANCE", rules)
# Правило 1100 = 1105 + … (strict: true) должно нарушиться
failed_relations = [item.relation for item in checks.relations_failed]
assert any("1100 =" in rel for rel in failed_relations)