From 2c0a2ad93931b43668fda71bf7bf0373ef1ac56c Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 21 Oct 2026 12:15:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/services/consolidated_osv.py | 57 ++++++++++++++++--------- api/src/services/intercompany_matrix.py | 22 +++++----- api/tests/test_intercompany_matrix.py | 6 ++- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/api/src/services/consolidated_osv.py b/api/src/services/consolidated_osv.py index 4e6542b..a9281ce 100644 --- a/api/src/services/consolidated_osv.py +++ b/api/src/services/consolidated_osv.py @@ -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.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( diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index c28c3cc..b21789d 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -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, diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index efdd2eb..236465f 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -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(