diff --git a/api/src/repositories/consolidations.py b/api/src/repositories/consolidations.py index ac7cdd9..ec4d1bb 100644 --- a/api/src/repositories/consolidations.py +++ b/api/src/repositories/consolidations.py @@ -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, diff --git a/api/src/repositories/files.py b/api/src/repositories/files.py index b839304..fe41b3d 100644 --- a/api/src/repositories/files.py +++ b/api/src/repositories/files.py @@ -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) diff --git a/api/src/services/consolidations.py b/api/src/services/consolidations.py index 8df89ef..1167262 100644 --- a/api/src/services/consolidations.py +++ b/api/src/services/consolidations.py @@ -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 diff --git a/api/tests/test_api_consolidation_checklist.py b/api/tests/test_api_consolidation_checklist.py index 93261b1..b822c81 100644 --- a/api/tests/test_api_consolidation_checklist.py +++ b/api/tests/test_api_consolidation_checklist.py @@ -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() diff --git a/api/tests/test_repositories_consolidations.py b/api/tests/test_repositories_consolidations.py index 0f0b8b9..e9fd037 100644 --- a/api/tests/test_repositories_consolidations.py +++ b/api/tests/test_repositories_consolidations.py @@ -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 diff --git a/openspec/specs/consolidations-api/spec.md b/openspec/specs/consolidations-api/spec.md index 201f922..989b5d1 100644 --- a/openspec/specs/consolidations-api/spec.md +++ b/openspec/specs/consolidations-api/spec.md @@ -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]` чек-листа той же