Andrey Epifancev 72e565621c fix(task-11): R-6…R-9 — цель в данных, общий допуск, валидация 4-цифр, тесты
R-6: проверка только если цель присутствует (в колонке)
R-8: убран zero tolerance для подстрок, общий tolerance
R-7: тесты с полными данными (подкомпоненты)
R-9: валидация — явные правила 4-цифр, подстроки 5-цифр
R-7: усиление abs_terms теста (цепочка 2100→2200→2300)

Refs #14

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

156 lines
6.3 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"), # 2100 = 2110 - 2120 = 1000 - (-600) = 1600 (plain) | 1000 - abs(-600) = 400 (abs)
line("2200", "400"), # 2200 = 2100 - 2210 - 2220 = 400 - 0 - 0 = 400 (plain) | abs
line("2300", "400"), # 2300 = 2200 + 2310 + … = 400 + 0 + … = 400
]
brackets = [
line("2110", "1000"),
line("2120", "600"),
line("2100", "400"), # 2100 = 2110 - 2120 = 1000 - 600 = 400
line("2200", "400"), # 2200 = 2100 - 2210 - 2220 = 400 - 0 - 0 = 400
line("2300", "400"), # 2300 = 2200 + … = 400
]
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 == []