From f017c37c51ae3f5c348e58dd19ec081c35ce2a0c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 16 Oct 2026 19:10:05 +0400 Subject: [PATCH] =?UTF-8?q?feat(checks):=20=C2=AB=D1=80=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=20=3D=20=CE=A3=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=C2=BB=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20(#45,=20AURORA-1527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit По колонке соотношение считается, только если хотя бы одна подстрока родителя имеет значение (amount не null; ноль — значение). Совпадает с arm-core. Co-Authored-By: Claude Opus 5.5 --- api/src/parser/statements/checks.py | 15 +++++++++++ .../unit/parser/statements/test_checks.py | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/api/src/parser/statements/checks.py b/api/src/parser/statements/checks.py index 338b199..563141c 100644 --- a/api/src/parser/statements/checks.py +++ b/api/src/parser/statements/checks.py @@ -6,6 +6,7 @@ Правило проверяется, если целевой код в данных (подстроки всегда). OQ-138: жёсткое нарушение не больше `rounding_tolerance_percent` от наибольшего |числа| соотношения — округление, уходит в мягкие. +AURORA-1527: «родитель = Σ подстрок» по колонке — только если хотя бы одна подстрока в ней непуста (amount не null). M-9: пятизначная подстрока относится к ближайшей четырёхзначной строке выше с теми же первыми тремя цифрами. """ @@ -33,6 +34,16 @@ def _amounts(lines: Sequence[StatementLine], column_count: int) -> list[dict[str return columns +def _filled(lines: Sequence[StatementLine], column_count: int) -> list[set[str]]: + """Коды строк, у которых в колонке есть значение (`amount` не null; ноль — значение).""" + columns: list[set[str]] = [set() for _ in range(column_count)] + for item in lines: + for value in item.values: + if 0 <= value.column_index < column_count and value.amount is not None: + columns[value.column_index].add(item.line_code) + return columns + + def _rhs(amounts: dict[str, Decimal], rule: RelationRule, *, absolute: bool) -> Decimal: total = _ZERO for sign, code in rule.terms: @@ -102,6 +113,7 @@ def run_statement_checks( hard: list[FailedRelation] = [] soft: list[FailedRelation] = [] warnings: list[str] = [] + filled = _filled(lines, column_count) for column_index, amounts in enumerate(_amounts(lines, column_count)): for rule in relations: # Явные правила из конфига пропускаются, если цель отсутствует в данных колонки. @@ -109,6 +121,9 @@ def run_statement_checks( is_subline = rule in subline_rules if not is_subline and rule.target not in amounts: continue + # AURORA-1527: детализации нет (все подстроки родителя в колонке пусты) — соотношение не проверяется + if is_subline and not any(code in filled[column_index] for _, code in rule.terms): + continue holds, expected, actual = _holds(amounts, rule, config.tolerance) if holds: continue diff --git a/api/tests/unit/parser/statements/test_checks.py b/api/tests/unit/parser/statements/test_checks.py index 95b9ab4..2a2f7ac 100644 --- a/api/tests/unit/parser/statements/test_checks.py +++ b/api/tests/unit/parser/statements/test_checks.py @@ -300,3 +300,28 @@ def test_rounding_base_is_the_largest_number_of_the_relation(rules: Rules) -> No assert checks.relations_failed == [] assert "2200 = 2100 - 2210 - 2220" in [item.relation for item in checks.soft_relations_failed] + + +# --- AURORA-1527: детализации нет — соотношение подстрок не проверяется --- + + +def test_sublines_all_empty_in_column_are_not_checked(rules: Rules) -> None: + lines = [line("1150", "90", "90"), line("11501", None, None), line("11502", None, None)] + checks, warnings = run_statement_checks(lines, 2, "BALANCE", rules) + assert [f for f in checks.soft_relations_failed if f.relation.startswith("1150 =")] == [] + assert not any("1150 =" in w for w in warnings) + + +def test_sublines_check_is_per_column(rules: Rules) -> None: + lines = [line("1150", "90", "90"), line("11501", None, "10"), line("11502", None, None)] + checks, _ = run_statement_checks(lines, 2, "BALANCE", rules) + failed = [f for f in checks.soft_relations_failed if f.relation == "1150 = 11501 + 11502"] + assert [(f.column_index, f.expected, f.actual) for f in failed] == [(1, Decimal("90"), Decimal("10"))] + + +def test_subline_zero_is_a_value(rules: Rules) -> None: + lines = [line("1150", "90"), line("11501", "0"), line("11502", None)] + checks, _ = run_statement_checks(lines, 1, "BALANCE", rules) + assert [f.relation for f in checks.soft_relations_failed if f.relation.startswith("1150 =")] == [ + "1150 = 11501 + 11502" + ]