"""Соотношения §7 по колонкам (design D-7): правила — `statement_checks.yaml`; отсутствующий код — 0; правая часть считается как записано и с членами `abs_terms` по модулю; допуск `tolerance`. Жёсткие нарушения → `relations_failed`, мягкие и «сумма подстрок = родитель» → `soft_relations_failed`; всё дублируется предупреждениями. """ from collections.abc import Sequence from decimal import Decimal from src.contract.result import FailedRelation, StatementChecks, StatementLine from src.parser.rules import FormType, RelationRule, Rules __all__ = ["run_statement_checks"] _ZERO = Decimal("0") _PARENT_LEN = 4 def _amounts(lines: Sequence[StatementLine], column_count: int) -> list[dict[str, Decimal]]: """Код → сумма по каждой колонке (null → 0).""" columns: list[dict[str, Decimal]] = [{} for _ in range(column_count)] for item in lines: for value in item.values: if 0 <= value.column_index < column_count: columns[value.column_index][item.line_code] = value.amount if value.amount is not None else _ZERO return columns def _rhs(amounts: dict[str, Decimal], rule: RelationRule, *, absolute: bool) -> Decimal: total = _ZERO for sign, code in rule.terms: value = amounts.get(code, _ZERO) if absolute and code in rule.abs_terms: value = abs(value) total += sign * value return total def _holds( amounts: dict[str, Decimal], rule: RelationRule, tolerance: Decimal, is_subline: bool = False ) -> tuple[bool, Decimal, Decimal]: expected = amounts.get(rule.target, _ZERO) plain = _rhs(amounts, rule, absolute=False) # Subline checks (sums of sub-codes) use strict equality without tolerance check_tolerance = _ZERO if is_subline else tolerance if abs(expected - plain) <= check_tolerance: return True, expected, plain if rule.abs_terms: with_abs = _rhs(amounts, rule, absolute=True) if abs(expected - with_abs) <= check_tolerance: return True, expected, with_abs return False, expected, plain def _subline_rules(lines: Sequence[StatementLine], form: FormType, rules: Rules) -> list[RelationRule]: """«1110 = 11101 + 11102 + …» для каждого четырёхзначного кода формы (в диапазоне кодов этой формы), у которого есть пятизначные подстроки.""" low, high = rules.statement_headers.code_ranges[form] codes = [item.line_code for item in lines] rules_out: list[RelationRule] = [] for parent in codes: if len(parent) != _PARENT_LEN or not low <= int(parent) <= high: continue children = [code for code in codes if len(code) == _PARENT_LEN + 1 and code.startswith(parent)] if children: rules_out.append(RelationRule(form=form, relation=f"{parent} = {' + '.join(children)}", strict=False)) return rules_out def run_statement_checks( lines: Sequence[StatementLine], column_count: int, form: FormType, rules: Rules ) -> tuple[StatementChecks, list[str]]: config = rules.statement_checks relations = list(config.relations_for(form)) subline_rules = _subline_rules(lines, form, rules) if config.subline_check else [] relations.extend(subline_rules) checks = StatementChecks() warnings: list[str] = [] for column_index, amounts in enumerate(_amounts(lines, column_count)): for rule in relations: # Skip explicit rules (from config) where the target code is not in the data # but always check generated subline rules if they were generated is_subline = rule in subline_rules if not is_subline and rule.target not in amounts: continue holds, expected, actual = _holds(amounts, rule, config.tolerance, is_subline=is_subline) if holds: continue failed = FailedRelation(column_index=column_index, relation=rule.relation, expected=expected, actual=actual) if rule.strict: checks.relations_failed.append(failed) warning_msg = f"соотношение «{rule.relation}» нарушено (колонка {column_index}): {expected} ≠ {actual}" warnings.append(warning_msg) else: checks.soft_relations_failed.append(failed) warning_msg = ( f"мягкое соотношение «{rule.relation}» нарушено (колонка {column_index}): {expected} ≠ {actual}" ) warnings.append(warning_msg) return checks, warnings