Merge pull request 'AURORA-1646 чек-лист версии — только файлы стола консолидации (#188)' (#189) from feat/188-checklist-scope into main
This commit is contained in:
commit
66fac5811e
@ -42,6 +42,7 @@ from src.models.enums import (
|
||||
from src.models.files import Document, StoredFile
|
||||
from src.models.osv import OsvEntry
|
||||
from src.repositories.directories import LIKE_ESCAPE, escape_like
|
||||
from src.repositories.files import on_desk
|
||||
|
||||
FILES_IN_PROGRESS = (FileStatus.UPLOADED, FileStatus.PROCESSING)
|
||||
GROUP_BINDINGS_LOCK_PREFIX = "consolidation-group" # пространство ключей advisory-блокировки привязок ГК (R-2)
|
||||
@ -339,24 +340,26 @@ async def history(session: AsyncSession, version_id: uuid.UUID) -> list[Consolid
|
||||
|
||||
|
||||
async def entity_files_state(
|
||||
session: AsyncSession, entity_ids: Sequence[uuid.UUID]
|
||||
session: AsyncSession, entity_ids: Sequence[uuid.UUID], consolidation_id: uuid.UUID
|
||||
) -> dict[uuid.UUID, EntityFilesState]:
|
||||
"""Состояние файлов каждого ЮЛ из `entity_ids` (чек-лист, D-13): файл в работе и статус последнего файла.
|
||||
"""Состояние файлов каждого ЮЛ из `entity_ids` на столе консолидации `consolidation_id` (чек-лист, D-13;
|
||||
`files.on_desk`, AURORA-1646): файл в работе и статус последнего файла.
|
||||
|
||||
«В работе» — любой файл `UPLOADED`/`PROCESSING`; «последний» — по `uploaded_at` (при равенстве или NULL — по `id`),
|
||||
включая замещённые файлы. ЮЛ без файлов — `EntityFilesState(False, None)`. Неподтверждённые документы-кандидаты
|
||||
сюда не входят (R-5).
|
||||
включая замещённые файлы. ЮЛ без файлов на столе — `EntityFilesState(False, None)`. Неподтверждённые
|
||||
документы-кандидаты сюда не входят (R-5).
|
||||
"""
|
||||
states = {entity_id: EntityFilesState(has_in_progress=False, last_file_status=None) for entity_id in entity_ids}
|
||||
if not states:
|
||||
return states
|
||||
desk = on_desk(consolidation_id)
|
||||
in_progress = select(StoredFile.legal_entity_id).where(
|
||||
StoredFile.legal_entity_id.in_(states), StoredFile.status.in_(FILES_IN_PROGRESS)
|
||||
StoredFile.legal_entity_id.in_(states), StoredFile.status.in_(FILES_IN_PROGRESS), desk
|
||||
)
|
||||
busy = set((await session.execute(in_progress.distinct())).scalars())
|
||||
last = (
|
||||
select(StoredFile.legal_entity_id, StoredFile.status)
|
||||
.where(StoredFile.legal_entity_id.in_(states))
|
||||
.where(StoredFile.legal_entity_id.in_(states), desk)
|
||||
.distinct(StoredFile.legal_entity_id)
|
||||
.order_by(StoredFile.legal_entity_id, StoredFile.uploaded_at.desc().nulls_last(), StoredFile.id.desc())
|
||||
)
|
||||
@ -399,24 +402,29 @@ async def file_consolidation_ids(
|
||||
async def unconfirmed_documents(
|
||||
session: AsyncSession,
|
||||
entity_ids: Collection[uuid.UUID],
|
||||
consolidation_id: uuid.UUID,
|
||||
year: int,
|
||||
consolidation_id: uuid.UUID | None = None,
|
||||
*,
|
||||
osv_any_year: bool = False,
|
||||
) -> list[Document]:
|
||||
"""Неподтверждённые документы ЮЛ за год (`RECOGNIZED`/`NEEDS_CHECK`) из файлов не `SUPERSEDED`; любых типов (R-5).
|
||||
`consolidation_id` (режим «период из версии», arm-core#103) — плюс ОСВ, загруженные в эту консолидацию, любого года.
|
||||
"""Неподтверждённые документы ЮЛ за год (`RECOGNIZED`/`NEEDS_CHECK`) из файлов не `SUPERSEDED` на столе консолидации
|
||||
`consolidation_id` (`files.on_desk`, AURORA-1646: документы других консолидаций и карточки ЮЛ не учитываются); любых
|
||||
типов (R-5). `osv_any_year` (режим «период из версии», arm-core#103) — плюс ОСВ, загруженные в эту консолидацию,
|
||||
любого года.
|
||||
|
||||
Кандидаты чек-листа: «подходящесть» версии (`fits`) проверяет сервис. Порядок — по ЮЛ, типу, счёту, кварталу, `id`.
|
||||
"""
|
||||
if not entity_ids:
|
||||
return []
|
||||
in_period: ColumnElement[bool] = Document.period_year == year
|
||||
if consolidation_id is not None:
|
||||
if osv_any_year:
|
||||
in_period = or_(in_period, _osv_of_consolidation(consolidation_id))
|
||||
stmt = (
|
||||
select(Document)
|
||||
.join(StoredFile, StoredFile.id == Document.stored_file_id)
|
||||
.where(
|
||||
Document.legal_entity_id.in_(list(entity_ids)),
|
||||
on_desk(consolidation_id),
|
||||
in_period,
|
||||
Document.status.in_(UNCONFIRMED_STATUSES),
|
||||
StoredFile.status != FileStatus.SUPERSEDED,
|
||||
|
||||
@ -12,8 +12,9 @@ import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Select, and_, delete, exists, func, or_, select, update
|
||||
from sqlalchemy import ColumnElement, Select, and_, delete, exists, func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from src.domain.policy import VisibilityScope, legal_entities_visible
|
||||
from src.models.access import AppUser
|
||||
@ -260,6 +261,20 @@ async def expire_s3_objects(
|
||||
await session.execute(update(S3Object).where(S3Object.id.in_(object_ids), owned).values(expires_at=now))
|
||||
|
||||
|
||||
def on_desk(consolidation_id: uuid.UUID) -> ColumnElement[bool]:
|
||||
"""Файл на столе консолидации (§9.5 п. 11): загружен в неё (`consolidation_id`) ИЛИ содержит документ, привязанный
|
||||
к любой её версии. Один фильтр для панели «Первичные документы» и чек-листа версии (AURORA-1646). Документ
|
||||
подзапроса — псевдоним: в запросе по `Document` иначе коррелирует с внешним и проверяет привязку его самого."""
|
||||
sibling = aliased(Document)
|
||||
bound = exists().where(
|
||||
sibling.stored_file_id == StoredFile.id,
|
||||
ConsolidationDocument.document_id == sibling.id,
|
||||
ConsolidationVersion.id == ConsolidationDocument.consolidation_version_id,
|
||||
ConsolidationVersion.consolidation_id == consolidation_id,
|
||||
)
|
||||
return or_(StoredFile.consolidation_id == consolidation_id, bound)
|
||||
|
||||
|
||||
async def list_desk_files( # noqa: PLR0913 - фильтр и пагинация списка передаются явными именованными аргументами
|
||||
session: AsyncSession,
|
||||
scope: VisibilityScope,
|
||||
@ -271,19 +286,13 @@ async def list_desk_files( # noqa: PLR0913 - фильтр и пагинация
|
||||
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,
|
||||
)
|
||||
файлы ЮЛ на столе консолидации (`on_desk`). Порядок и `include_superseded` — как в `list_files`; страница и общее
|
||||
число тем же фильтром."""
|
||||
|
||||
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),
|
||||
on_desk(consolidation_id),
|
||||
)
|
||||
if not include_superseded:
|
||||
stmt = stmt.where(StoredFile.status != FileStatus.SUPERSEDED)
|
||||
|
||||
@ -217,8 +217,9 @@ class ConsolidationService:
|
||||
self, version: ConsolidationVersion, consolidation: Consolidation, entities: list[LegalEntity]
|
||||
) -> tuple[list[checklist_domain.Rule], list[tuple[LegalEntity, list[checklist_domain.Cell]]]]:
|
||||
"""Правила подхода версии и ячейки по каждому ЮЛ `entities` (D-13): привязанные документы версии,
|
||||
неподтверждённые подходящие (`fits`) и состояние файлов ЮЛ. Кандидаты — `repo.unconfirmed_documents`,
|
||||
фильтр `fits` — здесь."""
|
||||
неподтверждённые подходящие (`fits`) и состояние файлов ЮЛ. Кандидаты и файлы — только со стола консолидации
|
||||
(§9.5 п. 11, AURORA-1646): документы других консолидаций на ячейки не влияют. Кандидаты —
|
||||
`repo.unconfirmed_documents`, фильтр `fits` — здесь."""
|
||||
rules = checklist_domain.order_rules(
|
||||
checklist_domain.Rule(rule.document_type, account, rule.is_mandatory)
|
||||
for rule, account in await repo.rules_for(self._session, version.approach)
|
||||
@ -227,7 +228,11 @@ class ConsolidationService:
|
||||
candidates = await consolidation_documents.for_consolidation(
|
||||
self._session,
|
||||
await repo.unconfirmed_documents(
|
||||
self._session, entity_ids, consolidation.year, self._period_from_version(consolidation)
|
||||
self._session,
|
||||
entity_ids,
|
||||
consolidation.id,
|
||||
consolidation.year,
|
||||
osv_any_year=consolidation_documents.period_from_version(),
|
||||
),
|
||||
consolidation,
|
||||
)
|
||||
@ -240,7 +245,7 @@ class ConsolidationService:
|
||||
self._session, await repo.bound_documents(self._session, version.id), consolidation
|
||||
),
|
||||
unbound=unbound,
|
||||
files_state=await repo.entity_files_state(self._session, entity_ids),
|
||||
files_state=await repo.entity_files_state(self._session, entity_ids, consolidation.id),
|
||||
)
|
||||
return rules, rows
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.models.access import AppUser
|
||||
from src.models.consolidation import ConsolidationDocument, ConsolidationVersion
|
||||
from src.models.directories import CompanyGroup, LegalEntity, RegionalOffice
|
||||
from src.models.enums import DocumentStatus, FileStatus, PeriodQuarter, RecognitionStatus
|
||||
from src.models.enums import DocumentStatus, DocumentType, FileStatus, PeriodQuarter, RecognitionStatus
|
||||
from tests.conftest import AuthClientFactory
|
||||
from tests.factories import (
|
||||
make_company_group,
|
||||
@ -109,10 +109,12 @@ async def test_checklist_gap_needs_check_and_processing(auth_client: AuthClientF
|
||||
file, job = await make_stored_file(
|
||||
seeded, entity=first, status=FileStatus.PROCESSED, job_status=RecognitionStatus.done
|
||||
)
|
||||
file.consolidation_id = version.consolidation_id
|
||||
await make_document(
|
||||
seeded, file=file, job=job, account="62", year=YEAR, quarter=Q3, status=DocumentStatus.NEEDS_CHECK
|
||||
)
|
||||
await make_stored_file(seeded, entity=second, status=FileStatus.PROCESSING)
|
||||
busy, _ = await make_stored_file(seeded, entity=second, status=FileStatus.PROCESSING)
|
||||
busy.consolidation_id = version.consolidation_id
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||
@ -129,6 +131,42 @@ async def test_checklist_gap_needs_check_and_processing(auth_client: AuthClientF
|
||||
assert {cell["status"] for cell in _cells(body, "ООО Б")} == {"PROCESSING"}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_checklist_ignores_files_outside_consolidation_desk(
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||
) -> None:
|
||||
"""AURORA-1646: неподтверждённые документы и файлы ЮЛ из другой консолидации и из карточки ЮЛ (без контекста) — не
|
||||
на столе консолидации (§9.5 п. 11), чек-лист их не учитывает: без документов — `MISSING`."""
|
||||
version, (first, second) = await _world(seeded)
|
||||
group = (await seeded.execute(select(CompanyGroup).where(CompanyGroup.name == "ГК Русский бисквит"))).scalar_one()
|
||||
user = (await seeded.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
|
||||
earlier, _ = await make_consolidation(
|
||||
seeded, group=group, year=YEAR, quarter=Q2, members=[first, second], initiator=user
|
||||
)
|
||||
file, job = await make_stored_file(
|
||||
seeded, entity=first, status=FileStatus.PROCESSED, job_status=RecognitionStatus.done
|
||||
)
|
||||
file.consolidation_id = earlier.id
|
||||
balance = await make_document(
|
||||
seeded, file=file, job=job, year=YEAR, quarter=Q3, status=DocumentStatus.NEEDS_CHECK, is_cumulative=True
|
||||
)
|
||||
balance.document_type, balance.osv_account_code = DocumentType.BALANCE, None
|
||||
await make_document(
|
||||
seeded, file=file, job=job, account="60", year=YEAR, quarter=Q3, status=DocumentStatus.RECOGNIZED
|
||||
)
|
||||
failed, _ = await make_stored_file(seeded, entity=second, status=FileStatus.ERROR)
|
||||
failed.consolidation_id = earlier.id
|
||||
await make_stored_file(seeded, entity=second, status=FileStatus.PROCESSING) # карточка ЮЛ, без контекста
|
||||
await seeded.flush()
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||
|
||||
for name in ("ООО А", "ООО Б"):
|
||||
assert {cell["status"] for cell in _cells(body, name)} == {"MISSING"}
|
||||
assert all(cell["document_ids"] == [] for cell in _cells(body, name))
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_card_members_have_same_cells_as_checklist(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
|
||||
version, (first, _) = await _bound_world(seeded)
|
||||
@ -188,8 +226,9 @@ async def test_checklist_visible_to_go_curator_hidden_from_foreign_office(
|
||||
assert (await admin.get(url)).status_code == HTTP_FORBIDDEN
|
||||
|
||||
|
||||
async def _unconfirmed( # noqa: PLR0913 - тестовая фабрика: ЮЛ, счёт, период, признак и статус документа
|
||||
async def _unconfirmed( # noqa: PLR0913 - тестовая фабрика: версия, ЮЛ, счёт, период, признак и статус документа
|
||||
session: AsyncSession,
|
||||
version: ConsolidationVersion,
|
||||
entity: LegalEntity,
|
||||
*,
|
||||
account: str,
|
||||
@ -201,6 +240,7 @@ async def _unconfirmed( # noqa: PLR0913 - тестовая фабрика: ЮЛ
|
||||
file, job = await make_stored_file(
|
||||
session, entity=entity, status=FileStatus.PROCESSED, job_status=RecognitionStatus.done
|
||||
)
|
||||
file.consolidation_id = version.consolidation_id # загружен в консолидацию версии — на её столе
|
||||
document = await make_document(
|
||||
session,
|
||||
file=file,
|
||||
@ -248,7 +288,9 @@ async def test_checklist_unconfirmed_candidate_must_fit_version( # noqa: PLR091
|
||||
) -> None:
|
||||
"""Неподтверждённый документ даёт `NEEDS_CHECK`/`RECOGNIZED` только если подходит версии (`fits`; I-1, T8.2)."""
|
||||
version, (first, _) = await _world(seeded)
|
||||
await _unconfirmed(seeded, first, account=account, year=year, quarter=quarter, cumulative=cumulative, status=status)
|
||||
await _unconfirmed(
|
||||
seeded, version, first, account=account, year=year, quarter=quarter, cumulative=cumulative, status=status
|
||||
)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||
@ -268,8 +310,8 @@ async def test_checklist_unconfirmed_of_entity_outside_perimeter_has_no_effect(
|
||||
version, _ = await _world(seeded)
|
||||
group = (await seeded.execute(select(CompanyGroup).where(CompanyGroup.name == "ГК Русский бисквит"))).scalar_one()
|
||||
outsider = await make_legal_entity(seeded, name="ООО В вне периметра", group=group)
|
||||
await _unconfirmed(seeded, outsider, account="62")
|
||||
await _unconfirmed(seeded, outsider, account="60", status=DocumentStatus.RECOGNIZED)
|
||||
await _unconfirmed(seeded, version, outsider, account="62")
|
||||
await _unconfirmed(seeded, version, outsider, account="60", status=DocumentStatus.RECOGNIZED)
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||
|
||||
@ -26,6 +26,7 @@ from src.models.enums import (
|
||||
PeriodQuarter,
|
||||
RecognitionStatus,
|
||||
)
|
||||
from src.models.files import RecognitionJob, StoredFile
|
||||
from src.repositories import consolidations as repo
|
||||
from tests.factories import (
|
||||
make_company_group,
|
||||
@ -377,30 +378,58 @@ async def test_bound_documents_and_document_versions(db_session: AsyncSession) -
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_entity_files_state_in_progress_and_last_status(db_session: AsyncSession) -> None:
|
||||
group = await make_company_group(db_session)
|
||||
busy = await make_legal_entity(db_session, group=group)
|
||||
failed = await make_legal_entity(db_session, group=group)
|
||||
idle = await make_legal_entity(db_session, group=group)
|
||||
recovered = await make_legal_entity(db_session, group=group)
|
||||
await make_stored_file(db_session, entity=busy, status=FileStatus.UPLOADED)
|
||||
await make_stored_file(db_session, entity=busy, status=FileStatus.PROCESSING)
|
||||
old, _ = await make_stored_file(db_session, entity=failed, status=FileStatus.PROCESSED)
|
||||
newer, _ = await make_stored_file(db_session, entity=failed, status=FileStatus.ERROR)
|
||||
old.uploaded_at = datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC)
|
||||
newer.uploaded_at = datetime.datetime(2024, 2, 1, tzinfo=datetime.UTC)
|
||||
bad, _ = await make_stored_file(db_session, entity=recovered, status=FileStatus.ERROR)
|
||||
good, _ = await make_stored_file(db_session, entity=recovered, status=FileStatus.PROCESSED)
|
||||
bad.uploaded_at = datetime.datetime(2024, 1, 1, tzinfo=datetime.UTC)
|
||||
good.uploaded_at = datetime.datetime(2024, 2, 1, tzinfo=datetime.UTC)
|
||||
_, group, (busy, failed, idle, recovered), user = await _world(db_session, members=4)
|
||||
consolidation, _ = await make_consolidation(
|
||||
db_session, group=group, year=2024, quarter=PeriodQuarter.Q3, members=[busy, failed], initiator=user
|
||||
)
|
||||
|
||||
async def file(owner: LegalEntity, status: FileStatus, uploaded: int | None = None) -> None:
|
||||
stored, _ = await make_stored_file(db_session, entity=owner, status=status)
|
||||
stored.consolidation_id = consolidation.id
|
||||
if uploaded is not None:
|
||||
stored.uploaded_at = datetime.datetime(2024, uploaded, 1, tzinfo=datetime.UTC)
|
||||
|
||||
await file(busy, FileStatus.UPLOADED)
|
||||
await file(busy, FileStatus.PROCESSING)
|
||||
await file(failed, FileStatus.PROCESSED, 1)
|
||||
await file(failed, FileStatus.ERROR, 2)
|
||||
await file(recovered, FileStatus.ERROR, 1)
|
||||
await file(recovered, FileStatus.PROCESSED, 2)
|
||||
await db_session.flush()
|
||||
|
||||
state = await repo.entity_files_state(db_session, [busy.id, failed.id, idle.id, recovered.id])
|
||||
state = await repo.entity_files_state(db_session, [busy.id, failed.id, idle.id, recovered.id], consolidation.id)
|
||||
|
||||
assert state[busy.id].has_in_progress is True
|
||||
assert state[failed.id] == EntityFilesState(has_in_progress=False, last_file_status=FileStatus.ERROR)
|
||||
assert state[idle.id] == EntityFilesState(has_in_progress=False, last_file_status=None)
|
||||
assert state[recovered.id] == EntityFilesState(has_in_progress=False, last_file_status=FileStatus.PROCESSED)
|
||||
assert await repo.entity_files_state(db_session, []) == {}
|
||||
assert await repo.entity_files_state(db_session, [], consolidation.id) == {}
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_entity_files_state_only_desk_files(db_session: AsyncSession) -> None:
|
||||
"""AURORA-1646: файлы другой консолидации и загруженные без контекста состояние ЮЛ не задают; файл с документом,
|
||||
привязанным к версии консолидации, — задаёт (стол консолидации, §9.5 п. 11)."""
|
||||
_, group, (entity, bound_owner), user = await _world(db_session)
|
||||
earlier, _ = await make_consolidation(
|
||||
db_session, group=group, year=2026, quarter=PeriodQuarter.Q1, members=[entity, bound_owner], initiator=user
|
||||
)
|
||||
consolidation, version = await make_consolidation(
|
||||
db_session, group=group, year=2026, quarter=PeriodQuarter.Q2, members=[entity, bound_owner], initiator=user
|
||||
)
|
||||
foreign, _ = await make_stored_file(db_session, entity=entity, status=FileStatus.ERROR)
|
||||
foreign.consolidation_id = earlier.id
|
||||
await make_stored_file(db_session, entity=entity, status=FileStatus.PROCESSING) # карточка ЮЛ, без контекста
|
||||
picked = await make_confirmed_osv(
|
||||
db_session, entity=bound_owner, account="60", year=2026, quarter=PeriodQuarter.Q1, is_cumulative=False, rows=[]
|
||||
)
|
||||
db_session.add(ConsolidationDocument(consolidation_version_id=version.id, document_id=picked.id))
|
||||
await db_session.flush()
|
||||
|
||||
state = await repo.entity_files_state(db_session, [entity.id, bound_owner.id], consolidation.id)
|
||||
|
||||
assert state[entity.id] == EntityFilesState(has_in_progress=False, last_file_status=None)
|
||||
assert state[bound_owner.id] == EntityFilesState(has_in_progress=False, last_file_status=FileStatus.PROCESSED)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@ -420,9 +449,10 @@ async def test_rules_for_basic_and_extended_from_seed(seeded: AsyncSession) -> N
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_unconfirmed_documents_filters_status_year_entity_and_superseded(db_session: AsyncSession) -> None:
|
||||
group = await make_company_group(db_session)
|
||||
entity = await make_legal_entity(db_session, group=group)
|
||||
other = await make_legal_entity(db_session, group=group)
|
||||
_, group, (entity, other), user = await _world(db_session)
|
||||
consolidation, _ = await make_consolidation(
|
||||
db_session, group=group, year=2024, quarter=PeriodQuarter.Q3, members=[entity, other], initiator=user
|
||||
)
|
||||
|
||||
async def doc(
|
||||
owner: LegalEntity,
|
||||
@ -435,6 +465,7 @@ async def test_unconfirmed_documents_filters_status_year_entity_and_superseded(d
|
||||
file, job = await make_stored_file(
|
||||
db_session, entity=owner, status=file_status, job_status=RecognitionStatus.done
|
||||
)
|
||||
file.consolidation_id = consolidation.id
|
||||
document = await make_document(
|
||||
db_session, file=file, job=job, account=account, year=year, quarter=PeriodQuarter.Q3, status=status
|
||||
)
|
||||
@ -448,10 +479,47 @@ async def test_unconfirmed_documents_filters_status_year_entity_and_superseded(d
|
||||
await doc(entity, status=DocumentStatus.NEEDS_CHECK, file_status=FileStatus.SUPERSEDED)
|
||||
await doc(other, status=DocumentStatus.NEEDS_CHECK)
|
||||
|
||||
found = await repo.unconfirmed_documents(db_session, [entity.id], 2024)
|
||||
found = await repo.unconfirmed_documents(db_session, [entity.id], consolidation.id, 2024)
|
||||
|
||||
assert sorted(d.id for d in found) == sorted([needs_check, recognized])
|
||||
assert await repo.unconfirmed_documents(db_session, [], 2024) == []
|
||||
assert await repo.unconfirmed_documents(db_session, [], consolidation.id, 2024) == []
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_unconfirmed_documents_only_desk_files(db_session: AsyncSession) -> None:
|
||||
"""AURORA-1646: кандидаты чек-листа — только из файлов стола консолидации (§9.5 п. 11): загруженных в неё или с
|
||||
документом, привязанным к её версии; файлы другой консолидации и карточки ЮЛ (без контекста) — не кандидаты."""
|
||||
_, group, (entity,), user = await _world(db_session, members=1)
|
||||
earlier, _ = await make_consolidation(
|
||||
db_session, group=group, year=2026, quarter=PeriodQuarter.Q1, members=[entity], initiator=user
|
||||
)
|
||||
consolidation, version = await make_consolidation(
|
||||
db_session, group=group, year=2026, quarter=PeriodQuarter.Q2, members=[entity], initiator=user
|
||||
)
|
||||
|
||||
async def doc(consolidation_id: uuid.UUID | None, account: str) -> tuple[StoredFile, RecognitionJob, uuid.UUID]:
|
||||
file, job = await make_stored_file(
|
||||
db_session, entity=entity, status=FileStatus.PROCESSED, job_status=RecognitionStatus.done
|
||||
)
|
||||
file.consolidation_id = consolidation_id
|
||||
document = await make_document(
|
||||
db_session, file=file, job=job, account=account, year=2026, status=DocumentStatus.NEEDS_CHECK
|
||||
)
|
||||
return file, job, document.id
|
||||
|
||||
_, _, own = await doc(consolidation.id, "60")
|
||||
await doc(earlier.id, "62")
|
||||
picked_file, picked_job, picked = await doc(None, "58")
|
||||
confirmed = await make_document(
|
||||
db_session, file=picked_file, job=picked_job, account="01", year=2026, status=DocumentStatus.CONFIRMED
|
||||
)
|
||||
db_session.add(ConsolidationDocument(consolidation_version_id=version.id, document_id=confirmed.id))
|
||||
await doc(None, "76")
|
||||
await db_session.flush()
|
||||
|
||||
found = await repo.unconfirmed_documents(db_session, [entity.id], consolidation.id, 2026)
|
||||
|
||||
assert sorted(d.id for d in found) == sorted([own, picked])
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@ -187,7 +187,10 @@ account, status, missing_quarters, document_ids}`: `document_type` и `account`
|
||||
документами (ОСВ: по правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии);
|
||||
`NEEDS_CHECK` / `RECOGNIZED` — есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии
|
||||
(приоритет `NEEDS_CHECK`); `PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по
|
||||
`uploaded_at` файл ЮЛ в `ERROR`; иначе `MISSING`. `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при
|
||||
`uploaded_at` файл ЮЛ в `ERROR`; иначе `MISSING`. Неподтверждённые документы и файлы SHALL браться только со стола
|
||||
консолидации версии (§9.5 п. 11: файлы, загруженные в эту консолидацию, и файлы с документом, привязанным к любой её
|
||||
версии) — тем же фильтром, что панель «Первичные документы»; документы и файлы ЮЛ из других консолидаций и из карточки
|
||||
ЮЛ на ячейки MUST NOT влиять (AURORA-1646). `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при
|
||||
частичном покрытии. Правило ОСВ SHALL закрываться только документом с `osv_account_code`, равным `account` правила
|
||||
(OQ-04). Те же ячейки (с `document_type` и `account`, в том же порядке) SHALL отдаваться в карточке версии у каждого
|
||||
члена. С `?format=csv` SHALL отдаваться тот же чек-лист файлом `text/csv; charset=utf-8` с BOM и разделителем `;`:
|
||||
@ -206,9 +209,20 @@ account, status, missing_quarters, document_ids}`: `document_type` и `account`
|
||||
- **THEN** ячейка ОСВ 58 — `MISSING`
|
||||
|
||||
#### Scenario: Файл в обработке
|
||||
- **WHEN** у ЮЛ загружен файл, задание ещё не завершено, других документов нет
|
||||
- **WHEN** у ЮЛ в консолидацию загружен файл, задание ещё не завершено, других документов нет
|
||||
- **THEN** все ячейки ЮЛ — `PROCESSING`
|
||||
|
||||
#### Scenario: Документы другой консолидации
|
||||
- **WHEN** создана консолидация 2024 Q3; у ЮЛ «А» в файле, загруженном в консолидацию 2024 Q2, — ББ нарастающий за Q3
|
||||
(`NEEDS_CHECK`) и ОСВ 60 за Q3 (`RECOGNIZED`); у ЮЛ «Б» — файл консолидации 2024 Q2 в `ERROR` и файл в `PROCESSING`,
|
||||
загруженный из карточки ЮЛ; в консолидацию 2024 Q3 ничего не загружено
|
||||
- **THEN** все ячейки обоих ЮЛ — `MISSING` с пустыми `document_ids`
|
||||
|
||||
#### Scenario: Файл, подхваченный привязкой
|
||||
- **WHEN** файл ЮЛ загружен из карточки ЮЛ, один его документ подтверждён и привязан к версии консолидации, другой —
|
||||
`NEEDS_CHECK` и подходит версии
|
||||
- **THEN** файл на столе консолидации, ячейка второго документа — `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Ячейки карточки подписаны
|
||||
- **WHEN** запрошена карточка версии подхода `BASIC`
|
||||
- **THEN** у каждого члена `checklist[i].document_type` и `checklist[i].account` равны `rules[i]` чек-листа той же
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user