AURORA-1646 чек-лист версии — только файлы стола консолидации (#188) #189

Merged
aep merged 1 commits from feat/188-checklist-scope into main 2026-10-27 09:58:43 +03:00
6 changed files with 200 additions and 54 deletions

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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]` чек-листа той же