Refs #14 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
100 lines
4.8 KiB
Python
100 lines
4.8 KiB
Python
"""Соотношения §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
|