arm-core/api/src/domain/statement_aggregate.py
Andrey Epifancev 570e82b5d3 refactor(statements): удалена копия словаря §7 ядра — проверки копией arm_statements
Удалены api/rules/statement_checks.yaml, load_check_rules, run_checks и правила соотношений; в
domain/statement_checks.py остались межформенная проверка 1370/2400 и subline_parents. Тест
test_name_dictionary_codes_in_code_orders сверяет справочник имён с перечнями редакций пакета.
CLAUDE.md и README.md: §7 — копия arm_statements, нарушения хранятся в document_issue. Решения P3-50…P3-52.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:08:27 +04:00

89 lines
4.1 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.

"""Агрегат формы ББ/ОФР по коду строки (спека `arm-core`, v5.19; P3-14).
Код строки в форме может повторяться (лист-продолжение, повтор в источнике, добавленная строка). Все чтения «по коду» —
проверки §7, межформенная проверка, `parent_code` подстрок, достройка по шаблону — идут через агрегат: суммы строк
`ok` с одним кодом складываются по колонке, пустая сумма не превращается в 0. Чистая функция, без БД.
"""
import uuid
from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
from src.models.enums import StatementLineStatus
__all__ = ["AggLine", "CodeAggregate", "aggregate_by_code", "checks_input"]
@dataclass(frozen=True)
class AggLine:
"""Строка формы для агрегата: `amounts` — сумма по `column_id` (`None` — пустая ячейка или её нет)."""
id: uuid.UUID
row_index: int | None
line_status: StatementLineStatus
line_code: str | None
amounts: Mapping[uuid.UUID, Decimal | None]
@dataclass(frozen=True)
class CodeAggregate:
code: str
amounts: dict[uuid.UUID, Decimal | None]
line_ids: tuple[uuid.UUID, ...] # по `row_index`, `NULL` (шаблонные) последними
reference_name: str | None
has_source_line: bool # есть строка источника (`row_index IS NOT NULL`)
def _no_name(_: str) -> str | None:
return None
def aggregate_by_code(
lines: Iterable[AggLine], reference_name: Callable[[str], str | None] = _no_name
) -> dict[str, CodeAggregate]:
"""Агрегат строк `ok` по коду, по возрастанию кода. Сумма по колонке — сумма непустых; нет непустых — `None`."""
groups: dict[str, list[AggLine]] = {}
for line in lines:
if line.line_status is StatementLineStatus.ok and line.line_code is not None:
groups.setdefault(line.line_code, []).append(line)
result: dict[str, CodeAggregate] = {}
for code in sorted(groups):
ordered = sorted(groups[code], key=lambda item: (item.row_index is None, item.row_index or 0, str(item.id)))
amounts: dict[uuid.UUID, Decimal | None] = {}
for line in ordered:
for column_id, amount in line.amounts.items():
previous = amounts.get(column_id)
amounts[column_id] = amount if previous is None else previous + (amount or Decimal(0))
result[code] = CodeAggregate(
code=code,
amounts=amounts,
line_ids=tuple(line.id for line in ordered),
reference_name=reference_name(code),
has_source_line=any(line.row_index is not None for line in ordered),
)
return result
class _Column(Protocol):
id: uuid.UUID
column_index: int
def checks_input(
columns: Sequence[_Column], aggregates: Mapping[str, CodeAggregate]
) -> tuple[list[int], list[tuple[str, dict[int, Decimal | None]]]]:
"""Коды формы для `subline_parents` и межформенной проверки: индексы колонок (по возрастанию) и
`(код, {column_index: сумма})` по возрастанию кода.
`columns` — колонки формы (`id`, `column_index`). Код без строки источника и без единой суммы (пустая шаблонная
строка) пропускается (design D-5 шаблонных строк)."""
index_by_id = {column.id: column.column_index for column in columns}
lines = [
(code, {index_by_id[cid]: amount for cid, amount in agg.amounts.items() if cid in index_by_id})
for code, agg in sorted(aggregates.items())
if agg.has_source_line or any(amount is not None for amount in agg.amounts.values())
]
return sorted(column.column_index for column in columns), lines