правки по ревью 2

This commit is contained in:
PotapovaA 2026-10-21 12:15:20 +03:00
parent abe1d890d0
commit 2c0a2ad939
3 changed files with 52 additions and 33 deletions

View File

@ -13,6 +13,7 @@
import uuid
from collections.abc import Collection, Sequence
from dataclasses import dataclass
from typing import Literal
from fastapi.concurrency import run_in_threadpool
@ -129,6 +130,16 @@ async def _entities(session: AsyncSession, ids: Collection[uuid.UUID]) -> dict[u
return {entity.id: _entity(entity) for entity in found.scalars()}
@dataclass(frozen=True)
class ConsolidatedOsvBuildResult:
"""Свод и данные версии, загруженные для его расчёта."""
consolidated: domain.Consolidated
version: ConsolidationVersion
consolidation: Consolidation
members: dict[uuid.UUID, domain.Entity]
class ConsolidatedOsvService:
"""Чтение сводной ОСВ версии: страница с фильтрами и сводка с пробелами (без блокировок и аудита)."""
@ -136,14 +147,13 @@ class ConsolidatedOsvService:
self._session = session
self._scope = scope
async def build(self, version_id: uuid.UUID) -> tuple[domain.Consolidated, ConsolidationVersion, Consolidation]:
"""Свод версии (`domain.build`) вместе с версией и консолидацией; версия вне видимости — 404."""
consolidated, version, consolidation, _ = await self.build_with_members(version_id)
return consolidated, version, consolidation
async def build(self, version_id: uuid.UUID) -> ConsolidatedOsvBuildResult:
"""Свод версии и данные его расчёта; версия вне видимости — 404."""
return await self.build_with_members(version_id)
async def build_with_members(
self, version_id: uuid.UUID
) -> tuple[domain.Consolidated, ConsolidationVersion, Consolidation, dict[uuid.UUID, domain.Entity]]:
) -> ConsolidatedOsvBuildResult:
"""Свод и участники периметра, загруженные при его расчёте."""
found = await repo.get_version(self._session, self._scope, version_id)
if found is None:
@ -174,12 +184,17 @@ class ConsolidatedOsvService:
rows=rows,
resolver=index,
)
return consolidated, version, consolidation, members
return ConsolidatedOsvBuildResult(
consolidated=consolidated,
version=version,
consolidation=consolidation,
members=members,
)
async def page(self, version_id: uuid.UUID, f: OsvFilters, page: OsvPageParams) -> Page[ConsolidatedRowOut]:
"""Страница свода после фильтров: `total` — число строк после фильтра, срез — в памяти (порядок D-16)."""
consolidated, _, _ = await self.build(version_id)
rows = domain.apply_filter(consolidated.rows, domain_filter(f))
result = await self.build(version_id)
rows = domain.apply_filter(result.consolidated.rows, domain_filter(f))
window = rows[page.offset : page.offset + page.limit]
return Page[ConsolidatedRowOut](
items=[row_out(row) for row in window], total=len(rows), limit=page.limit, offset=page.offset
@ -189,20 +204,20 @@ class ConsolidatedOsvService:
"""xlsx сводной ОСВ (spec «Экспорт xlsx», D-14): строки после тех же фильтров, что у `page`, без пагинации, в
порядке D-16; лист пробелов — по своду целиком. Имя файла — `svod-osv_<ГК>_<год>-<квартал>_v<n>.xlsx` (без
percent-кодирования: оно в маршруте). `audit_event(EXPORT)` пишется после сборки книги, в транзакции запроса."""
consolidated, version, consolidation = await self.build(version_id)
rows = domain.apply_filter(consolidated.rows, domain_filter(f))
content = await run_in_threadpool(build_workbook, consolidated, rows)
result = await self.build(version_id)
rows = domain.apply_filter(result.consolidated.rows, domain_filter(f))
content = await run_in_threadpool(build_workbook, result.consolidated, rows)
group_name = (
await self._session.execute(
select(CompanyGroup.name).where(CompanyGroup.id == consolidation.company_group_id)
select(CompanyGroup.name).where(CompanyGroup.id == result.consolidation.company_group_id)
)
).scalar_one()
await audit.record(
action=AuditAction.EXPORT,
section=AuditSection.EXPORT,
object_type="consolidation_version",
object_id=version.id,
consolidation_version_id=version.id,
object_id=result.version.id,
consolidation_version_id=result.version.id,
after_value={
"report": "consolidated_osv",
"filters": f.model_dump(mode="json", exclude_none=True),
@ -210,27 +225,27 @@ class ConsolidatedOsvService:
},
)
filename = (
f"svod-osv_{group_name}_{consolidation.year}-{consolidation.quarter.value}_v{version.version_no}.xlsx"
f"svod-osv_{group_name}_{result.consolidation.year}-{result.consolidation.quarter.value}_v{result.version.version_no}.xlsx"
)
return content, filename
async def summary(self, version_id: uuid.UUID) -> OsvSummaryOut:
"""Сводка: строки, несопоставленные, автосопоставленные по имени/алиасу, ОСВ-документы версии и пробелы."""
consolidated, _, _ = await self.build(version_id)
rows_total, unmapped_total, auto_matched_total = domain.summary(consolidated)
result = await self.build(version_id)
rows_total, unmapped_total, auto_matched_total = domain.summary(result.consolidated)
return OsvSummaryOut(
rows_total=rows_total,
unmapped_total=unmapped_total,
auto_matched_total=auto_matched_total,
documents_total=consolidated.documents_total,
gaps=[GapOut.model_validate(gap, from_attributes=True) for gap in consolidated.gaps],
documents_total=result.consolidated.documents_total,
gaps=[GapOut.model_validate(gap, from_attributes=True) for gap in result.consolidated.gaps],
)
async def row_for_entry(self, version_id: uuid.UUID, entry_id: uuid.UUID) -> ConsolidatedRowOut | None:
"""Строка свода, в `entry_ids` которой есть `entry_id`; `None` — строка в своде не участвует (пара с пробелом
или документ не выбран источником при наличии нарастающего)."""
consolidated, _, _ = await self.build(version_id)
row = next((row for row in consolidated.rows if entry_id in row.entry_ids), None)
result = await self.build(version_id)
row = next((row for row in result.consolidated.rows if entry_id in row.entry_ids), None)
return None if row is None else row_out(row)
async def candidates(

View File

@ -317,8 +317,8 @@ class IntercompanyMatrixService:
selection: MatrixSelection,
) -> IntercompanyMatrixOut:
"""Счета принимает готовыми; выбор режима ALL/MANUAL выполняется вызывающим кодом."""
consolidated, _, _, members = await self._osv_service.build_with_members(version_id)
return self._calculate(consolidated.rows, members, selection)
result = await self._osv_service.build_with_members(version_id)
return self._calculate(result.consolidated.rows, result.members, selection)
def _calculate(
self, rows: Iterable[ConsolidatedRow], members: dict[uuid.UUID, Entity], selection: MatrixSelection
@ -329,10 +329,10 @@ class IntercompanyMatrixService:
async def build_result_table(self, version_id: uuid.UUID) -> IntercompanyResultTableOut:
"""Статичная таблица по пяти статьям"""
consolidated, _, _, members = await self._osv_service.build_with_members(version_id)
result = await self._osv_service.build_with_members(version_id)
accounts = await directory_repo.list_accounts(self._session)
companies = [IntercompanyMatrixCompanyOut(id=entity.id, name=entity.name) for entity in members.values()]
return calculate_result_table(consolidated.rows, companies, accounts)
companies = [IntercompanyMatrixCompanyOut(id=entity.id, name=entity.name) for entity in result.members.values()]
return calculate_result_table(result.consolidated.rows, companies, accounts)
async def build_accounts(
self,
@ -510,8 +510,8 @@ class IntercompanyMatrixService:
credit_account_ids: Sequence[uuid.UUID],
rule_id: uuid.UUID | None = None,
) -> MatrixBuildResult:
consolidated, version, consolidation, members = await self._osv_service.build_with_members(version_id)
rows = consolidated.rows
osv_result = await self._osv_service.build_with_members(version_id)
rows = osv_result.consolidated.rows
accounts = await directory_repo.list_accounts(self._session)
by_id, children = _make_account_tree(accounts)
rule: EliminationRule | None = None
@ -528,16 +528,16 @@ class IntercompanyMatrixService:
credit = _select_accounts(credit_account_ids, (), "K", by_id, children)
selection = MatrixSelection(debit, credit, "closing")
elif mode == "PRESET":
selection, rule = await self._resolve_preset_selection(rule_id, version, by_id, children)
selection, rule = await self._resolve_preset_selection(rule_id, osv_result.version, by_id, children)
else:
raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов")
matrix = self._calculate(rows, members, selection)
matrix = self._calculate(rows, osv_result.members, selection)
if rule is not None:
matrix = await self._apply_corrections(matrix, version_id, rule.id)
return MatrixBuildResult(
matrix=matrix,
version=version,
consolidation=consolidation,
version=osv_result.version,
consolidation=osv_result.consolidation,
accounts=accounts,
selection=selection,
rows=rows,

View File

@ -120,7 +120,11 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
version = SimpleNamespace(elimination_rules_version=1, version_no=2)
consolidation = SimpleNamespace(company_group_id=uuid.uuid4(), year=2024, quarter=matrix.PeriodQuarter.Q3)
osv_service = SimpleNamespace(
build_with_members=AsyncMock(return_value=(SimpleNamespace(rows=[]), version, consolidation, {}))
build_with_members=AsyncMock(
return_value=SimpleNamespace(
consolidated=SimpleNamespace(rows=[]), version=version, consolidation=consolidation, members={}
)
)
)
monkeypatch.setattr(matrix.directory_repo, "list_accounts", AsyncMock(return_value=[parent, child]))
rule = SimpleNamespace(