api: рабочий стол консолидации для ЮЛ и consolidation_id в карточке файла (#96)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d5d09d0b5c
commit
956a730a3c
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.api.endpoints.files import comments, documents, read, statements, upload
|
from src.api.endpoints.files import comments, consolidation_files, documents, read, statements, upload
|
||||||
|
|
||||||
router = APIRouter(tags=["files"])
|
router = APIRouter(tags=["files"])
|
||||||
router.include_router(upload.router)
|
router.include_router(upload.router)
|
||||||
router.include_router(read.router)
|
router.include_router(read.router)
|
||||||
|
router.include_router(consolidation_files.router)
|
||||||
router.include_router(documents.router)
|
router.include_router(documents.router)
|
||||||
router.include_router(comments.router)
|
router.include_router(comments.router)
|
||||||
router.include_router(statements.router)
|
router.include_router(statements.router)
|
||||||
|
|||||||
46
api/src/api/endpoints/files/consolidation_files.py
Normal file
46
api/src/api/endpoints/files/consolidation_files.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Файлы ЮЛ в контексте консолидации (v5.18, RSHB/arm-ka-docs#90): `GET /consolidations/{consolidation_id}/
|
||||||
|
legal-entities/{legal_entity_id}/files` — рабочий стол консолидации для ЮЛ (spec documents-api «Рабочий стол
|
||||||
|
консолидации для ЮЛ»);
|
||||||
|
`POST …/files` — загрузка в консолидацию (spec primary-files «Загрузка файлов в консолидацию»). Права — как у маршрутов
|
||||||
|
ЮЛ (`DOCUMENT_VIEW` / `DOCUMENT_EDIT`, design R-1); консолидация или ЮЛ вне видимости — 404, как несуществующие."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query, status
|
||||||
|
|
||||||
|
from src.api.dependencies.auth import require
|
||||||
|
from src.api.dependencies.files import PrimaryFilesServiceDep
|
||||||
|
from src.api.endpoints.files.read import FileListQuery
|
||||||
|
from src.schemas.common import ErrorDetail, Page, PageParams
|
||||||
|
from src.schemas.files import FileOut
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
VIEW = [Depends(require("DOCUMENT_VIEW"))]
|
||||||
|
EDIT = [Depends(require("DOCUMENT_EDIT"))]
|
||||||
|
NOT_FOUND: dict[int | str, dict[str, Any]] = {
|
||||||
|
status.HTTP_404_NOT_FOUND: {"model": ErrorDetail, "description": "консолидация или ЮЛ: нет или вне видимости"}
|
||||||
|
}
|
||||||
|
DESK_PATH = "/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
DESK_PATH,
|
||||||
|
response_model=Page[FileOut],
|
||||||
|
dependencies=VIEW,
|
||||||
|
responses=NOT_FOUND,
|
||||||
|
summary="Рабочий стол консолидации для ЮЛ",
|
||||||
|
)
|
||||||
|
async def list_desk_files(
|
||||||
|
consolidation_id: uuid.UUID,
|
||||||
|
legal_entity_id: uuid.UUID,
|
||||||
|
service: PrimaryFilesServiceDep,
|
||||||
|
query: Annotated[FileListQuery, Query()],
|
||||||
|
) -> Page[FileOut]:
|
||||||
|
"""Файлы ЮЛ, загруженные в эту консолидацию, плюс файлы с документами, привязанными к её версиям (новые первыми)."""
|
||||||
|
page = PageParams(limit=query.limit, offset=query.offset)
|
||||||
|
items, total = await service.page_desk(
|
||||||
|
consolidation_id, legal_entity_id, include_superseded=query.include_superseded, page=page
|
||||||
|
)
|
||||||
|
return Page[FileOut](items=items, total=total, limit=page.limit, offset=page.offset)
|
||||||
@ -71,6 +71,14 @@ async def get_version(
|
|||||||
return None if row is None else (row[0], row[1])
|
return None if row is None else (row[0], row[1])
|
||||||
|
|
||||||
|
|
||||||
|
async def get_consolidation(
|
||||||
|
session: AsyncSession, scope: VisibilityScope, consolidation_id: uuid.UUID
|
||||||
|
) -> Consolidation | None:
|
||||||
|
"""Консолидация по id, если её ГК видна пользователю; чужая и несуществующая — `None` (v5.18, design D-3)."""
|
||||||
|
stmt = _visible_consolidations(scope).where(Consolidation.id == consolidation_id)
|
||||||
|
return (await session.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
async def get_consolidation_for_group(
|
async def get_consolidation_for_group(
|
||||||
session: AsyncSession, scope: VisibilityScope, group_id: uuid.UUID, year: int, quarter: PeriodQuarter
|
session: AsyncSession, scope: VisibilityScope, group_id: uuid.UUID, year: int, quarter: PeriodQuarter
|
||||||
) -> Consolidation | None:
|
) -> Consolidation | None:
|
||||||
|
|||||||
@ -12,12 +12,13 @@ import uuid
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import Select, and_, delete, func, or_, select, update
|
from sqlalchemy import Select, and_, delete, exists, func, or_, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.domain.policy import VisibilityScope, legal_entities_visible
|
from src.domain.policy import VisibilityScope, legal_entities_visible
|
||||||
from src.models.access import AppUser
|
from src.models.access import AppUser
|
||||||
from src.models.audit_ex import Comment
|
from src.models.audit_ex import Comment
|
||||||
|
from src.models.consolidation import ConsolidationDocument, ConsolidationVersion
|
||||||
from src.models.directories import LegalEntity
|
from src.models.directories import LegalEntity
|
||||||
from src.models.enums import FileStatus, S3BucketRole
|
from src.models.enums import FileStatus, S3BucketRole
|
||||||
from src.models.files import Document, RecognitionJob, S3Object, StoredFile
|
from src.models.files import Document, RecognitionJob, S3Object, StoredFile
|
||||||
@ -237,3 +238,43 @@ async def expire_s3_objects(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
await session.execute(update(S3Object).where(S3Object.id.in_(object_ids), owned).values(expires_at=now))
|
await session.execute(update(S3Object).where(S3Object.id.in_(object_ids), owned).values(expires_at=now))
|
||||||
|
|
||||||
|
|
||||||
|
async def list_desk_files( # noqa: PLR0913 - фильтр и пагинация списка передаются явными именованными аргументами
|
||||||
|
session: AsyncSession,
|
||||||
|
scope: VisibilityScope,
|
||||||
|
entity_id: uuid.UUID,
|
||||||
|
consolidation_id: uuid.UUID,
|
||||||
|
*,
|
||||||
|
include_superseded: bool,
|
||||||
|
limit: int,
|
||||||
|
offset: int,
|
||||||
|
) -> tuple[list[StoredFile], int]:
|
||||||
|
"""Рабочий стол консолидации для ЮЛ (spec documents-api «Рабочий стол консолидации для ЮЛ»; §9.5 п. 11; D-6):
|
||||||
|
файлы ЮЛ с `consolidation_id` этой консолидации ИЛИ с документом, привязанным к любой её версии. Порядок и
|
||||||
|
`include_superseded` — как в `list_files`; страница и общее число тем же фильтром."""
|
||||||
|
bound = exists().where(
|
||||||
|
Document.stored_file_id == StoredFile.id,
|
||||||
|
ConsolidationDocument.document_id == Document.id,
|
||||||
|
ConsolidationVersion.id == ConsolidationDocument.consolidation_version_id,
|
||||||
|
ConsolidationVersion.consolidation_id == consolidation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def filtered(stmt: Select[Any]) -> Select[Any]:
|
||||||
|
stmt = _visible_files(stmt, scope).where(
|
||||||
|
StoredFile.legal_entity_id == entity_id,
|
||||||
|
or_(StoredFile.consolidation_id == consolidation_id, bound),
|
||||||
|
)
|
||||||
|
if not include_superseded:
|
||||||
|
stmt = stmt.where(StoredFile.status != FileStatus.SUPERSEDED)
|
||||||
|
return stmt
|
||||||
|
|
||||||
|
total = (await session.execute(filtered(select(func.count(StoredFile.id))))).scalar_one()
|
||||||
|
page_stmt = (
|
||||||
|
filtered(select(StoredFile))
|
||||||
|
.order_by(StoredFile.uploaded_at.desc().nulls_last(), StoredFile.id)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
files = list((await session.execute(page_stmt)).scalars().all())
|
||||||
|
return files, total
|
||||||
|
|||||||
@ -93,5 +93,6 @@ class FileOut(BaseModel):
|
|||||||
uploaded_at: datetime.datetime | None
|
uploaded_at: datetime.datetime | None
|
||||||
uploaded_by: uuid.UUID | None
|
uploaded_by: uuid.UUID | None
|
||||||
superseded_by_file_id: uuid.UUID | None
|
superseded_by_file_id: uuid.UUID | None
|
||||||
|
consolidation_id: uuid.UUID | None # v5.18: консолидация, в контексте которой загружен файл; None — без контекста
|
||||||
job: JobBrief | None
|
job: JobBrief | None
|
||||||
documents: list[DocumentBrief]
|
documents: list[DocumentBrief]
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from src.models.consolidation import Consolidation, ConsolidationVersion
|
|||||||
from src.repositories import consolidations as repo
|
from src.repositories import consolidations as repo
|
||||||
|
|
||||||
VERSION_NOT_FOUND = "версия консолидации не найдена" # одинаково для чужой и несуществующей
|
VERSION_NOT_FOUND = "версия консолидации не найдена" # одинаково для чужой и несуществующей
|
||||||
|
CONSOLIDATION_NOT_FOUND = "консолидация не найдена" # одинаково для чужой и несуществующей
|
||||||
|
|
||||||
|
|
||||||
async def lock_visible_version(
|
async def lock_visible_version(
|
||||||
|
|||||||
@ -93,6 +93,7 @@ def file_out(
|
|||||||
uploaded_at=file.uploaded_at,
|
uploaded_at=file.uploaded_at,
|
||||||
uploaded_by=file.uploaded_by,
|
uploaded_by=file.uploaded_by,
|
||||||
superseded_by_file_id=file.superseded_by_file_id,
|
superseded_by_file_id=file.superseded_by_file_id,
|
||||||
|
consolidation_id=file.consolidation_id,
|
||||||
job=job_brief(job) if job is not None else None,
|
job=job_brief(job) if job is not None else None,
|
||||||
documents=[
|
documents=[
|
||||||
DocumentBrief.model_validate(document).model_copy(update={"comments_count": counts.get(document.id, 0)})
|
DocumentBrief.model_validate(document).model_copy(update={"comments_count": counts.get(document.id, 0)})
|
||||||
|
|||||||
@ -56,7 +56,7 @@ from src.schemas.common import PageParams
|
|||||||
from src.schemas.files import FileOut
|
from src.schemas.files import FileOut
|
||||||
from src.services import consolidation_documents
|
from src.services import consolidation_documents
|
||||||
from src.services.audit import AuditService
|
from src.services.audit import AuditService
|
||||||
from src.services.consolidation_guard import VERSION_NOT_FOUND
|
from src.services.consolidation_guard import CONSOLIDATION_NOT_FOUND, VERSION_NOT_FOUND
|
||||||
from src.services.documents import file_out
|
from src.services.documents import file_out
|
||||||
from src.services.legal_entities import LEGAL_ENTITY_NOT_FOUND
|
from src.services.legal_entities import LEGAL_ENTITY_NOT_FOUND
|
||||||
from src.services.result_mapping import RESULTS_PREFIX
|
from src.services.result_mapping import RESULTS_PREFIX
|
||||||
@ -241,6 +241,26 @@ class PrimaryFilesService:
|
|||||||
)
|
)
|
||||||
return await self._cards(files), total
|
return await self._cards(files), total
|
||||||
|
|
||||||
|
async def page_desk(
|
||||||
|
self, consolidation_id: uuid.UUID, entity_id: uuid.UUID, *, include_superseded: bool, page: PageParams
|
||||||
|
) -> tuple[list[FileOut], int]:
|
||||||
|
"""Рабочий стол консолидации для ЮЛ (spec documents-api «Рабочий стол консолидации для ЮЛ»; §9.5 п. 11; D-6):
|
||||||
|
консолидация вне видимости — 404 `CONSOLIDATION_NOT_FOUND`, ЮЛ вне видимости — 404; принадлежность ЮЛ к ГК
|
||||||
|
консолидации не проверяется (чужому ЮЛ — пустой стол). Карточки — как в `page_files`."""
|
||||||
|
if await consolidations_repo.get_consolidation(self._session, self._scope, consolidation_id) is None:
|
||||||
|
raise NotFoundError(CONSOLIDATION_NOT_FOUND)
|
||||||
|
entity = await self.get_entity(entity_id)
|
||||||
|
files, total = await files_repo.list_desk_files(
|
||||||
|
self._session,
|
||||||
|
self._scope,
|
||||||
|
entity.id,
|
||||||
|
consolidation_id,
|
||||||
|
include_superseded=include_superseded,
|
||||||
|
limit=page.limit,
|
||||||
|
offset=page.offset,
|
||||||
|
)
|
||||||
|
return await self._cards(files), total
|
||||||
|
|
||||||
async def get_file(self, file_id: uuid.UUID) -> FileOut:
|
async def get_file(self, file_id: uuid.UUID) -> FileOut:
|
||||||
"""Карточка видимого файла; чужой и несуществующий — одинаковый 404."""
|
"""Карточка видимого файла; чужой и несуществующий — одинаковый 404."""
|
||||||
file = await files_repo.get_file(self._session, self._scope, file_id)
|
file = await files_repo.get_file(self._session, self._scope, file_id)
|
||||||
|
|||||||
194
api/tests/test_api_consolidation_files.py
Normal file
194
api/tests/test_api_consolidation_files.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
"""Рабочий стол консолидации для ЮЛ и `consolidation_id` в карточке файла (spec documents-api «Рабочий стол консолидации
|
||||||
|
для ЮЛ», «Файлы организации и карточка файла»; design D-6, D-7). Поверх фикстуры `app`: данные — фабрики в транзакции
|
||||||
|
теста."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.models.access import AppUser
|
||||||
|
from src.models.consolidation import ConsolidationDocument
|
||||||
|
from src.models.directories import LegalEntity, RegionalOffice
|
||||||
|
from src.models.enums import DocumentStatus, FileStatus, PeriodQuarter, RecognitionStatus
|
||||||
|
from tests.conftest import AuthClientFactory
|
||||||
|
from tests.factories import make_company_group, make_consolidation, make_document, make_legal_entity, make_stored_file
|
||||||
|
|
||||||
|
HTTP_OK = 200
|
||||||
|
HTTP_NOT_FOUND = 404
|
||||||
|
HTTP_FORBIDDEN = 403
|
||||||
|
KA_RF = "ka_rf@armka.local" # Ростовский филиал, DOCUMENT_VIEW + DOCUMENT_EDIT
|
||||||
|
CLIENT_MANAGER = "client_manager@armka.local" # Краснодарский филиал
|
||||||
|
CURATOR_GO = "curator_go@armka.local" # головной офис, права DOCUMENT_VIEW нет
|
||||||
|
ROSTOV = "Ростовский региональный филиал"
|
||||||
|
KRASNODAR = "Краснодарский региональный филиал"
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
async def _group(session: AsyncSession, office_name: str = ROSTOV, name: str = "ГК Стол"):
|
||||||
|
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == office_name))).scalar_one()
|
||||||
|
group = await make_company_group(session, name=name, office=office)
|
||||||
|
entity = await make_legal_entity(session, name=f"ООО {name}", group=group)
|
||||||
|
initiator = (await session.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||||||
|
return group, entity, initiator
|
||||||
|
|
||||||
|
|
||||||
|
async def _consolidation( # noqa: PLR0913, PLR0917 - тестовый помощник
|
||||||
|
session: AsyncSession, group, entity, initiator, year: int, quarter: PeriodQuarter
|
||||||
|
):
|
||||||
|
consolidation, version = await make_consolidation(
|
||||||
|
session, group=group, year=year, quarter=quarter, members=[entity], initiator=initiator
|
||||||
|
)
|
||||||
|
return consolidation, version
|
||||||
|
|
||||||
|
|
||||||
|
async def _file(
|
||||||
|
session: AsyncSession, entity: LegalEntity, name: str, *, consolidation_id=None, status=FileStatus.PROCESSED
|
||||||
|
):
|
||||||
|
file, job = await make_stored_file(
|
||||||
|
session, entity=entity, status=status, job_status=RecognitionStatus.done, original_name=name
|
||||||
|
)
|
||||||
|
file.consolidation_id = consolidation_id
|
||||||
|
await session.flush()
|
||||||
|
return file, job
|
||||||
|
|
||||||
|
|
||||||
|
def _desk_url(consolidation_id: uuid.UUID, entity_id: uuid.UUID, **query: str) -> str:
|
||||||
|
base = f"/consolidations/{consolidation_id}/legal-entities/{entity_id}/files"
|
||||||
|
return base + ("?" + "&".join(f"{k}={v}" for k, v in query.items()) if query else "")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_shows_only_files_of_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
"""Сценарий «Стол показывает только файлы консоли»: три файла ЮЛ — 2025 Q1, 2025 Q2, без контекста."""
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
c2, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||||||
|
f1, _ = await _file(seeded, entity, "q1.xls", consolidation_id=c1.id)
|
||||||
|
f2, _ = await _file(seeded, entity, "q2.xls", consolidation_id=c2.id)
|
||||||
|
f0, _ = await _file(seeded, entity, "none.xls")
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
desk1 = await client.get(_desk_url(c1.id, entity.id))
|
||||||
|
desk2 = await client.get(_desk_url(c2.id, entity.id))
|
||||||
|
all_files = await client.get(f"/legal-entities/{entity.id}/files")
|
||||||
|
|
||||||
|
assert desk1.status_code == HTTP_OK, desk1.text
|
||||||
|
assert [item["id"] for item in desk1.json()["items"]] == [str(f1.id)]
|
||||||
|
assert desk1.json()["total"] == 1
|
||||||
|
assert desk1.json()["items"][0]["consolidation_id"] == str(c1.id)
|
||||||
|
assert [item["id"] for item in desk2.json()["items"]] == [str(f2.id)]
|
||||||
|
assert {item["id"] for item in all_files.json()["items"]} == {str(f1.id), str(f2.id), str(f0.id)}
|
||||||
|
by_id = {item["id"]: item["consolidation_id"] for item in all_files.json()["items"]}
|
||||||
|
assert by_id[str(f0.id)] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_includes_file_bound_to_version(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
"""Сценарий «Подхваченный файл виден через привязку»: файл без контекста, документ привязан к версии консоли."""
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||||||
|
file, job = await _file(seeded, entity, "q1-confirmed.xls")
|
||||||
|
document = await make_document(
|
||||||
|
seeded,
|
||||||
|
file=file,
|
||||||
|
job=job,
|
||||||
|
account="60",
|
||||||
|
year=2025,
|
||||||
|
quarter=PeriodQuarter.Q1,
|
||||||
|
status=DocumentStatus.CONFIRMED,
|
||||||
|
is_cumulative=False,
|
||||||
|
)
|
||||||
|
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
|
||||||
|
await seeded.flush()
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
response = await client.get(_desk_url(c2.id, entity.id))
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_OK, response.text
|
||||||
|
assert [item["id"] for item in response.json()["items"]] == [str(file.id)]
|
||||||
|
assert response.json()["items"][0]["consolidation_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_excludes_other_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
"""Review Focus 2: документ привязан к версии ДРУГОЙ консолидации — файл в этом столе не показывается."""
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
|
||||||
|
file, job = await _file(seeded, entity, "bound-elsewhere.xls", consolidation_id=c2.id)
|
||||||
|
document = await make_document(seeded, file=file, job=job, status=DocumentStatus.CONFIRMED)
|
||||||
|
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
|
||||||
|
await seeded.flush()
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
response = await client.get(_desk_url(c1.id, entity.id))
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_OK, response.text
|
||||||
|
assert response.json()["items"] == []
|
||||||
|
assert response.json()["total"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_superseded_hidden_by_default(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
"""Сценарий «Заменённый файл скрыт по умолчанию» для DESK и BOUND; `include_superseded=true` — оба."""
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c1, v1 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
new, _ = await _file(seeded, entity, "new.xls", consolidation_id=c1.id)
|
||||||
|
old_desk, _ = await _file(seeded, entity, "old-desk.xls", consolidation_id=c1.id, status=FileStatus.SUPERSEDED)
|
||||||
|
old_desk.superseded_by_file_id = new.id
|
||||||
|
old_bound, job = await _file(seeded, entity, "old-bound.xls", status=FileStatus.SUPERSEDED)
|
||||||
|
old_bound.superseded_by_file_id = new.id
|
||||||
|
document = await make_document(seeded, file=old_bound, job=job, status=DocumentStatus.CONFIRMED)
|
||||||
|
seeded.add(ConsolidationDocument(consolidation_version_id=v1.id, document_id=document.id))
|
||||||
|
await seeded.flush()
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
default = await client.get(_desk_url(c1.id, entity.id))
|
||||||
|
with_superseded = await client.get(_desk_url(c1.id, entity.id, include_superseded="true"))
|
||||||
|
|
||||||
|
assert [item["id"] for item in default.json()["items"]] == [str(new.id)]
|
||||||
|
assert {item["id"] for item in with_superseded.json()["items"]} == {
|
||||||
|
str(new.id),
|
||||||
|
str(old_desk.id),
|
||||||
|
str(old_bound.id),
|
||||||
|
}
|
||||||
|
assert with_superseded.json()["total"] == 3 # noqa: PLR2004
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_foreign_and_missing_consolidation_are_same_404(
|
||||||
|
seeded: AsyncSession, auth_client: AuthClientFactory
|
||||||
|
) -> None:
|
||||||
|
"""Сценарий «Чужая консолидация»: ГК другого филиала и несуществующий id — одинаковый 404; чужое ЮЛ — тоже 404."""
|
||||||
|
group, entity, initiator = await _group(seeded, office_name=KRASNODAR, name="ГК Чужая")
|
||||||
|
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
_, own_entity, _ = await _group(seeded, name="ГК Своя")
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
foreign = await client.get(_desk_url(c.id, own_entity.id))
|
||||||
|
missing = await client.get(_desk_url(uuid.uuid4(), own_entity.id))
|
||||||
|
|
||||||
|
assert foreign.status_code == missing.status_code == HTTP_NOT_FOUND
|
||||||
|
assert foreign.json() == missing.json()
|
||||||
|
assert foreign.json()["detail"]["code"] == "NOT_FOUND"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_desk_requires_document_view(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
client = await auth_client(CURATOR_GO)
|
||||||
|
|
||||||
|
response = await client.get(_desk_url(c.id, entity.id))
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_file_card_has_consolidation_id(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||||
|
"""Сценарий «Консолидация в карточке»: `GET /files/{id}` отдаёт `consolidation_id`."""
|
||||||
|
group, entity, initiator = await _group(seeded)
|
||||||
|
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
|
||||||
|
file, _ = await _file(seeded, entity, "card.xls", consolidation_id=c.id)
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
response = await client.get(f"/files/{file.id}")
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_OK, response.text
|
||||||
|
assert response.json()["consolidation_id"] == str(c.id)
|
||||||
Loading…
x
Reference in New Issue
Block a user