"""Соотношения §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)