Andrey Epifancev 7bcd324194 feat(statements): соотношения §7 из словаря — жёсткие/мягкие, abs_terms, допуск, подстроки
Refs #14

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

100 lines
4.8 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): правила — `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