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