Удалены 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>
89 lines
4.1 KiB
Python
89 lines
4.1 KiB
Python
"""Агрегат формы ББ/ОФР по коду строки (спека `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
|