feat(consolidations): отбор документов чек-листа параметром documents (AURORA-1678)

GET /consolidations/versions/{id}/checklist принимает documents
(BALANCE, INCOME, OSV:<счёт> через запятую) и отдаёт только эти правила
и их ячейки — в JSON и CSV; неверный список — 422 INVALID_DOCUMENTS.
Фронт PR #227 уже передаёт параметр при скачивании CSV из окна чек-листа.

OpenSpec change checklist-documents-filter: дельта consolidations-api
(«Чек-лист комплекта») и consolidations-ui (окно чек-листа, фильтр
«Документы»); README — параметр и код ошибки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-10 16:42:00 +04:00
parent 28329c5b2d
commit c3a8deb9f5
11 changed files with 362 additions and 2 deletions

View File

@ -785,7 +785,7 @@ advisory-блокировкой.
| `DELETE /consolidations/versions/{id}/lock` | EDIT | снять свою блокировку (204; нет активной — тоже 204) | | `DELETE /consolidations/versions/{id}/lock` | EDIT | снять свою блокировку (204; нет активной — тоже 204) |
| `POST /consolidations/versions/{id}/status` | EDIT | ручной переход: `CANCELLED` (с комментарием) или возврат в `INITIATED` | | `POST /consolidations/versions/{id}/status` | EDIT | ручной переход: `CANCELLED` (с комментарием) или возврат в `INITIATED` |
| `PUT /consolidations/versions/{id}/members` | EDIT | заменить периметр (ЮЛ должны принадлежать ГК, ≥ 2) | | `PUT /consolidations/versions/{id}/members` | EDIT | заменить периметр (ЮЛ должны принадлежать ГК, ≥ 2) |
| `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки; `?format=csv` — тот же чек-лист файлом CSV | | `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки; `?format=csv` — тот же чек-лист файлом CSV; `?documents=BALANCE,INCOME,OSV:01` — только эти столбцы |
| `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры, в т. ч. `counterparty_id` — строки с этим контрагентом (`limit` ≤ 2000, по умолчанию 500) | | `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры, в т. ч. `counterparty_id` — строки с этим контрагентом (`limit` ≤ 2000, по умолчанию 500) |
| `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]`, `counter_pairs[]` (пары «участник не видит встречных записей») | | `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]`, `counter_pairs[]` (пары «участник не видит встречных записей») |
| `GET /consolidations/versions/{id}/osv/export` | VIEW | xlsx: лист «Сводная ОСВ» и лист «Пробелы» | | `GET /consolidations/versions/{id}/osv/export` | VIEW | xlsx: лист «Сводная ОСВ» и лист «Пробелы» |
@ -860,6 +860,11 @@ Q2–Q4 не подходят (R-14a). Привязываются только `
в `Content-Disposition` по RFC 5987. Формат по умолчанию — `json`; другое значение `format` — 422 валидации. Скачивание в аудит не пишется в `Content-Disposition` по RFC 5987. Формат по умолчанию — `json`; другое значение `format` — 422 валидации. Скачивание в аудит не пишется
(`domain/checklist_csv.py`, чистая функция). (`domain/checklist_csv.py`, чистая функция).
`documents` (JSON и CSV, AURORA-1678) — отбор столбцов окна чек-листа: `BALANCE`, `INCOME`, `OSV:<счёт>` через запятую; остаются
названные правила в порядке `rules` и их ячейки, ЮЛ — все; документ вне правил подхода столбца не добавляет. Пустой список, пустой
токен, неизвестный документ, `OSV` без счёта — 422 `INVALID_DOCUMENTS` (`detail.token`), длиннее 500 символов — 422 валидации
(`domain/checklist_filter.py`).
### Сводная ОСВ ### Сводная ОСВ
Чистая функция от версии (`domain/consolidated_osv.build` + `domain/consolidation_periods.select_sources`), в БД не пишется Чистая функция от версии (`domain/consolidated_osv.build` + `domain/consolidation_periods.select_sources`), в БД не пишется
@ -1024,6 +1029,7 @@ email без учёта регистра проверяет только сер
| `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом | | `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом |
| `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр; в правке формы (`PATCH …/statement`) — не из 10 или 12 цифр | | `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр; в правке формы (`PATCH …/statement`) — не из 10 или 12 цифр |
| `INVALID_KPP` | 422 | КПП не из 9 символов формата `NNNN[0-9A-Z]{2}NNN` или КПП у ИП (DBML v5.17, OQ-31) | | `INVALID_KPP` | 422 | КПП не из 9 символов формата `NNNN[0-9A-Z]{2}NNN` или КПП у ИП (DBML v5.17, OQ-31) |
| `INVALID_DOCUMENTS` | 422 | в `documents` чек-листа (`GET …/checklist`) пустой токен, неизвестный документ или `OSV` без счёта (`detail.token`) |
| `EFFECTIVE_DATE_IN_PAST` | 422 | дата перевода/вывода раньше сегодняшней (по Москве) | | `EFFECTIVE_DATE_IN_PAST` | 422 | дата перевода/вывода раньше сегодняшней (по Москве) |
| `EMPTY_ALIAS` | 422 | алиас пуст после нормализации (одни ОПФ и знаки препинания) | | `EMPTY_ALIAS` | 422 | алиас пуст после нормализации (одни ОПФ и знаки препинания) |
| `DIFFERENT_COMPANY_GROUP` | 422 | ЮЛ не входит в ГК этой ГСК/ГСЗ | | `DIFFERENT_COMPANY_GROUP` | 422 | ЮЛ не входит в ГК этой ГСК/ГСЗ |

View File

@ -2,7 +2,7 @@
«Чек-лист комплекта»; design D-13). Право проверяется до видимости; версия ГК вне видимости — тот же 404.""" «Чек-лист комплекта»; design D-13). Право проверяется до видимости; версия ГК вне видимости — тот же 404."""
import uuid import uuid
from typing import Any, Literal from typing import Annotated, Any, Literal
from fastapi import APIRouter, Depends, Query, Response, status from fastapi import APIRouter, Depends, Query, Response, status
@ -10,6 +10,7 @@ from src.api.dependencies.auth import require
from src.api.dependencies.consolidations import ConsolidationServiceDep from src.api.dependencies.consolidations import ConsolidationServiceDep
from src.common.filenames import content_disposition from src.common.filenames import content_disposition
from src.domain.checklist_csv import checklist_filename, render_checklist_csv from src.domain.checklist_csv import checklist_filename, render_checklist_csv
from src.domain.checklist_filter import parse_documents, select_documents
from src.schemas.common import ErrorDetail from src.schemas.common import ErrorDetail
from src.schemas.consolidations import ChecklistOut from src.schemas.consolidations import ChecklistOut
@ -17,12 +18,17 @@ router = APIRouter(prefix="/consolidations/versions")
VIEW = [Depends(require("CONSOLIDATION_VIEW"))] VIEW = [Depends(require("CONSOLIDATION_VIEW"))]
CSV_MEDIA_TYPE = "text/csv" CSV_MEDIA_TYPE = "text/csv"
MAX_DOCUMENTS_LENGTH = 500
RESPONSES: dict[int | str, dict[str, Any]] = { RESPONSES: dict[int | str, dict[str, Any]] = {
status.HTTP_200_OK: { status.HTTP_200_OK: {
"description": "чек-лист: JSON или (при `format=csv`) файл CSV — UTF-8 с BOM, разделитель `;`, CRLF", "description": "чек-лист: JSON или (при `format=csv`) файл CSV — UTF-8 с BOM, разделитель `;`, CRLF",
"content": {CSV_MEDIA_TYPE: {"schema": {"type": "string", "format": "binary"}}}, "content": {CSV_MEDIA_TYPE: {"schema": {"type": "string", "format": "binary"}}},
}, },
status.HTTP_404_NOT_FOUND: {"model": ErrorDetail, "description": "нет или вне видимости"}, status.HTTP_404_NOT_FOUND: {"model": ErrorDetail, "description": "нет или вне видимости"},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"model": ErrorDetail,
"description": "`INVALID_DOCUMENTS` — в `documents` неизвестный документ, пустой токен или `OSV` без счёта",
},
} }
@ -37,12 +43,18 @@ async def get_checklist(
version_id: uuid.UUID, version_id: uuid.UUID,
service: ConsolidationServiceDep, service: ConsolidationServiceDep,
format: Literal["json", "csv"] = Query("json"), format: Literal["json", "csv"] = Query("json"),
documents: Annotated[str | None, Query(max_length=MAX_DOCUMENTS_LENGTH)] = None,
) -> ChecklistOut | Response: ) -> ChecklistOut | Response:
"""Правила комплекта подхода версии и строки по ЮЛ периметра: ячейки `{status, missing_quarters, document_ids}`. """Правила комплекта подхода версии и строки по ЮЛ периметра: ячейки `{status, missing_quarters, document_ids}`.
При `?format=csv` возвращает чек-лист файлом CSV (UTF-8 с BOM, разделитель `;`, CRLF). При `?format=csv` возвращает чек-лист файлом CSV (UTF-8 с BOM, разделитель `;`, CRLF).
`documents` (`BALANCE,INCOME,OSV:01`) — только эти правила и их ячейки, в порядке правил; не задан — все
(AURORA-1678).
""" """
keys = parse_documents(documents) if documents is not None else None
checklist = await service.checklist(version_id) checklist = await service.checklist(version_id)
if keys is not None:
checklist = select_documents(checklist, keys)
if format == "csv": if format == "csv":
group_name, year, quarter = await service.version_title(version_id) group_name, year, quarter = await service.version_title(version_id)

View File

@ -0,0 +1,48 @@
"""Отбор документов чек-листа параметром `documents` (spec consolidations-api «Чек-лист комплекта»; AURORA-1678).
`documents` — список через запятую: `BALANCE` (ББ), `INCOME` (ОФР), `OSV:<счёт>` (ОСВ по счёту правила). Отбор
оставляет правила комплекта, названные в списке, в их порядке, и ячейки этих правил у каждого ЮЛ; ЮЛ периметра не
отбираются. Документ, которого нет в правилах подхода версии, столбца не добавляет.
"""
from typing import TypeAlias
from src.common.exceptions import DomainValidationError
from src.models.enums import DocumentType
from src.schemas.consolidations import ChecklistOut
INVALID_DOCUMENTS = "INVALID_DOCUMENTS"
OSV_PREFIX = "OSV:"
DocumentKey: TypeAlias = tuple[DocumentType, str | None]
_PLAIN: dict[str, DocumentType] = {"BALANCE": DocumentType.BALANCE, "INCOME": DocumentType.INCOME}
def _parse_token(token: str) -> DocumentKey:
if token in _PLAIN:
return _PLAIN[token], None
if token.startswith(OSV_PREFIX) and (account := token.removeprefix(OSV_PREFIX).strip()):
return DocumentType.OSV, account
raise DomainValidationError(
INVALID_DOCUMENTS,
f"неизвестный документ «{token}»: допустимы BALANCE, INCOME, OSV:<счёт>",
token=token,
)
def parse_documents(raw: str) -> set[DocumentKey]:
"""`BALANCE,INCOME,OSV:01` → ключи правил `(document_type, account)`; пробелы вокруг токенов отбрасываются.
Пустой список, пустой токен, неизвестный тип и `OSV` без счёта — 422 `INVALID_DOCUMENTS`.
"""
return {_parse_token(token.strip()) for token in raw.split(",")}
def select_documents(checklist: ChecklistOut, keys: set[DocumentKey]) -> ChecklistOut:
"""Чек-лист только с правилами из `keys` (порядок правил сохраняется) и их ячейками; исходный не меняется."""
kept = [i for i, rule in enumerate(checklist.rules) if (rule.document_type, rule.account) in keys]
return ChecklistOut(
rules=[checklist.rules[i] for i in kept],
rows=[row.model_copy(update={"cells": [row.cells[i] for i in kept]}) for row in checklist.rows],
)

View File

@ -394,3 +394,61 @@ async def test_checklist_csv_of_invisible_version_is_404(auth_client: AuthClient
response = await foreign.get(f"/consolidations/versions/{version.id}/checklist", params={"format": fmt}) response = await foreign.get(f"/consolidations/versions/{version.id}/checklist", params={"format": fmt})
assert response.status_code == HTTP_NOT_FOUND, fmt assert response.status_code == HTTP_NOT_FOUND, fmt
assert "content-disposition" not in response.headers assert "content-disposition" not in response.headers
@pytest.mark.integration
async def test_checklist_csv_with_selected_documents(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
"""`documents` оставляет в CSV только выбранные столбцы в порядке правил; ЮЛ — все (AURORA-1678)."""
version, (first, second) = await _bound_world(seeded)
client = await auth_client(KA_RF)
response = await client.get(
f"/consolidations/versions/{version.id}/checklist",
params={"format": "csv", "documents": "OSV:60,BALANCE,OSV:99"},
)
assert response.status_code == HTTP_OK, response.text
lines = response.content.decode("utf-8-sig").splitlines()
assert lines == [
"Организация;ИНН;Бух. баланс;ОСВ 60",
f"{first.name};{first.inn or ''};Отсутствует;Загружен",
f"{second.name};{second.inn or ''};Отсутствует;Отсутствует",
]
@pytest.mark.integration
async def test_checklist_json_with_selected_documents(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
"""Тот же отбор в JSON: `rules` и ячейки строк — только выбранные документы."""
version, (first, _) = await _bound_world(seeded)
client = await auth_client(KA_RF)
response = await client.get(
f"/consolidations/versions/{version.id}/checklist", params={"documents": "INCOME,OSV:60"}
)
assert response.status_code == HTTP_OK, response.text
body = response.json()
assert [(rule["document_type"], rule["account"]) for rule in body["rules"]] == [("INCOME", None), ("OSV", "60")]
assert [(c["document_type"], c["account"], c["status"]) for c in _cells(body, first.name)] == [
("INCOME", None, "MISSING"),
("OSV", "60", "CONFIRMED"),
]
@pytest.mark.integration
@pytest.mark.parametrize("documents", ["", "OFR", "OSV:", "BALANCE,,INCOME", "x" * 501])
async def test_checklist_invalid_documents_is_422(
auth_client: AuthClientFactory, seeded: AsyncSession, documents: str
) -> None:
"""Пустой список, неизвестный документ, `OSV` без счёта, пустой токен и длиннее 500 символов — 422, без файла."""
version, _ = await _world(seeded)
client = await auth_client(KA_RF)
response = await client.get(
f"/consolidations/versions/{version.id}/checklist", params={"format": "csv", "documents": documents}
)
assert response.status_code == HTTP_UNPROCESSABLE, response.text
assert "content-disposition" not in response.headers
if len(documents) <= 500: # noqa: PLR2004 - граница длины
assert response.json()["detail"]["code"] == "INVALID_DOCUMENTS"

View File

@ -0,0 +1,77 @@
"""Отбор документов чек-листа параметром `documents` (spec consolidations-api «Чек-лист комплекта»; AURORA-1678)."""
import uuid
import pytest
from src.common.exceptions import DomainValidationError
from src.domain.checklist import CellStatus
from src.domain.checklist_filter import INVALID_DOCUMENTS, parse_documents, select_documents
from src.models.enums import DocumentType
from src.schemas.consolidations import ChecklistCellOut, ChecklistOut, ChecklistRowOut, ChecklistRuleOut
BALANCE, INCOME, OSV = DocumentType.BALANCE, DocumentType.INCOME, DocumentType.OSV
RULES = [(BALANCE, None), (INCOME, None), (OSV, "01"), (OSV, "60")]
def _checklist() -> ChecklistOut:
"""Два ЮЛ, правила ББ, ОФР, ОСВ 01, ОСВ 60; статус ячейки различает столбцы."""
statuses = [CellStatus.CONFIRMED, CellStatus.PROCESSING, CellStatus.ERROR, CellStatus.MISSING]
return ChecklistOut(
rules=[ChecklistRuleOut(document_type=t, account=a, is_mandatory=True) for t, a in RULES],
rows=[
ChecklistRowOut(
legal_entity_id=uuid.uuid4(),
legal_entity_name=name,
legal_entity_inn="7700000000",
cells=[
ChecklistCellOut(document_type=t, account=a, status=s, missing_quarters=[], document_ids=[])
for (t, a), s in zip(RULES, statuses, strict=True)
],
)
for name in ("ООО А", "ООО Б")
],
)
def test_parse_plain_and_osv_tokens() -> None:
assert parse_documents("BALANCE,INCOME,OSV:01") == {(BALANCE, None), (INCOME, None), (OSV, "01")}
def test_parse_strips_spaces_and_ignores_duplicates() -> None:
assert parse_documents(" OSV:60 , OSV:60,BALANCE ") == {(OSV, "60"), (BALANCE, None)}
@pytest.mark.parametrize("raw", ["", "BALANCE,", "OFR", "balance", "OSV", "OSV:", "OSV: ", "DISCLOSURE"])
def test_parse_rejects_unknown_tokens(raw: str) -> None:
with pytest.raises(DomainValidationError) as error:
parse_documents(raw)
assert error.value.code == INVALID_DOCUMENTS
def test_select_keeps_rules_order_and_aligns_cells() -> None:
selected = select_documents(_checklist(), {(OSV, "60"), (BALANCE, None)})
assert [(r.document_type, r.account) for r in selected.rules] == [(BALANCE, None), (OSV, "60")]
for row in selected.rows:
assert [(c.document_type, c.account, c.status) for c in row.cells] == [
(BALANCE, None, CellStatus.CONFIRMED),
(OSV, "60", CellStatus.MISSING),
]
assert [row.legal_entity_name for row in selected.rows] == ["ООО А", "ООО Б"]
def test_select_skips_documents_absent_from_rules() -> None:
"""Счёт, которого нет в правилах подхода, столбца не добавляет; если не совпало ничего — остаются только ЮЛ."""
selected = select_documents(_checklist(), {(OSV, "90")})
assert selected.rules == []
assert all(row.cells == [] for row in selected.rows)
assert len(selected.rows) == 2 # noqa: PLR2004 - оба ЮЛ периметра
def test_select_does_not_mutate_source() -> None:
checklist = _checklist()
select_documents(checklist, {(INCOME, None)})
assert len(checklist.rules) == len(RULES)
assert all(len(row.cells) == len(RULES) for row in checklist.rows)

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-11-10

View File

@ -0,0 +1,22 @@
# Design
## Context
PR arm-core #227 (AURORA-1678, фронт — В. Цыганкова) добавил фильтр «Документы» в окно чек-листа и параметр
`documents` в запрос CSV. Бэк и OpenSpec доведены в той же ветке при ревью.
## Decisions
1. **Отбор — чистая функция `domain/checklist_filter.py`.** `parse_documents` разбирает список в ключи правил
`(document_type, account)`, `select_documents` оставляет правила и ячейки по ключам. CSV-рендер не меняется.
2. **Формат токенов — как прислал фронт:** `BALANCE`, `INCOME`, `OSV:<счёт>`; имена типов — `document_type` DBML.
3. **Порядок столбцов — порядок `rules`,** а не порядок в запросе: CSV совпадает с матрицей окна.
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 10.11.2026 | `documents` действует и на JSON, не только на CSV | Один контракт отбора для обоих форматов; JSON без параметра не меняется | Низкая: фронт JSON без параметра не трогает, отбор можно снять |
| 10.11.2026 | Документ, которого нет в правилах подхода (`OSV:99`), столбца не добавляет и не даёт ошибку; неверный синтаксис — 422 `INVALID_DOCUMENTS` | Правила подхода — данные; фронт строит список из ответа, рассинхрон не должен ломать выгрузку | Низкая: опечатка в счёте даёт файл без столбца, а не ошибку |
| 10.11.2026 | `documents` разбирается до обращения к БД: для невидимой версии с неверным списком — 422, не 404 | Как у прочих границ входа консолидаций («Границы входных данных»); 422 не раскрывает существование версии | Низкая |
| 10.11.2026 | Фильтра «Группа компаний» из макета [37] нет | Предложение автора PR (Jira AURORA-1678): окно открыто внутри версии одной ГК | Низкая: добавить, если чек-лист появится вне версии |

View File

@ -0,0 +1,33 @@
# Proposal
## Why
Окно «Чек-лист загруженных документов» и CSV (AURORA-1444) показывали всю матрицу. AURORA-1678 (PR arm-core #227):
фильтр «Документы» в окне и выгрузка CSV с тем же отбором. Фронт PR передаёт `documents` в запросе CSV, но ядро
параметр не принимало — файл выгружался целиком.
## What Changes
- `GET /consolidations/versions/{id}/checklist` принимает `documents` (`BALANCE`, `INCOME`, `OSV:<счёт>` через
запятую) и отдаёт только эти правила и ячейки — в JSON и CSV; неверный список — 422 `INVALID_DOCUMENTS`.
- Окно чек-листа: фильтр «Документы», заголовок и кнопка «Скачать» по макету [37], CSV — с отбором окна.
## Capabilities
### Modified Capabilities
- `consolidations-api` — «Чек-лист комплекта»: параметр `documents`.
- `consolidations-ui` — новое требование «Окно «Чек-лист загруженных документов»».
## Impact
Срез 4 (консолидации), репозиторий arm-core: `api/src/domain/checklist_filter.py`,
`api/src/api/endpoints/consolidations/checklist.py`, `web/src/widgets/ConsoleLegalEntities/*`. Таблицы DBML v5.25 не
меняются (`required_document_rule`, `document` — только чтение). Правила — `05-domain-rules.md` §9.5 (чек-лист
комплекта) без изменений: отбор — представление, не алгоритм.
## Non-goals
- Фильтр «Группа компаний» из макета [37] — окно открыто внутри версии одной ГК.
- Колонка «Прочие документы» макета — правил подхода для неё нет.
- Сохранение выбора между открытиями окна.

View File

@ -0,0 +1,73 @@
## MODIFIED Requirements
### Requirement: Чек-лист комплекта
`GET /consolidations/versions/{id}/checklist` (`CONSOLIDATION_VIEW`, policy) SHALL вернуть `rules[]` (правила
`required_document_rule` подхода версии: `document_type`, `account`, `is_mandatory`; порядок — `BALANCE`, `INCOME`,
затем ОСВ по коду счёта) и `rows[]` по ЮЛ периметра (по имени) с `cells[]` в порядке `rules`. Ячейка `{document_type,
account, status, missing_quarters, document_ids}`: `document_type` и `account` — правила ячейки (ячейка
самоописывающая, фронт не опирается на позицию); `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными
документами (ОСВ: по правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии);
`NEEDS_CHECK` / `RECOGNIZED` — есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии
(приоритет `NEEDS_CHECK`); `PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по
`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 и разделителем `;`:
заголовок «Организация;ИНН;<правило>…» (правила — «Бух. баланс», «ОФР», «ОСВ <счёт>»), ячейки — подписи макета:
`CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED` «Требуется проверка», `ERROR` «Ошибка
загрузки», `MISSING` «Отсутствует» (с « (нет: Q1, Q2)» при `missing_quarters`); `Content-Disposition` — `attachment` с
именем `checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987.
Параметр `documents` (JSON и CSV) SHALL отбирать столбцы: список через запятую из `BALANCE`, `INCOME` и `OSV:<счёт>`;
в ответе остаются только названные правила (в порядке `rules`) и их ячейки у каждого ЮЛ, ЮЛ периметра не отбираются;
документ, которого нет в правилах подхода версии, столбца не добавляет. Без `documents` — все правила. Пустой список,
пустой токен, неизвестный документ и `OSV` без счёта SHALL давать 422 `INVALID_DOCUMENTS` (`detail.token`), значение
длиннее 500 символов — 422 валидации; проверка — до обращения к БД.
#### Scenario: Пробел Q2
- **WHEN** у ЮЛ привязаны квартальные ОСВ 60 за Q1 и Q3 2024, версия за 2024 Q3
- **THEN** ячейка ОСВ 60: `document_type = OSV`, `account = "60"`, `status = MISSING`, `missing_quarters = ["Q2"]`,
`document_ids` — два документа
#### Scenario: Субсчёт не закрывает правило
- **WHEN** у ЮЛ подтверждена только ОСВ 58.03 за квартал версии
- **THEN** ячейка ОСВ 58 — `MISSING`
#### Scenario: Файл в обработке
- **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]` чек-листа той же
версии; ячейка ББ — `document_type = BALANCE`, `account = null`
#### Scenario: Чек-лист в CSV
- **WHEN** запрошен чек-лист версии с `format=csv`, у ЮЛ подтверждён ББ, ОСВ 60 отсутствует за Q2
- **THEN** файл начинается с BOM, первая строка — «Организация;ИНН;Бух. баланс;ОФР;ОСВ 60…», в строке ЮЛ — «Загружен» и «Отсутствует (нет: Q2)»
#### Scenario: CSV по выбранным документам
- **WHEN** запрошен чек-лист версии с `format=csv&documents=OSV:60,BALANCE,OSV:99`, у ЮЛ «А» подтверждена ОСВ 60
- **THEN** заголовок — «Организация;ИНН;Бух. баланс;ОСВ 60», строки обоих ЮЛ периметра с двумя ячейками, у «А» —
«Отсутствует;Загружен»
#### Scenario: JSON по выбранным документам
- **WHEN** запрошен чек-лист версии с `documents=INCOME,OSV:60`
- **THEN** `rules` — ОФР и ОСВ 60, у каждой строки две ячейки тех же правил
#### Scenario: Неизвестный документ
- **WHEN** запрошен чек-лист с `documents=OFR` (или пустым, `OSV:`, `BALANCE,,INCOME`)
- **THEN** 422 `INVALID_DOCUMENTS`, файл не отдаётся

View File

@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Окно «Чек-лист загруженных документов»
Кнопка «Скачать чек-лист» панели «Организации» SHALL открывать окно «Чек-лист загруженных документов» (макет [37]):
фильтр «Документы», легенда статусов и матрица «Организация» × «Бух. баланс», «ОФР», «ОСВ» по счетам правил версии.
Фильтр «Документы» — селект с подписью сверху, как фильтры отчётности: в списке пункт «Все документы» и по пункту на
столбец матрицы с флажками; в поле — «Все документы», «Документы не выбраны» или «N документ(а/ов)». При открытии
отмечены все документы. Матрица SHALL показывать только отмеченные столбцы (колонка «Организация» — всегда; группа «ОСВ»
без отмеченных счетов не показывается). «Скачать» SHALL выгружать CSV с теми же документами (`documents` запроса
`GET …/checklist?format=csv`; при всех отмеченных — без параметра) и неактивна, пока не отмечен ни один документ.
Фильтра «Группа компаний» из макета нет: окно открывается внутри версии одной ГК.
#### Scenario: Выбраны два документа
- **WHEN** в фильтре «Документы» отмечены только «ОФР» и «ОСВ 01»
- **THEN** в поле «2 документа», в матрице колонки «Организация», «ОФР» и «ОСВ» → «01»; «Скачать» выгружает CSV с
`documents=INCOME,OSV:01`
#### Scenario: Сняты все документы
- **WHEN** на «Все документы» нажали при всех отмеченных
- **THEN** в поле «Документы не выбраны», в матрице только «Организация», «Скачать» неактивна

View File

@ -0,0 +1,9 @@
# Tasks
- [x] 1.1 arm-core `api/tests/test_domain_checklist_filter.py` — разбор `documents` и отбор столбцов
- [x] 1.2 arm-core `api/src/domain/checklist_filter.py` — `parse_documents`, `select_documents`
- [x] 1.3 arm-core `api/tests/test_api_consolidation_checklist.py` — CSV/JSON с `documents`, 422
- [x] 1.4 arm-core `api/src/api/endpoints/consolidations/checklist.py` — параметр `documents` (≤ 500 символов)
- [x] 1.5 arm-core `web/` — фильтр «Документы» окна чек-листа, CSV с отбором (PR #227, автор)
- [x] 1.6 Сценарий на стенде против макета [37] (Playwright, РБ 2024 К3: «ОФР» + «ОСВ 60» → CSV из двух столбцов)
- [x] 1.7 README — `documents` в описании `GET …/checklist`