From dea64c9f1de39b46636be04da4b5f742578886cc Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 7 Oct 2026 13:30:00 +0400 Subject: [PATCH] =?UTF-8?q?docs(arm-core):=20README,=20CLAUDE.md,=20tasks.?= =?UTF-8?q?md=20=D0=BF=D0=BE=D0=B4=20=D0=A1=D1=80=D0=B5=D0=B7=203b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- CLAUDE.md | 22 +++- README.md | 107 +++++++++++++++--- .../changes/slice-3b-statements-api/tasks.md | 21 +++- 3 files changed, 132 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4b617ae..e26fdc6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 → статус «в работе» → своя блокировка), diff --git a/README.md b/README.md index d18f7cb..2275c76 100644 --- a/README.md +++ b/README.md @@ -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=,` | любой аутентифицированный (права не нужны) | `[{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 по маркерам) diff --git a/openspec/changes/slice-3b-statements-api/tasks.md b/openspec/changes/slice-3b-statements-api/tasks.md index f58a0c8..795d042 100644 --- a/openspec/changes/slice-3b-statements-api/tasks.md +++ b/openspec/changes/slice-3b-statements-api/tasks.md @@ -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