docs(arm-core): README, CLAUDE.md, tasks.md под Срез 3b

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 13:30:00 +04:00
parent 0c203c1d91
commit dea64c9f1d
3 changed files with 132 additions and 18 deletions

View File

@ -25,7 +25,9 @@
ИНН, `file_types.py` — тип файла по расширению и сигнатуре, `osv_mapping.py` — результат парсера → документ/строки
ОСВ (§1.3, §1.5, §9.3); Срез 4 — `consolidation_periods.py` (выбор источников и приведение периодов, §2, Q1-эквивалентность ОСВ,
R-14), `consolidated_osv.py` (чистая сборка свода: склейка кварталов, пробелы, дорезолв, фильтры, §2–§3), `checklist.py`
(чек-лист комплекта, ячейки `CellStatus`); `policy.py` дополнен `IN_WORK_STATUSES`/`ensure_version_editable`, §10; для шахматки
(чек-лист комплекта, ячейки `CellStatus`); Срез 3b — `statement_mapping.py` (формы ББ/ОФР: период, черновик, статус D-6),
`statement_checks.py` (проверки §7 по словарю `api/rules/statement_checks.yaml`), `checklist_csv.py` (CSV чек-листа);
`policy.py` дополнен `IN_WORK_STATUSES`/`ensure_version_editable`, §10; для шахматки
и КФО — Срезы 5–6), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`,
`seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`.
- **DI** — ручные фабрики `Depends` в `src/api/dependencies/`; `storage`/`nats` — из `request.app.state`. **Сессия** —
@ -83,6 +85,24 @@
взаимоблокировки; автозамена дополнительно берёт advisory-lock по ЮЛ. Аудит: `FILE_UPLOAD`,
`FILE_REPLACE` (системное, `user_id` пуст), `FILE_DELETE`, `DOCUMENT_CONFIRM`; смена статусов обмена и маппинга не аудируется
(OQ-55). `osv_entry` пишет только маппер, один раз на файл (очистка перед вставкой); `TOTAL` не хранится.
- **Формы ББ/ОФР (Срез 3b)** — `map_file` (`services/result_mapping.py`) мапит и формы ББ/ОФР: `domain/statement_mapping.py` (чистые
функции) + `repositories/statements.py` (`financial_statement_meta/column/line/value`); форма создаётся с тем, что есть в результате (колонки могут отсутствовать);
документ `BALANCE`/`INCOME` без `meta` (создан до Среза 3b) подтверждается без условий D-9, `…/statement` по нему — 409
`DOCUMENT_NOT_STATEMENT` (решение №16). Проверки §7 — `domain/statement_checks.py` по словарю `api/rules/statement_checks.yaml`; **файл —
копия словаря tabular-parser, при изменении правится в обоих репозиториях** (расхождение молчаливо меняет результат проверок).
Проверки считаются на лету и не хранятся; хранится только акцепт (`checks_accepted_by/at/comment` в шапке). Правка формы и акцепт —
`services/statements.py` (`PATCH …/statement`, `POST …/statement/checks/accept`): **только до подтверждения** (`CONFIRMED` →
`DOCUMENT_READONLY`), аудит `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type=document`; один запрос — одно событие), первичка
рядом (`original_amount`, `*_original_value`, `is_modified`/`*_modified`); любая правка сбрасывает акцепт и пересчитывает статус
документа. **Порядок блокировок «файл → документ»**, шапка (`meta`) читается ПОД блокировкой с `refresh=True` — иначе в сессии
остаётся устаревший акцепт. Подтверждение формы (`services/documents._check_statement`) требует период (`PERIOD_REQUIRED`) и
пройденные/акцептованные жёсткие проверки (`CHECKS_NOT_PASSED`, `detail.failures`); мягкие (в т. ч. межформенная 1370↔2400)
не блокируют. Комментарии к документу — `services/documents.py` (`comments`/`add_comment`, `comment(object_type='document')`,
без аудита; `comments_count` в карточках). `GET /files/{id}/content` — `services/primary_files.content` +
`ObjectStorage.stream` (поток из S3, первая порция до заголовков → `FILE_CONTENT_NOT_FOUND`; сессию тело потока не держит;
имя — RFC 5987). `GET /users/brief` — `current_user` без права; `documents_count` у ЮЛ — `repositories/directories`; CSV чек-листа —
`?format=csv` у `GET …/checklist`. Контракт для фронта — `../docs/dev/api-contract-slice-3b.md`; решения —
`openspec/changes/slice-3b-statements-api/design.md`.
- **Консолидации (Срез 4)** — `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export,
mapping), сервисы: `consolidations` (создание, список, карточка, периметр `PUT …/members`), `consolidation_guard`
(`lock_visible_version`/`lock_editable_version`: версия `FOR UPDATE` → видимость 404 → статус «в работе» → своя блокировка),

107
README.md
View File

@ -28,6 +28,12 @@ PostgreSQL 16, core NATS без JetStream), `web/` — фронтенд (React +
«Первичные документы: загрузка», «Статусы файла и документа», «Цикл маппинга», «API файлов и документов», «Удаление и
замена», «Ограничения Среза 2c», «Сквозной прогон (e2e)».
Срез 3b («формы ББ/ОФР в ядре и API до сводной ОСВ»): маппинг форм ББ/ОФР из результата парсера в ядро, форма для
экрана «Проверка распознавания» с проверками §7, правка формы до подтверждения и акцепт проверок, условия подтверждения
формы, комментарии к документам, исходный файл потоком, имена пользователей, `documents_count` у ЮЛ, CSV чек-листа.
Спецификация — `openspec/changes/slice-3b-statements-api/`; разделы ниже: «API файлов и документов», «Формы ББ/ОФР (Срез 3b)»,
«Ошибки API».
Срез 4 («консолидация»): схема по DBML v5.16, версии консолидации ГК за период, периметр, блокировка «взять в работу»,
статусы, привязка подтверждённых документов к версии, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx.
Спецификация — `openspec/changes/slice-4-consolidation/`; раздел ниже: «Консолидация (Срез 4)».
@ -232,9 +238,9 @@ docker compose -f ../docs/dev/docker-compose.yml down -v # полный сб
| `DELETE /directories/related-groups/{group_id}` | `DIRECTORY_DELETE` | удалить **пустую** ГСК/ГСЗ (`GROUP_NOT_EMPTY`) |
| `POST /directories/related-groups/{group_id}/members` | `DIRECTORY_EDIT` | добавить ЮЛ (только из той же ГК) |
| `DELETE /directories/related-groups/{group_id}/members/{legal_entity_id}` | `DIRECTORY_EDIT` | исключить ЮЛ (членство закрывается, не удаляется) |
| `GET /directories/legal-entities` | `DIRECTORY_VIEW` | видимые ЮЛ/ИП: `company_group_id`, `outside_group`, `inn`, `q`, `closed`, `limit`/`offset` |
| `GET /directories/legal-entities` | `DIRECTORY_VIEW` | видимые ЮЛ/ИП: `company_group_id`, `outside_group`, `inn`, `q`, `closed`, `limit`/`offset`; в каждой записи `documents_count` |
| `POST /directories/legal-entities` | `DIRECTORY_EDIT` | создать ЮЛ/ИП (ИНН: ЮЛ — 10 цифр, ИП — 12) |
| `GET`, `PATCH /directories/legal-entities/{entity_id}` | `DIRECTORY_VIEW` / `DIRECTORY_EDIT` | карточка (с членствами и алиасами) / изменить |
| `GET`, `PATCH /directories/legal-entities/{entity_id}` | `DIRECTORY_VIEW` / `DIRECTORY_EDIT` | карточка (с членствами, алиасами и `documents_count`) / изменить |
| `POST /directories/legal-entities/{entity_id}/move` | `DIRECTORY_EDIT` | перевести в другую ГК с датой `effective_date` ≥ сегодня (дата по Москве, Europe/Moscow); членства ГСК/ГСЗ закрываются |
| `POST /directories/legal-entities/{entity_id}/withdraw` | `DIRECTORY_EDIT` | вывести из ГК: `CLOSE` (с `close_date`) или `REMOVE_FROM_GROUP` |
| `GET /directories/legal-entities/{entity_id}/history` | `DIRECTORY_VIEW` | история ЮЛ/ИП, новые первыми |
@ -243,6 +249,9 @@ docker compose -f ../docs/dev/docker-compose.yml down -v # полный сб
| `POST /directories/import` | `DIRECTORY_ADMIN` | импорт выгрузки ЕЦП CRM (см. ниже) |
| `GET /admin/users`, `POST /admin/users`, `PATCH /admin/users/{user_id}`, `PUT /admin/users/{user_id}/roles` | `USERS_ADMIN` | пользователи и их роли (см. «Администрирование») |
| `GET /admin/roles`, `GET /admin/permissions`, `PUT /admin/roles/{code}/permissions` | `USERS_ADMIN` | матрица «роль × право» |
| `GET /users/brief?ids=<uuid>,<uuid>` | любой аутентифицированный (права не нужны) | `[{id, full_name, is_active}]` для найденных пользователей (в т. ч. неактивных; неизвестные id пропускаются) — чтобы фронт показывал имена вместо UUID (`confirmed_by`, `locked_by`, авторы комментариев); до 200 id, больше — `TOO_MANY_IDS`, некорректный UUID — `INVALID_IDS` |
`documents_count` у ЮЛ — число документов его файлов, кроме файлов `SUPERSEDED` (подсчёт одним запросом на страницу).
Списки верхнего уровня (ГК, ЮЛ, пользователи) отдают страницу `{items, total, limit, offset}` (`limit` 1–200, по
умолчанию 50); список ГСК/ГСЗ одной ГК — простой массив. Каждая мутация пишет `audit_event` в той же транзакции, что и
@ -476,12 +485,12 @@ S3 (storage); без S3 — предупреждение в логе, цикл
отвергаются);
2. читает объект из S3 **вне транзакции** и валидирует `RecognitionResult` v1.x (`job_id`/`file_id` должны совпасть с заданием);
3. в одной транзакции (порядок блокировок «задание → файл») удаляет прежние строки файла (идемпотентность), создаёт
`document` на каждый лист ОСВ и пакетами вставляет `osv_entry` (без строки `TOTAL`; `parent_entry_id`, `account_root`,
`document` на каждый лист ОСВ и на каждую форму ББ/ОФР (шапка, колонки, строки, значения; см. «Формы ББ/ОФР»), пакетами вставляет `osv_entry` (без строки `TOTAL`; `parent_entry_id`, `account_root`,
`account_id`, `counterparty_id`), пишет `recognition_job.group_schema` и `metrics.mapping_warnings`, переводит файл
в `PROCESSED` и выполняет автозамену (см. «Удаление и замена»).
Ошибки: **неповторяемые** (ключ вне папки задания, нет объекта, результат другого задания, невалидный результат, нет
листов ОСВ) сразу переводят файл в `ERROR` с `metrics.mapping_error`; **повторяемые** (БД, S3) откатывают транзакцию,
документов) сразу переводят файл в `ERROR` с `metrics.mapping_error`; **повторяемые** (БД, S3) откатывают транзакцию,
увеличивают `metrics.mapping_attempts`, цикл спит `MAPPING_POLL_SECONDS` и пробует снова; после `MAPPING_MAX_ATTEMPTS`
— `ERROR`. Пока попытки не исчерпаны, файл остаётся `PROCESSING`; повторный маппинг начинается с очистки строк файла, дублей нет.
@ -507,13 +516,64 @@ S3 (storage); без S3 — предупреждение в логе, цикл
| `DELETE /files/{file_id}` | `DOCUMENT_EDIT` | удалить файл (см. «Удаление и замена»), 204 |
| `GET /documents/{document_id}` | `DOCUMENT_VIEW` | документ: статус, проверки, `locator`, `group_schema` листа, `warnings`, `file_id` |
| `GET /documents/{document_id}/entries` | `DOCUMENT_VIEW` | строки ОСВ по `row_number` (`limit` ≤ 2000, по умолчанию 500); дерево — по `parent_entry_id` |
| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM` |
| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM`; для формы ББ/ОФР — ещё `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` |
| `GET /documents/{document_id}/statement` | `DOCUMENT_VIEW` | форма ББ/ОФР: шапка, колонки, строки со значениями, редакция, проверки §7 (см. «Формы ББ/ОФР») |
| `PATCH /documents/{document_id}/statement` | `DOCUMENT_EDIT` | правка значений и шапки (`inn`, `report_date`, `units`) до подтверждения; аудит `RECOGNIZED_DATA_EDIT` |
| `POST /documents/{document_id}/statement/checks/accept` | `DOCUMENT_EDIT` | акцепт непройденных жёстких проверок с комментарием (`{comment}`) |
| `GET /documents/{document_id}/comments` | `DOCUMENT_VIEW` | комментарии по возрастанию `created_at`: `{id, text, author {id, full_name}, created_at}` |
| `POST /documents/{document_id}/comments` | `DOCUMENT_EDIT` | добавить комментарий (`{text}`, 1–4000 символов после обрезки пробелов), 201; в любом статусе документа |
| `GET /files/{file_id}/content` | `DOCUMENT_VIEW` | исходный файл потоком из S3; `?disposition=attachment` (по умолчанию) или `inline`; имя — `Content-Disposition` по RFC 5987 |
Карточка документа и документ в карточке файла несут `comments_count`. Просмотр и скачивание первички (`/files/{id}/content`)
и CSV чек-листа в аудит не пишутся (как экспорт сводной ОСВ). Чужой и несуществующий файл — 404 `NOT_FOUND`; файл есть, а объекта
в S3 нет (истёк или пропал) — 404 `FILE_CONTENT_NOT_FOUND`; S3 не подключено — 503 `STORAGE_UNAVAILABLE`.
Страницы — `{items, total, limit, offset}`. Суммы строк — десятичные значения без округления, как в результате парсера.
Коды ошибок этих маршрутов — в таблице «Ошибки API» (`FILE_TOO_LARGE`, `UNSUPPORTED_FILE`, `DUPLICATE_FILE`,
`STORAGE_UNAVAILABLE`, `FILE_SUPERSEDED`, `ALREADY_CONFIRMED`, `DOCUMENT_NOT_CONFIRMABLE`,
`FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS`). События аудита: `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`,
`DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`).
`FILE_HAS_CONFIRMED_DOCUMENTS`, `FILE_SUPERSEDES_OTHERS`, `FILE_CONTENT_NOT_FOUND`) и форм ББ/ОФР (`DOCUMENT_NOT_STATEMENT`,
`DOCUMENT_READONLY`, `UNKNOWN_CELL`, `DUPLICATE_CELL`, `INVALID_INN`, `NOTHING_TO_ACCEPT`, `PERIOD_REQUIRED`,
`CHECKS_NOT_PASSED`). События аудита: `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел
`PRIMARY_DOCUMENTS`), `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`; правка формы и акцепт).
## Формы ББ/ОФР (Срез 3b)
Цикл маппинга разбирает не только листы ОСВ, но и формы ББ/ОФР результата парсера: на каждую форму создаются `document`
(`BALANCE`/`INCOME`) и шапка `financial_statement_meta` с колонками, строками и значениями (`financial_statement_column`,
`_line`, `_value`). Первичка не мутируется: правка аналитика пишется рядом (`is_modified`, `original_amount`,
`modified_by/at`; у полей шапки — `*_modified`, `*_original_value`). Период формы — по дате отчёта, а при её отсутствии по самой
поздней дате колонок; ББ/ОФР всегда нарастающие. Статус документа формы — `RECOGNIZED` только если задание `done`, нет нарушений
проверок §7 (или они акцептованы), контрагент не `MISMATCH`, период определён, значения разобраны, есть строки и нет
предупреждений; иначе `NEEDS_CHECK` (правило D-6 спеки). Проверки §7 (`domain/statement_checks.py`) считаются на лету по
текущим суммам формы и не хранятся; словарь — `api/rules/statement_checks.yaml` (копия словаря tabular-parser — правится
в обоих репозиториях).
- **`GET /documents/{id}/statement`** — `{document, edition, header, columns[], lines[], checks}`: `header` — `inn`,
`report_date`, `units` вида `{value, original_value, verified, modified}` (у ИНН ещё `confidence`) и справочные поля формы
(`organization_name`, `okud`, `okpo`, …); `lines[].values[]` — суммы строкой, `is_modified`, `original_amount`; `checks` —
`{failed, soft_failed, cross_form, accepted}` (нарушение — `{column_index, relation, expected, actual}`, у межформенной
проверки `column_index = -1`). Не форма (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) — 409 `DOCUMENT_NOT_STATEMENT`.
Подтверждённая форма читается.
- **`PATCH /documents/{id}/statement`** — `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}`
(до 5000 ячеек; `amount: null` очищает ячейку; не более двух знаков). Правится только неподтверждённая форма
(`RECOGNIZED`/`NEEDS_CHECK`): `CONFIRMED` — 409 `DOCUMENT_READONLY` (исправление — новым файлом), заменённый файл —
409 `FILE_SUPERSEDED`, статус `ERROR` — 409 `DOCUMENT_NOT_CONFIRMABLE`. Ячейка не из этой формы — 422 `UNKNOWN_CELL`,
одна ячейка дважды — 422 `DUPLICATE_CELL`, ИНН не из 10 или 12 цифр — 422 `INVALID_INN`. Один запрос — одна транзакция и одно
событие `RECOGNIZED_DATA_EDIT` с `before/after`; возврат ячейки к исходному значению снимает `is_modified`; правка ИНН
пересчитывает `counterparty_check`, даты отчёта — период документа; любая правка сбрасывает акцепт и пересчитывает статус.
Запрос без изменений — 200 без аудита. Ответ — та же форма, что у `GET`.
- **`POST /documents/{id}/statement/checks/accept`** — `{comment}` (1–2000 символов): акцепт нарушенных жёстких проверок
(`checks_accepted_by/at/comment`). Нарушений нет — 409 `NOTHING_TO_ACCEPT`. Акцепт сбрасывается любой правкой формы.
- **Подтверждение формы** (`POST /documents/{id}/confirm`) дополнительно требует определённый период (иначе 409
`PERIOD_REQUIRED`) и пройденные или акцептованные жёсткие проверки §7 (иначе 409 `CHECKS_NOT_PASSED`; в `detail.failures[]` —
`{column_index, relation, expected, actual}`). Мягкие проверки (в т. ч. межформенная `1370 − 1370(начало года) = 2400`)
подтверждение не блокируют. Подтверждённая форма привязывается к версиям консолидации и закрывает ячейки ББ/ОФР чек-листа, как ОСВ.
- **Порядок блокировок** правки и акцепта — «файл → документ» (как у подтверждения); шапка перечитывается под блокировкой.
Комментарии аудит не пишут; при удалении файла комментарии его документов остаются в таблице `comment` без владельца
(полиморфная связь без FK, решение №18; наружу не отдаются).
Расчёт — `src/domain/statement_mapping.py` и `src/domain/statement_checks.py` (чистые функции), доступ к данным —
`src/repositories/statements.py`, правка и акцепт — `src/services/statements.py`.
## Удаление и замена
@ -538,8 +598,8 @@ S3 (storage); без S3 — предупреждение в логе, цикл
- Объекты S3 при удалении не стираются (OQ-22, OQ-111): растёт бакет; e2e оставляет ~2 объекта за прогон.
- Общий размер multipart-запроса `POST /legal-entities/{id}/files` и число файлов в нём приложением не ограничены
(Starlette спулит части на диск, `UPLOAD_MAX_BYTES` — лимит одного файла); лимит запроса задаётся ingress платформы.
- Маппер Среза 2c разбирает только листы ОСВ (`osvDocument`); ББ/ОФР и результаты `scan-gateway` — следующие срезы,
прочие документы результата (BALANCE/INCOME/OTHER) игнорируются без предупреждения.
- Маппер Среза 2c разбирал только листы ОСВ (`osvDocument`); с Среза 3b он мапит и формы ББ/ОФР (раздел «Формы ББ/ОФР»).
Результаты `scan-gateway` (PDF/изображения) и документы `OTHER` по-прежнему игнорируются без предупреждения.
- Корневой сегмент счёта (`account_root`) — только цифровой (`ASSUMPTION(OQ-103)`): забалансовые «МЦ.04», «ГТД»
получают `account_root = NULL`; `account_id` ищется точным совпадением с планом счетов seed, субсчетов в seed
неполный набор — часть строк остаётся с `account_id = NULL`.
@ -547,8 +607,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл
- ОСВ «нарастающая по данным Q1» (Q2–Q4 с `opening` корня, равным `opening` Q1) получает предупреждение маппинга,
поэтому документ остаётся `NEEDS_CHECK`: аналитик подтверждает его вручную (решение R-17, ждёт подтверждения владельца).
- `period_check` — `UNKNOWN` (OQ-120); к версии консолидации привязываются только `CONFIRMED` документы, а их файл удалить
нельзя — удаление файла с документом в версии невозможно (OQ-110, Срез 4); FK `financial_statement_meta` и `ex_snapshot_item`
— Срезы 3 и 7; `EntryOut` не отдаёт `is_modified` (панель правки — подсрез 2d).
нельзя — удаление файла с документом в версии невозможно (OQ-110, Срез 4); FK `ex_snapshot_item` — Срез 7; `EntryOut` не отдаёт `is_modified` (панель правки — подсрез 2d).
- Два экземпляра ядра могут одновременно мапить один файл (второй проиграет блокировку и пропустит кандидата);
при уменьшении `MAPPING_MAX_ATTEMPTS` файлы с исчерпанным счётчиком остаются `PROCESSING` до ручного вмешательства.
@ -574,7 +633,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл
| `DELETE /consolidations/versions/{id}/lock` | EDIT | снять свою блокировку (204; нет активной — тоже 204) |
| `POST /consolidations/versions/{id}/status` | EDIT | ручной переход: `CANCELLED` (с комментарием) или возврат в `INITIATED` |
| `PUT /consolidations/versions/{id}/members` | EDIT | заменить периметр (ЮЛ должны принадлежать ГК, ≥ 2) |
| `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки |
| `GET /consolidations/versions/{id}/checklist` | VIEW | комплект документов: ЮЛ × правила, статус ячейки; `?format=csv` — тот же чек-лист файлом CSV |
| `GET /consolidations/versions/{id}/osv` | VIEW | страница сводной ОСВ и фильтры (`limit` ≤ 2000, по умолчанию 500) |
| `GET /consolidations/versions/{id}/osv/summary` | VIEW | `rows_total`, `unmapped_total`, `auto_matched_total`, `documents_total`, `gaps[]` |
| `GET /consolidations/versions/{id}/osv/export` | VIEW | xlsx: лист «Сводная ОСВ» и лист «Пробелы» |
@ -631,6 +690,13 @@ Q2–Q4 не подходят (R-14a). Привязываются только `
`ERROR` (последний файл ЮЛ в `ERROR`), `MISSING`; `missing_quarters` — пропущенные кварталы при частичном покрытии ОСВ.
`PROCESSING` ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (до маппинга счёт файла неизвестен).
`GET …/checklist?format=csv` (Срез 3b, право `CONSOLIDATION_VIEW`) отдаёт чек-лист файлом `text/csv`: UTF-8 с BOM, разделитель `;`,
строки через CRLF, заголовок — «Организация;ИНН;<правило>…» (подписи правил: «Бух. баланс», «ОФР», «ОСВ 66»), строки — ЮЛ, в ячейках — подписи
статусов («Загружен», «Обработка», «Требуется проверка», «Ошибка загрузки», «Отсутствует»; при пропущенных кварталах —
«… (нет: Q1, Q2)»); имя файла — `checklist-<ГК>-<год>-<квартал>.csv`
в `Content-Disposition` по RFC 5987. Формат по умолчанию — `json`; другое значение `format` — 422 валидации. Скачивание в аудит не пишется
(`domain/checklist_csv.py`, чистая функция).
### Сводная ОСВ
Чистая функция от версии (`domain/consolidated_osv.build` + `domain/consolidation_periods.select_sources`), в БД не пишется
@ -728,7 +794,7 @@ email без учёта регистра проверяет только сер
| `NOT_FOUND` | 404 | объекта нет или он вне видимости; в консолидациях различие — по тексту: «версия консолидации не найдена», «ГК не найдена», «ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено» |
| `OFFICE_NOT_FOUND` | 422 | филиал из тела запроса не существует (ГК, ЮЛ, пользователь) |
| `OFFICE_NOT_ALLOWED` | 422 | ГК можно закрепить только за своим филиалом |
| `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр |
| `INVALID_INN` | 422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр; в правке формы (`PATCH …/statement`) — не из 10 или 12 цифр |
| `EFFECTIVE_DATE_IN_PAST` | 422 | дата перевода/вывода раньше сегодняшней (по Москве) |
| `EMPTY_ALIAS` | 422 | алиас пуст после нормализации (одни ОПФ и знаки препинания) |
| `DIFFERENT_COMPANY_GROUP` | 422 | ЮЛ не входит в ГК этой ГСК/ГСЗ |
@ -756,6 +822,15 @@ email без учёта регистра проверяет только сер
| `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение: статус документа не `RECOGNIZED`/`NEEDS_CHECK` (например, `ERROR`) |
| `FILE_HAS_CONFIRMED_DOCUMENTS` | 409 | удаление файла с `CONFIRMED` документом |
| `FILE_CONTENT_NOT_FOUND` | 404 | `GET /files/{id}/content`: файл есть, объекта нет в S3 (истёк или пропал) |
| `DOCUMENT_NOT_STATEMENT` | 409 | `…/statement`: документ — не форма ББ/ОФР (ОСВ, расшифровка, ББ/ОФР без данных формы — созданные до Среза 3b) |
| `DOCUMENT_READONLY` | 409 | правка/акцепт формы: форма уже `CONFIRMED` (исправление — новым файлом) |
| `UNKNOWN_CELL` | 422 | `PATCH …/statement`: `line_id`/`column_id` не принадлежат этой форме |
| `DUPLICATE_CELL` | 422 | `PATCH …/statement`: одна ячейка указана в запросе дважды |
| `NOTHING_TO_ACCEPT` | 409 | акцепт: непройденных жёстких проверок нет (в том числе уже акцептованы) |
| `PERIOD_REQUIRED` | 409 | подтверждение формы: период не определён — поправьте дату отчёта |
| `CHECKS_NOT_PASSED` | 409 | подтверждение формы: жёсткие проверки §7 не пройдены и не акцептованы; `detail.failures[] {column_index, relation, expected, actual}` |
| `TOO_MANY_IDS` | 422 | `GET /users/brief`: больше 200 id |
| `INVALID_IDS` | 422 | `GET /users/brief`: элемент `ids` — не UUID |
| `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла, на который ссылаются `superseded_by_file_id` других файлов |
| `CONSOLIDATION_EXISTS` | 409 | консолидация этой ГК за год и квартал уже создана (в т. ч. гонка двух `POST /consolidations`) |
| `PERIMETER_TOO_SMALL` | 422 | в ГСК (при создании) или в новом периметре (`PUT …/members`) меньше двух ЮЛ; ничего не создаётся и не меняется |
@ -918,6 +993,7 @@ make migration-cycle # проверить цикл upgr
api/
run_server.py точка входа (uvicorn; хост и порт — из настроек)
alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14, 0003_dbml_v5_15.py — v5.15, 0004_dbml_v5_16.py — v5.16)
rules/ statement_checks.yaml — словарь проверок §7 формы ББ/ОФР (копия словаря tabular-parser; меняется в обоих репозиториях)
seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист)
seed/dev/ dev-данные: филиалы и пользователи по ролям
scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников)
@ -932,13 +1008,16 @@ api/
repositories/ доступ к данным
schemas/ pydantic-схемы запросов и ответов
services/ бизнес-логика; primary_files (загрузка/удаление), result_mapping (цикл маппинга и автозамена),
file_status (статус файла по заданию), documents (карточки, подтверждение), recognition_exchange (обмен);
file_status (статус файла по заданию), documents (карточки, подтверждение, комментарии), statements (форма ББ/ОФР: чтение, правка, акцепт; Срез 3b),
recognition_exchange (обмен);
Срез 4 — consolidations (версии, периметр), consolidation_guard (общий пролог правок), consolidation_locks,
consolidation_status (`transition`), consolidation_documents (привязка), consolidated_osv (свод, кандидаты),
counterparty_mapping (ручное сопоставление), osv_export (xlsx)
common/ доменные ошибки (`{detail: {code, message}}`), часы
domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn,
file_types (тип файла по расширению и сигнатуре), osv_mapping (результат парсера → документы/строки, §1.3, §1.5, §9.3);
Срез 3b — statement_mapping (формы ББ/ОФР: период, статус, §9.3, §9.5), statement_checks (проверки §7),
checklist_csv (CSV чек-листа);
Срез 4 — consolidation_periods (выбор источников, §2), consolidated_osv (свод), checklist (чек-лист), policy
(`ensure_version_editable`, §10)
tests/ pytest (юнит и integration/fixtures по маркерам)

View File

@ -1,6 +1,21 @@
# Tasks: Срез 3b — формы ББ/ОФР в ядре и API до сводной ОСВ
Заполняется планом (`writing-plans`) после одобрения спеки владельцем. Репозиторий — arm-core; команды — из корня
(`make`), `pytest` — из `api/`.
План реализации — `docs/superpowers/plans/2026-10-07-slice-3b-statements-api.md` (16 задач). Репозиторий — arm-core; команды —
из корня (`make`), `pytest` — из `api/`.
- [ ] 0.1 План реализации написан и одобрен; задачи перенесены сюда из плана
- [x] 0 Подготовка окружения (план одобрен, задачи перенесены сюда)
- [x] 1 Проверки §7 — словарь `rules/statement_checks.yaml` и чистые функции `domain/statement_checks.py`
- [x] 2 Чистые функции маппинга формы (`domain/statement_mapping.py`): период, черновик, статус
- [x] 3 Помощники golden-тестов форм
- [x] 4 Маппинг форм ББ/ОФР в `map_file` (`repositories/statements.py`): шапка, колонки, строки, значения, статус
- [x] 5 API формы — `GET /documents/{id}/statement`
- [x] 6 Правка формы (`PATCH …/statement`) и акцепт проверок (`POST …/statement/checks/accept`), аудит `RECOGNIZED_DATA_EDIT`
- [x] 7 Подтверждение формы (`PERIOD_REQUIRED`, `CHECKS_NOT_PASSED`) и сквозной сценарий чек-листа
- [x] 8 Комментарии к документу (`GET/POST /documents/{id}/comments`, `comments_count`)
- [x] 9 Отдача исходного файла потоком (`GET /files/{id}/content`)
- [x] 10 Справочник имён пользователей (`GET /users/brief`)
- [x] 11 `documents_count` у ЮЛ
- [x] 12 Чек-лист в CSV (`GET …/checklist?format=csv`)
- [x] 13 README, CLAUDE.md, tasks.md
- [ ] 14 Документы workspace (bounded-PR в docs): §7/§9, контракт `dev/api-contract-slice-3b.md`, OQ — PR docs
- [ ] 15 Завершение: слияние `origin/main`, `make check-full`, `openspec validate --strict`, финальное ревью ветки, PR arm-core