правки по ревью 2
This commit is contained in:
parent
abe1d890d0
commit
2c0a2ad939
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Collection, Sequence
|
from collections.abc import Collection, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi.concurrency import run_in_threadpool
|
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()}
|
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:
|
class ConsolidatedOsvService:
|
||||||
"""Чтение сводной ОСВ версии: страница с фильтрами и сводка с пробелами (без блокировок и аудита)."""
|
"""Чтение сводной ОСВ версии: страница с фильтрами и сводка с пробелами (без блокировок и аудита)."""
|
||||||
|
|
||||||
@ -136,14 +147,13 @@ class ConsolidatedOsvService:
|
|||||||
self._session = session
|
self._session = session
|
||||||
self._scope = scope
|
self._scope = scope
|
||||||
|
|
||||||
async def build(self, version_id: uuid.UUID) -> tuple[domain.Consolidated, ConsolidationVersion, Consolidation]:
|
async def build(self, version_id: uuid.UUID) -> ConsolidatedOsvBuildResult:
|
||||||
"""Свод версии (`domain.build`) вместе с версией и консолидацией; версия вне видимости — 404."""
|
"""Свод версии и данные его расчёта; версия вне видимости — 404."""
|
||||||
consolidated, version, consolidation, _ = await self.build_with_members(version_id)
|
return await self.build_with_members(version_id)
|
||||||
return consolidated, version, consolidation
|
|
||||||
|
|
||||||
async def build_with_members(
|
async def build_with_members(
|
||||||
self, version_id: uuid.UUID
|
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)
|
found = await repo.get_version(self._session, self._scope, version_id)
|
||||||
if found is None:
|
if found is None:
|
||||||
@ -174,12 +184,17 @@ class ConsolidatedOsvService:
|
|||||||
rows=rows,
|
rows=rows,
|
||||||
resolver=index,
|
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]:
|
async def page(self, version_id: uuid.UUID, f: OsvFilters, page: OsvPageParams) -> Page[ConsolidatedRowOut]:
|
||||||
"""Страница свода после фильтров: `total` — число строк после фильтра, срез — в памяти (порядок D-16)."""
|
"""Страница свода после фильтров: `total` — число строк после фильтра, срез — в памяти (порядок D-16)."""
|
||||||
consolidated, _, _ = await self.build(version_id)
|
result = await self.build(version_id)
|
||||||
rows = domain.apply_filter(consolidated.rows, domain_filter(f))
|
rows = domain.apply_filter(result.consolidated.rows, domain_filter(f))
|
||||||
window = rows[page.offset : page.offset + page.limit]
|
window = rows[page.offset : page.offset + page.limit]
|
||||||
return Page[ConsolidatedRowOut](
|
return Page[ConsolidatedRowOut](
|
||||||
items=[row_out(row) for row in window], total=len(rows), limit=page.limit, offset=page.offset
|
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`, без пагинации, в
|
"""xlsx сводной ОСВ (spec «Экспорт xlsx», D-14): строки после тех же фильтров, что у `page`, без пагинации, в
|
||||||
порядке D-16; лист пробелов — по своду целиком. Имя файла — `svod-osv_<ГК>_<год>-<квартал>_v<n>.xlsx` (без
|
порядке D-16; лист пробелов — по своду целиком. Имя файла — `svod-osv_<ГК>_<год>-<квартал>_v<n>.xlsx` (без
|
||||||
percent-кодирования: оно в маршруте). `audit_event(EXPORT)` пишется после сборки книги, в транзакции запроса."""
|
percent-кодирования: оно в маршруте). `audit_event(EXPORT)` пишется после сборки книги, в транзакции запроса."""
|
||||||
consolidated, version, consolidation = await self.build(version_id)
|
result = await self.build(version_id)
|
||||||
rows = domain.apply_filter(consolidated.rows, domain_filter(f))
|
rows = domain.apply_filter(result.consolidated.rows, domain_filter(f))
|
||||||
content = await run_in_threadpool(build_workbook, consolidated, rows)
|
content = await run_in_threadpool(build_workbook, result.consolidated, rows)
|
||||||
group_name = (
|
group_name = (
|
||||||
await self._session.execute(
|
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()
|
).scalar_one()
|
||||||
await audit.record(
|
await audit.record(
|
||||||
action=AuditAction.EXPORT,
|
action=AuditAction.EXPORT,
|
||||||
section=AuditSection.EXPORT,
|
section=AuditSection.EXPORT,
|
||||||
object_type="consolidation_version",
|
object_type="consolidation_version",
|
||||||
object_id=version.id,
|
object_id=result.version.id,
|
||||||
consolidation_version_id=version.id,
|
consolidation_version_id=result.version.id,
|
||||||
after_value={
|
after_value={
|
||||||
"report": "consolidated_osv",
|
"report": "consolidated_osv",
|
||||||
"filters": f.model_dump(mode="json", exclude_none=True),
|
"filters": f.model_dump(mode="json", exclude_none=True),
|
||||||
@ -210,27 +225,27 @@ class ConsolidatedOsvService:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
filename = (
|
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
|
return content, filename
|
||||||
|
|
||||||
async def summary(self, version_id: uuid.UUID) -> OsvSummaryOut:
|
async def summary(self, version_id: uuid.UUID) -> OsvSummaryOut:
|
||||||
"""Сводка: строки, несопоставленные, автосопоставленные по имени/алиасу, ОСВ-документы версии и пробелы."""
|
"""Сводка: строки, несопоставленные, автосопоставленные по имени/алиасу, ОСВ-документы версии и пробелы."""
|
||||||
consolidated, _, _ = await self.build(version_id)
|
result = await self.build(version_id)
|
||||||
rows_total, unmapped_total, auto_matched_total = domain.summary(consolidated)
|
rows_total, unmapped_total, auto_matched_total = domain.summary(result.consolidated)
|
||||||
return OsvSummaryOut(
|
return OsvSummaryOut(
|
||||||
rows_total=rows_total,
|
rows_total=rows_total,
|
||||||
unmapped_total=unmapped_total,
|
unmapped_total=unmapped_total,
|
||||||
auto_matched_total=auto_matched_total,
|
auto_matched_total=auto_matched_total,
|
||||||
documents_total=consolidated.documents_total,
|
documents_total=result.consolidated.documents_total,
|
||||||
gaps=[GapOut.model_validate(gap, from_attributes=True) for gap in consolidated.gaps],
|
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:
|
async def row_for_entry(self, version_id: uuid.UUID, entry_id: uuid.UUID) -> ConsolidatedRowOut | None:
|
||||||
"""Строка свода, в `entry_ids` которой есть `entry_id`; `None` — строка в своде не участвует (пара с пробелом
|
"""Строка свода, в `entry_ids` которой есть `entry_id`; `None` — строка в своде не участвует (пара с пробелом
|
||||||
или документ не выбран источником при наличии нарастающего)."""
|
или документ не выбран источником при наличии нарастающего)."""
|
||||||
consolidated, _, _ = await self.build(version_id)
|
result = await self.build(version_id)
|
||||||
row = next((row for row in consolidated.rows if entry_id in row.entry_ids), None)
|
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)
|
return None if row is None else row_out(row)
|
||||||
|
|
||||||
async def candidates(
|
async def candidates(
|
||||||
|
|||||||
@ -317,8 +317,8 @@ class IntercompanyMatrixService:
|
|||||||
selection: MatrixSelection,
|
selection: MatrixSelection,
|
||||||
) -> IntercompanyMatrixOut:
|
) -> IntercompanyMatrixOut:
|
||||||
"""Счета принимает готовыми; выбор режима ALL/MANUAL выполняется вызывающим кодом."""
|
"""Счета принимает готовыми; выбор режима ALL/MANUAL выполняется вызывающим кодом."""
|
||||||
consolidated, _, _, members = await self._osv_service.build_with_members(version_id)
|
result = await self._osv_service.build_with_members(version_id)
|
||||||
return self._calculate(consolidated.rows, members, selection)
|
return self._calculate(result.consolidated.rows, result.members, selection)
|
||||||
|
|
||||||
def _calculate(
|
def _calculate(
|
||||||
self, rows: Iterable[ConsolidatedRow], members: dict[uuid.UUID, Entity], selection: MatrixSelection
|
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:
|
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)
|
accounts = await directory_repo.list_accounts(self._session)
|
||||||
companies = [IntercompanyMatrixCompanyOut(id=entity.id, name=entity.name) for entity in members.values()]
|
companies = [IntercompanyMatrixCompanyOut(id=entity.id, name=entity.name) for entity in result.members.values()]
|
||||||
return calculate_result_table(consolidated.rows, companies, accounts)
|
return calculate_result_table(result.consolidated.rows, companies, accounts)
|
||||||
|
|
||||||
async def build_accounts(
|
async def build_accounts(
|
||||||
self,
|
self,
|
||||||
@ -510,8 +510,8 @@ class IntercompanyMatrixService:
|
|||||||
credit_account_ids: Sequence[uuid.UUID],
|
credit_account_ids: Sequence[uuid.UUID],
|
||||||
rule_id: uuid.UUID | None = None,
|
rule_id: uuid.UUID | None = None,
|
||||||
) -> MatrixBuildResult:
|
) -> MatrixBuildResult:
|
||||||
consolidated, version, consolidation, members = await self._osv_service.build_with_members(version_id)
|
osv_result = await self._osv_service.build_with_members(version_id)
|
||||||
rows = consolidated.rows
|
rows = osv_result.consolidated.rows
|
||||||
accounts = await directory_repo.list_accounts(self._session)
|
accounts = await directory_repo.list_accounts(self._session)
|
||||||
by_id, children = _make_account_tree(accounts)
|
by_id, children = _make_account_tree(accounts)
|
||||||
rule: EliminationRule | None = None
|
rule: EliminationRule | None = None
|
||||||
@ -528,16 +528,16 @@ class IntercompanyMatrixService:
|
|||||||
credit = _select_accounts(credit_account_ids, (), "K", by_id, children)
|
credit = _select_accounts(credit_account_ids, (), "K", by_id, children)
|
||||||
selection = MatrixSelection(debit, credit, "closing")
|
selection = MatrixSelection(debit, credit, "closing")
|
||||||
elif mode == "PRESET":
|
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:
|
else:
|
||||||
raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов")
|
raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов")
|
||||||
matrix = self._calculate(rows, members, selection)
|
matrix = self._calculate(rows, osv_result.members, selection)
|
||||||
if rule is not None:
|
if rule is not None:
|
||||||
matrix = await self._apply_corrections(matrix, version_id, rule.id)
|
matrix = await self._apply_corrections(matrix, version_id, rule.id)
|
||||||
return MatrixBuildResult(
|
return MatrixBuildResult(
|
||||||
matrix=matrix,
|
matrix=matrix,
|
||||||
version=version,
|
version=osv_result.version,
|
||||||
consolidation=consolidation,
|
consolidation=osv_result.consolidation,
|
||||||
accounts=accounts,
|
accounts=accounts,
|
||||||
selection=selection,
|
selection=selection,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
|
|||||||
@ -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)
|
version = SimpleNamespace(elimination_rules_version=1, version_no=2)
|
||||||
consolidation = SimpleNamespace(company_group_id=uuid.uuid4(), year=2024, quarter=matrix.PeriodQuarter.Q3)
|
consolidation = SimpleNamespace(company_group_id=uuid.uuid4(), year=2024, quarter=matrix.PeriodQuarter.Q3)
|
||||||
osv_service = SimpleNamespace(
|
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]))
|
monkeypatch.setattr(matrix.directory_repo, "list_accounts", AsyncMock(return_value=[parent, child]))
|
||||||
rule = SimpleNamespace(
|
rule = SimpleNamespace(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user