arm-ka-docs/dev/qa-guide.md
Andrey Epifancev b0834fbcfb qa-guide §8.6–8.7: сводная ОСВ под текущий экран, шаг «Единицы»; Content-Disposition открыт (#162, AURORA-1532)
Прогон §8.6 на стенде из main 23.10.2026: W29 — статус «Корректировка данных»,
«Шахматка» доступна; W31a — «Единицы» и «Знаков после запятой»; W32a/W33 —
сброс в окне фильтра; W35 — запасное имя без arm-core #173; §8.7 — «Взять
в работу», «Изменить статус» и сопоставление контрагентов уже в UI.
frontend-api-guide §7, §10 п. 9 — Access-Control-Expose-Headers (arm-core #173).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:38:00 +04:00

996 lines
149 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 3b, 4 и фронт)
Документ для ручного тестирования текущего `main` репозитория `arm-core` — бэкенда и встроенного фронта `web/` (с
21.10.2026 фронт ведётся там; прежний отдельный репозиторий `arm-web` заморожен и для тестирования не нужен). Путь
проверен 05.10.2026 на macOS (Docker 28, compose 2.34, Node 24, uv 0.10): от пустого каталога до экрана администратора —
около 15 минут, из них 10 — скачивание образов. Сценарий в UI (раздел 8) проверен 08.10.2026 в Chrome 1600×1000 на данных РБ 2024 Q3.
Как читать: раздел 4 — поднять стенд, раздел 8 — пройти основной путь аналитика в браузере, разделы 6–7 — то же и
глубже через API (Swagger/curl): там проверяются права, коды ошибок и граничные случаи, до которых UI не доходит.
Инструкция пользователя по экранам со скриншотами (без стенда и API) — [`../user/user-guide.md`](../user/user-guide.md).
Единая нормализация ББ/ОФР (контракт результата распознавания 2.0, выкладка 19.10.2026): что изменилось, справка по
предупреждениям, чек-лист N1–N66 и известные замечания — [`qa-statements-v2.md`](qa-statements-v2.md); разделы 6.8, 6.9,
8.5 ниже приведены к этому поведению.
## 1. Что сейчас есть и чего нет
**Есть (Срез 0 + Срез 1 + Срез 2c):**
- Бэкенд `arm-core` (FastAPI, `/api/v1`): вход по email в dev-режиме, профиль пользователя, права как данные.
- API справочников: группы компаний (ГК), юрлица и ИП (ЮЛ) с историей, переводом между ГК и закрытием, алиасы
контрагентов, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Видимость по филиалу: головной
офис видит всё, филиал — только свои ГК.
- Импорт справочников из выгрузки ЕЦП CRM (xlsx/csv) с пробным прогоном и отчётом.
- Администрирование: пользователи, роли, матрица «роль × право», защита последнего администратора.
- Фронтенд `arm-core/web` (раздел 8): группы компаний, консолидации и создание консолидации, панель
«Организации» с чек-листом CSV, загрузка файлов ЮЛ с опросом статуса, просмотр ОСВ-документа с подтверждением, проверка
форм ББ/ОФР рядом со сканом PDF (правка, акцепт нарушений, подтверждение), сводная ОСВ с фильтрами, пробелами и
экспортом xlsx; администрирование `/admin` («Пользователи и роли»).
- Журнал аудита в БД (`dkb.audit_event`).
- Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор
результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление
файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ и формах ББ/ОФР — раздел 6.7; формы ББ/ОФР в ядре (просмотр, правка, акцепт проверок, подтверждение, комментарии, исходный файл, CSV чек-листа; Срез 3b) — раздел 6.9. Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
- Распознавание PDF в `scan-gateway` с эмулятором ИС Сканирование (Срез 3a, раздел 6.8): отдельно от ядра, без UI; загружать
`aep_3.pdf` / `aep_6.pdf` из `fixtures/ocr`.
- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых
документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и
контрольные цифры — 7.1–7.3). Основной путь консолидации есть и в UI — раздел 8.
- План счетов — полная выгрузка 1С заказчика, 488 строк (arm-core #25): субсчета реальных ОСВ (66.01, 76.07.x, забалансовые,
буквенные `01.К`) резолвятся в `account_id`. Границы входов консолидации: периметр ≤ 500 ЮЛ, комментарий статуса ≤ 1000, поиск
`q` ≤ 500 символов (arm-core #26, раздел 6.5).
- Парсер ОСВ после пакета ДКБ (arm-tabular-parser #13, #17): скользящие периоды, несколько корней без строки счёта, «Итого
развернутое», шапка без меток, ИНН в тексте названия и строкой выше (#15); тесты на 25 файлах `osv-dkb/**` по манифесту
ожиданий, golden — 113 файлов (раздел 6.7).
- Единая нормализация ББ/ОФР (arm-ka-docs#116, DBML v5.19, контракт результата 2.0): Excel и PDF разбираются одним общим
кодом `arm_statements`; строки без кода и с нечитаемым кодом сохраняются со статусами, повторы кода законны, подсветка
только «требует внимания», предупреждения у места, правка кодов строк и дат колонок, новые блокировки подтверждения —
[`qa-statements-v2.md`](qa-statements-v2.md).
**Нет (это не баги):**
- Экранов справочников ГК/ЮЛ/ГСК (пункты меню «ГК и организации», «ЮЛ вне ГК» — заглушки) — проверяются только через
Swagger (`/docs`) или curl.
- В UI нет: блокировки версии («Взять в работу»), смены периметра и ручных переходов статусов, сопоставления
контрагентов в сводной ОСВ, экрана чек-листа (только CSV), комментариев к документам, удаления файлов, правки строк
ОСВ. Это проверяется через API (разделы 6.6, 6.9, 7). Шахматка, КФО, «Единое хранилище», «Журнал действий» —
следующие срезы (в UI — заглушки или неактивные вкладки).
- Настоящего входа через SSO банка. Вход — dev-форма по email без пароля.
## 2. Что установить и проверить из ПО
| Что | Версия | Проверка | Если нет |
|---|---|---|---|
| ОС | macOS или Linux. Windows — только через WSL2 (Ubuntu); нативно не проверялось | — | — |
| Git + SSH-ключ в Gitea | любая свежая | `ssh -T -p 2222 git@git.aepif.ru` отвечает приветствием | добавить публичный ключ в профиль Gitea `git.aepif.ru` |
| Docker + compose v2 | Docker ≥ 24, compose ≥ 2.20 (проверено 28.0 / 2.34) | `docker compose version` | Docker Desktop (macOS) или docker-ce + docker-compose-plugin (Linux) |
| `uv` | ≥ 0.4 (проверено 0.10) | `uv --version` | `curl -LsSf https://astral.sh/uv/install.sh \| sh`; Python 3.11 `uv` скачает сам |
| Node.js + npm | Node ≥ 20 LTS (проверено 24.7) | `node --version && npm --version` | nodejs.org или `nvm install --lts` |
| `make`, `curl` | любые | `make --version`, `curl --version` | macOS: Xcode Command Line Tools (`xcode-select --install`); Linux: `build-essential` |
| Свободные порты | 5432, 4222, 8222, 9000, 9001 (compose), 8000 (бэкенд), 8001 (tabular-parser), 3000 (фронт) | `lsof -i :5432` и т. д. пусто | закрыть локальный PostgreSQL/MinIO или сменить порты в `docs/dev/.env` и `arm-core/api/.env` |
| Ресурсы | Docker: ≥ 4 ГБ RAM, ≥ 3 ГБ диска под образы и тома | — | — |
| Доступ в интернет | нужен один раз: образы Docker Hub, PyPI, npm | — | Nexus банка не требуется |
Необязательно, но удобно: клиент PostgreSQL (DBeaver или `psql`) для просмотра аудита; Postman или аналог — Swagger
на `/docs` достаточно.
Браузер — свежий Chrome, Firefox, Safari или Edge (просмотр PDF использует современный pdf.js; в старых браузерах скан
не откроется). Удобный размер окна — от 1600×1000: на нём сверялся макет.
## 3. Клонирование
Репозитории должны лежать **рядом**, в одном каталоге, под именами `docs`, `arm-core`, `fixtures` (в коде пути
вида `../docs/...`, `../fixtures/...`):
```bash
mkdir arm-ka && cd arm-ka
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-ka-docs.git docs
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-core.git arm-core
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-ka-fixtures.git fixtures
```
Фронт — каталог `web/` в `arm-core`, отдельно его клонировать не нужно. Обновление: `git -C arm-core pull`.
Сервисы распознавания собираются из исходников, поэтому для загрузки файлов (6.6–7, раздел 8) рядом нужен ещё
`tabular-parser`, для PDF (6.8, 8.5) — `scan-gateway`:
```bash
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-tabular-parser.git tabular-parser
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-scan-gateway.git scan-gateway
```
Без SSH-ключа можно клонировать по HTTPS с логином и паролем Gitea: `https://git.aepif.ru/RSHB/arm-core.git` и т. д.
`fixtures` содержит реальные данные (внутренний доступ) — нужен для файла импорта. Если доступа нет, пункт 6.3
чек-листа можно выполнить на полной выгрузке из `docs/source/biq/`.
## 4. Запуск стенда
Все команды — из каталога `arm-core`, если не сказано иное.
### 4.1 Инфраструктура (PostgreSQL, NATS, MinIO)
```bash
cp ../docs/dev/.env.example ../docs/dev/.env
make up
docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio
docker compose -f ../docs/dev/docker-compose.yml ps
```
Ожидание: `postgres`, `nats`, `minio` — `Up (healthy)`; `minio-init` — `Exited (0)` (это нормально: он создаёт бакет и
завершается). Первый запуск скачивает образы, это самая долгая часть.
### 4.2 Бэкенд
```bash
make venv # api/.venv на Python 3.11, зависимости из PyPI
cp api/.env.example api/.env # dev-настройки: DEBUG=true, AUTH_DEV_ISSUER=true
make migrate # схема БД
make seed-dev # справочники + 7 dev-пользователей + dev-ГК под документы fixtures (см. ниже)
cd api && .venv/bin/python run_server.py
```
Ожидание: в логе нет трейсбэков; `curl -s localhost:8000/healthcheck` отвечает `{"uptime": ..., "nats": "ok"}`.
Swagger: <http://localhost:8000/docs>. Сервер слушает только `127.0.0.1`, так и оставьте.
Для сценариев загрузки ОСВ (6.6) нужны ещё `NATS_ENABLED=true` (так в `api/.env.example`) и **tabular-parser**; поднимайте
только его, без второго экземпляра ядра (он забирает часть сообщений `files.tabular_parsed`):
```bash
docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps tabular-parser
curl -s localhost:8001/healthcheck # nats: ok
```
### 4.3 Фронтенд (второй терминал, из корня `arm-ka`)
```bash
cd arm-core/web
npm install # первый раз — несколько минут
REACT_APP_ROOT_PATH=/ npm run start # dev-сервер rsbuild на :3000, браузер откроется сам
```
Откроется <http://localhost:3000/> с формой «Вход для разработки». Запросы `/api` dev-сервер проксирует на ядро
(`ARM_CORE_URL`, по умолчанию `http://localhost:8000`), поэтому `REACT_APP_API_URL` задавать не нужно, а CORS не мешает
скачиванию файлов. Ядро на другом адресе: `ARM_CORE_URL=http://host:port REACT_APP_ROOT_PATH=/ npm run start`.
Не забывайте `REACT_APP_ROOT_PATH=/`: без него `npm run start` ждёт путь приложения как на
платформе банка (`/aurora/apps/fastapi-dkb-armka`) и показывает белый экран. Не используйте и `npm run build`
для тестирования: в production-сборке dev-формы входа нет, страница показывает «Вход выполняется через РАИСА».
Проверка сборки фронта (линтер, типы, юнит-тесты, сборка) — `make web-check` в `arm-core` и `npm test`, `npm run lint` в
`arm-core/web`; ручному тестированию она не нужна.
Настройки из `api/.env.example` (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `CORS_ORIGINS=`, `FORWARDED_ALLOW_IPS=127.0.0.1`)
для локального стенда менять не нужно: `DEBUG=true` включает документацию API на `/docs`, dev-вход и разрешает `make seed-dev`.
`make seed-dev` (arm-core #31) создаёт, кроме пользователей, **dev-ГК под документы из `fixtures`**: ГК «Русский Бисквит» ровно как в
выгрузке ЕЦП CRM (20 ЮЛ/ИП, филиал «Санкт-Петербургский РФ», ГСК из АО «Русский бисквит» `3528052718` и ООО «Пекарня
Счастливочная» `3528104476`, алиасы под имена контрагентов из ОСВ) и ГК владельцев форм пакета ДКБ («Унагранде», «Новороссийский
прокатный завод», «Агрофирма Межениновская»). Поэтому ОСВ и формы из `../fixtures` загружаются сразу на «свои» ЮЛ (6.6, 7.1), а
импорт выгрузки ЕЦП (6.3) на таком стенде отвечает `GK_EXISTS`/`LE_EXISTS`. Стенд без dev-ГК (пустые справочники, как в тестах
ядра): `cd api && .venv/bin/python -m scripts.seed --dev --no-dev-groups`.
### 4.4 Вход
В форме введите **email** dev-пользователя (не имя и не логин) и нажмите «Войти (dev)». Пароля нет. Это единственная форма
входа АРМ: страница с полями «Имя пользователя / пароль» — это Gitea, а не стенд. Токен живёт в текущей вкладке
(`sessionStorage`), новая вкладка — новый вход. Токен действует 8 часов; после истечения форма входа появится снова с
сообщением «Сессия истекла». Кнопки «Выйти» нет: чтобы сменить пользователя, откройте новую вкладку (или закройте текущую).
Главная `/` после входа показывает ФИО dev-пользователя («Dev: Аналитик ГО») и его роли («Роли: KA_GO»).
| Email | Роль | Филиал | Что видит |
|---|---|---|---|
| `admin@armka.local` | ADMIN | Головной офис | всё; единственный с правом администрирования и импорта |
| `ka_go@armka.local` | KA_GO | Головной офис | все ГК и ЮЛ, может создавать и менять |
| `curator_go@armka.local` | CURATOR_GO | Головной офис | все ГК и ЮЛ, может создавать и менять |
| `expert@armka.local` | EXPERT | Головной офис | всё, только чтение |
| `ka_rf@armka.local` | KA_RF | Ростовский РФ | только ГК своего филиала, может создавать и менять |
| `rka@armka.local` | RKA | Ростовский РФ | только ГК своего филиала |
| `client_manager@armka.local` | CLIENT_MANAGER | Краснодарский РФ | только ГК своего филиала, чтение |
### 4.5 Токен для Swagger и curl
В Swagger (`/docs`) токен подставляется кнопкой **Authorize** (замок справа вверху): вставьте значение `access_token` без слова
`Bearer`, дальше все защищённые ручки отправляют заголовок сами. Кнопка появилась с arm-core #29; если её нет — бэкенд старше, используйте curl:
```bash
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
```
В Swagger: кнопка **Authorize** → вставить токен (без слова Bearer) → все запросы пойдут от этого пользователя.
Чтобы проверить видимость разными ролями, получите второй токен другим email и переавторизуйтесь.
## 5. Как читать ответы
- Успех: 200/201, тело JSON. Удаление — 204 без тела.
- Нет токена или он плохой — 401. Нет права — 403 с текстом `недостаточно прав: требуется <КОД>`.
- Доменные ошибки — объект `{"detail": {"code": "...", "message": "..."}}`: 404 `NOT_FOUND` (в том числе для **чужой**
записи — так задумано, чужое неотличимо от несуществующего), 409 конфликты (`INN_EXISTS`, `LAST_ADMIN`, …),
422 валидация (`INVALID_INN`, `OFFICE_NOT_ALLOWED`, …).
- Ошибки валидации тела (не тот тип поля) — 422 со списком от pydantic.
- Списки — `{"items": [...], "total": N, "limit": 50, "offset": 0}`, `limit` ≤ 200.
Полный каталог кодов — `arm-core/README.md`, раздел «Ошибки API».
## 6. Чек-лист сценариев
Отмечайте: ✅ ожидание совпало / ❌ баг (с номером issue) / ⚠️ вопрос. Пустые поля ожиданий — проверьте сами и
зафиксируйте результат.
### 6.1 Администрирование (UI `/admin`, вход `admin@armka.local`)
Пункт меню «Система → Пользователи и роли». **Известный дефект arm-web#7:** на этом экране не появляются
уведомления (ни об успехе, ни об ошибке), в консоли браузера — `message.error is not a function`, модалка создания при ошибке
остаётся открытой без текста. Пока он не исправлен, в сценариях с ожиданием «уведомлением» (A3, A6, A8, A10) проверяйте код ответа во
вкладке Network.
| № | Шаги | Ожидание |
|---|---|---|
| A1 | Открыть «Система → Пользователи и роли» | таблица из 7 пользователей, вкладки «Пользователи» и «Матрица прав» |
| A2 | «Создать пользователя»: email `qa1@example.com`, ФИО, филиал, роль KA_RF | пользователь появился; войти им в новой вкладке — получится, роли KA_RF |
| A3 | Создать пользователя с email `qa1@EXAMPLE.com` | ошибка 409 `EMAIL_EXISTS` уведомлением |
| A4 | Создать пользователя с email `x@corp.local` | ошибка 422 (домен `.local` отклоняется — известное ограничение) |
| A5 | У `qa1@example.com` сменить роли на KA_GO + EXPERT | сохранилось; `/me` этого пользователя (новая вкладка) показывает обе роли |
| A6 | Снять роль ADMIN у `admin@armka.local` (у себя), пока он единственный админ | 409 `LAST_ADMIN`, роли не изменились |
| A7 | Выдать роль ADMIN пользователю `qa1@example.com`, затем снять ADMIN у себя | теперь разрешено |
| A8 | Деактивировать единственного активного админа | 409 `LAST_ADMIN` |
| A9 | Матрица прав: роли EXPERT добавить `EXPORT`, сохранить | `/me` эксперта содержит `EXPORT` |
| A10 | Матрица прав: снять `USERS_ADMIN` у ADMIN, когда других админов нет | 409 `LAST_ADMIN` |
| A11 | Войти `expert@armka.local`, открыть `/admin` напрямую | «Нет доступа» / «Раздел доступен пользователям с правом администрирования», запросов к `/admin/*` нет (вкладка Network). Пункт меню виден всем ролям — известное ограничение |
| A12 | После правки своих ролей обновить страницу | профиль в шапке обновился только после перезагрузки (известное ограничение) |
### 6.2 Справочники (Swagger `/docs`)
| № | Пользователь | Шаги | Ожидание |
|---|---|---|---|
| D1 | ka_go | `POST /directories/company-groups` `{"name":"ГК Тест ГО"}` | 201, `code` вида `GK-000001`, `base_regional_office_id` пустой |
| D2 | ka_rf | `POST /directories/company-groups` `{"name":"ГК Тест РФ"}` без филиала | 201, филиал подставлен Ростовский |
| D3 | ka_rf | то же с `base_regional_office_id` Краснодарского РФ (id из `GET /directories/regional-offices`) | 422 `OFFICE_NOT_ALLOWED` |
| D4 | ka_rf | `GET /directories/company-groups` | только «ГК Тест РФ», `total` = 1 (ГК ГО не видна) |
| D5 | ka_rf | `GET /directories/company-groups/{id ГК Тест ГО}` | 404, тело такое же, как для случайного uuid |
| D6 | ka_go | `GET /directories/company-groups` | обе ГК |
| D7 | ka_go | `PATCH` ГК: сменить имя | 200; `GET .../history` — одна запись с прежним именем и `valid_to` |
| D8 | ka_go | `POST /directories/legal-entities` ИП с ИНН из 10 цифр | 422 `INVALID_INN` |
| D9 | ka_go | ЮЛ `{"inn":"7700000001","entity_type":"ЮЛ","name":"ООО Альфа","company_group_id":<ГК ГО>}` | 201 |
| D10 | ka_go | повторно тот же ИНН | 409 `INN_EXISTS` |
| D11 | ka_go | ЮЛ без `company_group_id` | 201, в списке `outside_group = true` |
| D12 | ka_go | `POST .../legal-entities/{id}/aliases` `{"alias":"ООО «Альфа»"}` | 201, `normalized_alias` = `альфа` |
| D13 | ka_go | тот же алиас другому ЮЛ | 409 `ALIAS_TAKEN` с `legal_entity_id` владельца |
| D14 | ka_go | `POST /directories/company-groups/{ГК}/related-groups` kind `gsk`, затем `gsz` | 201 оба, коды `GSK-…`, `GSZ-…` |
| D15 | ka_go | добавить ЮЛ «Альфа» в ГСК и в ГСЗ той же ГК | оба 201 |
| D16 | ka_go | создать вторую ГСК в той же ГК и добавить туда «Альфа» | 409 `MEMBER_OF_OTHER_GROUP` |
| D17 | ka_go | добавить в ГСК ЮЛ из другой ГК | 422 `DIFFERENT_COMPANY_GROUP` |
| D18 | ka_go | `POST .../legal-entities/{Альфа}/move` в другую ГК, `effective_date` сегодня | 200; членства закрыты (`GET .../legal-entities/{id}` — `memberships` пуст); история `MOVE_TO_GROUP` |
| D19 | ka_go | `move` с `effective_date` вчера | 422 |
| D20 | ka_go | `POST .../withdraw` `{"event":"CLOSE","effective_date":сегодня}` | `close_date` заполнен, `company_group_id` пуст |
| D21 | admin | `DELETE /directories/related-groups/{id непустой}` | 409 `GROUP_NOT_EMPTY`; пустой — 204 |
| D22 | ka_go | `DELETE /directories/related-groups/{id пустой}` | 403 (право `DIRECTORY_DELETE` только у ADMIN — так задумано) |
| D23 | expert | любой `POST` в справочники | 403 |
| D24 | любой | `GET /directories/company-groups?limit=201` | 422 |
| D25 | ka_go | `POST /directories/legal-entities` в ГК из D14 с `"aliases":["ООО «Бета»","Бета-Юг"]`, `gsk_id` и `gsz_id` — ГСК и ГСЗ из D14 | 201, в ответе два `aliases` и два `memberships`; в журнале — создание ЮЛ, два события по алиасам и два `MEMBERSHIP_CHANGE` |
| D26 | ka_go | то же с новым ИНН, но в `aliases` алиас «Альфа» (занят в D12) | 409 `ALIAS_TAKEN`; ЮЛ с этим ИНН не появилось |
| D27 | ka_go | `POST` ЮЛ с `gsk_id` = id ГСЗ | 422 `WRONG_GROUP_KIND` |
| D28 | ka_go | `PATCH` ЮЛ из D25 `{"aliases":["ООО «Бета»","Гамма"]}` | 403 «недостаточно прав: требуется DIRECTORY_DELETE» (удаляется «Бета-Юг»); алиасы не изменились |
| D29 | admin | тот же `PATCH` | 200: «ООО «Бета»» с прежним `id`, «Бета-Юг» удалён, «Гамма» добавлен; истории ЮЛ (`…/history`) не прибавилось |
| D30 | ka_go | `PATCH` ЮЛ из D25 `{"gsz_id":null}`, затем повтор | 200, в `memberships` только ГСК; повтор — без новых событий в журнале |
### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger)
Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на стенде **без dev-ГК**: после сброса
(раздел 10) засейте `python -m scripts.seed --dev --no-dev-groups` вместо `make seed-dev`, иначе ГК «Русский Бисквит» и её ЮЛ уже
есть и счётчики будут другими (I2 даст `GK_EXISTS`/`LE_EXISTS`, `offices_created` 0).
| № | Шаги | Ожидание |
|---|---|---|
| I1 | `POST /directories/import` с `dry_run=true` | 200; `rows` 21, `groups_created` 1, `offices_created` 1, `entities_created` 20, `memberships_created` 2, `errors` 1 (строка типа ФЛ → `UNSUPPORTED_TYPE`); в БД и справочниках ничего не появилось |
| I2 | тот же файл `dry_run=false` | 200, тот же отчёт; появились ГК «Русский Бисквит», филиал «Санкт-Петербургский РФ», 20 ЮЛ/ИП, одна ГСК с двумя членами |
| I3 | ещё раз `dry_run=false`, `on_existing=skip` | все строки `LE_EXISTS`/`GK_EXISTS`, число записей не изменилось |
| I4 | csv без колонки «Тип» | 422 `MISSING_COLUMNS`, в ответе список недостающих колонок |
| I5 | файл в кодировке cp1251 (сохранить csv из Excel «CSV (разделители — запятые)») | 422 `UNSUPPORTED_FILE` |
| I6 | пользователем `ka_go` | 403 |
| I7 | полная выгрузка `docs/source/biq/ecp-export-lagal-entity.xlsx`, `dry_run=true` | 200 за несколько секунд; `rows` 8400, `offices_created` 53; есть коды `UNSUPPORTED_TYPE`, `INVALID_INN`, `DUPLICATE_IN_FILE` |
```bash
ADMIN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
-d '{"email":"admin@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN" \
-F file=@../fixtures/directories/ecp-export-russkiy-biskvit.xlsx -F dry_run=true | python3 -m json.tool | head -40
```
### 6.4 Аудит (БД)
```bash
docker compose -f ../docs/dev/docker-compose.yml exec postgres \
psql -U armka -d armka -c "select occurred_at, action, section, object_type, changed_field from dkb.audit_event order by occurred_at desc limit 20"
```
| № | После чего | Ожидание |
|---|---|---|
| L1 | A2, A5, A9 | записи `USER_CHANGE`, `USER_ROLE_CHANGE`, `ROLE_PERMISSION_CHANGE` с `before_value`/`after_value` |
| L2 | D1, D7 | `GROUP_CHANGE`; смена филиала ГК — `ASSIGNMENT_CHANGE` |
| L3 | D9, D12, D18 | `LEGAL_ENTITY_CHANGE`, членства — `MEMBERSHIP_CHANGE` |
| L4 | I2 | одна запись `DIRECTORY_IMPORT` с отчётом в `after_value`; после I1 (`dry_run`) записи нет |
| L5 | A6, D10 (отказы) | записей аудита нет |
### 6.5 Негатив и границы
| № | Шаги | Ожидание |
|---|---|---|
| N1 | запрос без заголовка `Authorization` | 401 |
| N2 | токен для несуществующего email (`nobody@armka.local`) через `POST /dev/token` | 404 `пользователь не найден или отключён`, токен не выдан |
| N3 | тело `PATCH` ГК с неизвестным полем | 422 (поля вне схемы запрещены) |
| N4 | импорт файла больше 20 МБ | 413 |
| N5 | `PUT /consolidations/versions/{id}/members` с 501 `legal_entity_ids` (можно повторить один id) | 422, тело — массив pydantic `detail[] {loc: ["body", "legal_entity_ids"], …}`, без `code` |
| N6 | `POST …/status` с `comment` длиннее 1000 символов | 422, массив pydantic с `loc: ["body", "comment"]` |
| N7 | `GET …/osv?q=<501 символ>` и `GET …/osv/export?q=<501 символ>` | 422, массив pydantic с `loc: ["query", "q"]`; 500 символов — 200 |
| N8 | `DELETE /directories/legal-entities/{id}/aliases/{alias_id}` для уже перенесённого алиаса | 404 `NOT_FOUND`, записи аудита об удалении нет |
### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)
Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), файлы из `../fixtures/osv/`. Выполняйте на
свежем стенде (раздел 10) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до
`PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды). В UI загрузка, опрос статуса,
дубликат и подтверждение ОСВ — W12–W17 (раздел 8); удаления файлов и автозамены в UI нет.
Подготовка: пользователь `ka_go` (права `DOCUMENT_EDIT`, `DOCUMENT_VIEW`, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ —
`АО "Русский бисквит"`, ИНН `3528052718`. После `make seed-dev` оно уже есть («РУССКИЙ БИСКВИТ, АО» с алиасом под шапку):
`GET /directories/legal-entities?q=бисквит` — берите его `id`; создавать как в D9 нужно только на стенде без dev-ГК.
Для проверки «чужого филиала» понадобится ГК с филиалом (dev-ГК «Унагранде» — Головной офис, «Агрофирма Межениновская» —
Ростовский РФ).
```bash
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
H="Authorization: Bearer $TOKEN"
# ЮЛ — только на стенде без dev-ГК (после make seed-dev оно есть: возьмите id из GET …/legal-entities?q=бисквит)
LE=$(curl -s -X POST localhost:8000/api/v1/directories/legal-entities -H "$H" -H 'Content-Type: application/json' \
-d '{"inn":"3528052718","entity_type":"ЮЛ","name":"АО \"Русский бисквит\""}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
# загрузка (U1)
curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files" -H "$H" -F 'files=@../fixtures/osv/rb/2024-FY/ОСВ_66 за 2024.xls'
FILE=<file_id из ответа>
# опрос карточки файла (U2): повторяйте до status = PROCESSED
curl -s "localhost:8000/api/v1/files/$FILE" -H "$H" | python3 -m json.tool
DOC=<documents[0].id из карточки>
curl -s "localhost:8000/api/v1/documents/$DOC" -H "$H" | python3 -m json.tool
curl -s "localhost:8000/api/v1/documents/$DOC/entries" -H "$H" | python3 -m json.tool
curl -s -X POST "localhost:8000/api/v1/documents/$DOC/confirm" -H "$H"
curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n'
```
| № | Пользователь | Шаги | Ожидание |
|---|---|---|---|
| U1 | ka_go | загрузить `ОСВ_66 за 2024.xls` для ЮЛ «Русский бисквит» | 201, `accepted` из одного элемента (`file_id`, `job_id`, `name`), `rejected` пуст; сразу после загрузки `GET /files/{id}` — `status` `UPLOADED` или `PROCESSING` |
| U2 | ka_go | опрашивать `GET /files/{file_id}` (раз в 2–3 с) | не позднее чем через ~15 с `status = PROCESSED`, `job.status = done`, `job.error_code` пуст; `documents` — один: `document_type = OSV`, `osv_account_code = "66"`, `period_year = 2024`, `period_quarter = Q4`, `is_cumulative = true`, `status = CONFIRMED` (автоподтверждение, OQ-157), `counterparty_check = MATCH`, `period_check = UNKNOWN` |
| U3 | ka_go | `GET /documents/{id}` | `locator = sheet:Лист_1`, `file_id` = файл из U1, `group_schema` с группировками листа, `warnings` пуст, `confirmed_by` пуст (документ подтверждён системой) |
| U4 | ka_go | `GET /documents/{id}/entries` | `total = 9`; первая строка — `account_code = "66"`, `level = 0`, `parent_entry_id` пуст; строка «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» под `66.03` имеет `parent_entry_id` = id строки `66.03`; строки «Итого» нет; суммы — десятичные строки как в Excel (сальдо на конец счёта 66: кредит `54.64`) |
| U5 | ka_go | загрузить тот же файл ещё раз | 422, `rejected[0].code = DUPLICATE_FILE`, `rejected[0].file_id` — файл из U1; новых файлов нет |
| U6 | ka_go | загрузить `.docx` (или `.xlsx`, переименованный из `.xls`) | 422, `rejected[0].code = UNSUPPORTED_FILE` |
| U7 | ka_go | загрузить `ОСВ_67 за 2024.xls` и `.docx` одним запросом (`-F 'files=@…' -F 'files=@…'`) | 201, один в `accepted`, один в `rejected` с `UNSUPPORTED_FILE` |
| U8 | ka_go | `POST /documents/{id}/confirm` для документа из U1 (уже `CONFIRMED` автоматически) | 409 `ALREADY_CONFIRMED`; ручное подтверждение `NEEDS_CHECK`-документа — в W17 (API) |
| U9 | ka_go | `DELETE /files/{file_id}` файла из U1 (документ автоподтверждён, к версии не привязан) | 204; `GET /files/{id}` — 404; документы и строки удалены |
| U10 | ka_go | для того же ЮЛ загрузить `ОСВ_60 за 2024.xls` (rb) и дождаться `PROCESSED`; затем `../fixtures/osv/schastl/2024-FY/Оборотносальдовая ведомость по счету 60 за 2024 г..xlsx` | второй файл — `PROCESSED`, `version_no = 2` (если оба документа 60 / 2024 / Q4); первый — `SUPERSEDED`, `superseded_by_file_id` = второй; у документа второго файла `counterparty_check = MISMATCH`, `status = NEEDS_CHECK` (шапка — другая организация) |
| U11 | ka_go | `GET /legal-entities/{id}/files`, затем с `include_superseded=true` | в первом — только актуальные файлы; во втором — и заменённый `SUPERSEDED` |
| U12 | ka_go | `DELETE` второго файла из U10 (заменил первый) | 409 `FILE_SUPERSEDES_OTHERS`; `DELETE` первого (`SUPERSEDED`) — 204, далее `GET /files/{id}` — 404 |
| U13 | ka_go | `POST /documents/{id}/confirm` для документа первого файла из U10 (`SUPERSEDED`), до удаления в U12 | 409 `FILE_SUPERSEDED` |
| U14 | expert | `POST .../files` | 403 (нет `DOCUMENT_EDIT`); `GET /files/{id}` — 200 (есть `DOCUMENT_VIEW`) |
| U15 | client_manager | `GET /files/{id}` файла ЮЛ другого филиала | 404, тело как у случайного uuid |
| U16 | ka_go | остановить tabular-parser, загрузить новый файл | `status` остаётся `UPLOADED` (ack нет — в `PROCESSING` файл не переходит) ~2 мин (3 повтора по 30 с), затем `ERROR`, `job.error_code` заполнен; `DELETE` такого файла — 204 |
| U17 | ka_go | загрузить ОСВ ЮЛ в консолидацию `C` (`POST /consolidations/$C/legal-entities/$LE/files`), у которой версия `V` в статусе «Корректировка данных» (`DATA_CORRECTION`); после `PROCESSED` (документ автоподтверждён) — `DELETE /files/{file_id}` | 204; `FILE_DELETE.after_value.unbound_version_ids` содержит `V`; статус версии `V` не меняется; `GET /files/{id}` — 404 |
| U18 | ka_go | то же, но версия `V` переведена на согласование (`CONSOLIDATION_RF` и далее, см. §9.1) | 409 `FILE_HAS_CONFIRMED_DOCUMENTS`; файл и привязка на месте |
Аудит (6.4 — запрос из psql): после U1 — `FILE_UPLOAD` и `DOCUMENT_CONFIRM` с пустым `user_id` (автоподтверждение); W17 — `DOCUMENT_CONFIRM` с пользователем; U10 — `FILE_REPLACE` с пустым `user_id`
(системное событие) и `description`, где перечислены документы заменённого файла; U12 — `FILE_DELETE` с карточкой файла
в `before_value`. Смены статусов обмена (`PROCESSING`, `PROCESSED`, `ERROR`) аудитом не фиксируются — так задумано.
### 6.7 Проверка парсинга ОСВ (xlsx/xls/xlsb) на произвольных файлах
Парсер (`tabular-parser`) не привязан к координатам: лист ОСВ, заголовок таблицы, колонки сумм, колонка ИНН и иерархия
строк ищутся по содержимому (`05-domain-rules.md` §1.0). Поэтому фикстуры РБ и Счастливочной — только примеры, и
основная задача тестирования — **файлы других форматов**: иные конфигурации 1С, другие учётные системы, файлы после
ручной правки бухгалтером. Два способа прогнать файл.
**Способ А — через стенд** (как в 6.6): загрузить файл на ЮЛ, дождаться `PROCESSED`, смотреть `GET /documents/{id}` и
`…/entries`. Так проверяется весь путь, включая маппинг в ядре.
**Способ Б — парсер без стенда** (быстрее, виден «сырой» результат парсера до ядра; нужен клон `tabular-parser` и
`make venv` в нём):
```bash
cd ../tabular-parser/api
ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.xlsx" > result.json; echo "exit=$?"
```
Код возврата: `0` — `done`, `1` — `needs_check` (контрольные суммы не сошлись, причины в `metrics.warnings`), `2` — ошибка
(`error.code`: `INVALID_DOCUMENT` — не найден лист ОСВ, `INVALID_FORMAT` — нет заголовка таблицы или обязательных групп
сумм, `UNREADABLE`, `UNSUPPORTED_FORMAT`, `TIMEOUT`), `3` — файл не найден. Для расширения не из `.xls/.xlsx/.xlsb`
добавьте `--file-type XLSX`. В `result.json` — то же, что ядро кладёт в `document`/`osv_entry`: `documents[].header`
(организация, ИНН, счёт, период, нарастающий итог, БУ/НУ, валюта), `group_schema`, `rows[]` (уровень, тип строки,
счёт, контрагент, ИНН, шесть сумм, `extra`), `checks`, `metrics.warnings`.
**Что сверять с Excel (чек-лист на каждый файл).** Откройте файл рядом с результатом и проверьте:
| № | Что | Ожидание |
|---|---|---|
| P1 | Лист | найден каждый лист с текстом «Оборотно-сальдовая ведомость» (один `documents[]` на лист, `locator = sheet:<имя>`); листы без ОСВ (справки) пропущены с `warnings` `skipped: …`, а не ошибка; листы форм ББ/ОФР — отдельные документы, см. «Формы ББ/ОФР» ниже |
| P2 | Шапка | `organization_name`, `organization_inn` (если есть в шапке), `account_code_raw`, `period_text` → `period_year`, `period_quarter`, `is_cumulative` соответствуют тексту над таблицей. Поле не найдено → `null` + предупреждение, не ошибка |
| P3 | Период | «1 квартал» → `Q1`, нарастающий; «2 квартал» → `Q2`, квартальный; «полугодие», «6 месяцев», «январь – июнь» → `Q2`, нарастающий; «9 месяцев», «3 квартал» → `Q3`; «2024 г.», «12 месяцев» → `Q4`, нарастающий; римские кварталы («I кв.») тоже. Диапазон месяцев: «Апрель – Июнь 2025 г.» → `Q2`, квартальный; **скользящий** «Октябрь 2024 г. – Сентябрь 2025 г.» → `period_year = 2025`, `period_quarter` и `is_cumulative` — `null`, в `warnings` «не с начала года» (ядро такой документ к версии не привяжет, OQ-131). Период отдельной ячейкой без «за» («9 месяцев 2025 г.» в строке «Бухгалтерский \| …») тоже распознаётся |
| P4 | Колонки сумм | шесть сумм разнесены верно: сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт. Проверьте 2–3 строки с ненулевыми значениями во всех шести колонках и строку «Итого» |
| P5 | Иерархия | уровни строк (`level`, `parent_row_number`) повторяют дерево в Excel: счёт → субсчёт → контрагент → договор. Стратегия — в `warnings` `hierarchy=outline|indent|account_pattern|sums`; если `sums` — дерево восстановлено по равенству сумм, посмотрите внимательнее |
| P6 | Типы строк | `group_type`: `ACCOUNT` (код счёта), `COUNTERPARTY`, `CONTRACT`, `DEPARTMENT`, `OTHER`, `TOTAL` (строка «Итого»; в 1С бывает вторая — «Итого развернутое», обе `TOTAL` уровня 0, первая — основная). Строки «Вал.», «Кол.», «USD» → `data_type` `CURRENCY`/`QUANTITY`; «НУ» → `NU` |
| P7 | ИНН | если в таблице есть колонка ИНН (по заголовку или по содержимому, когда ≥ 80 % значений — 10/12 цифр), `counterparty_inn` заполнен у контрагентов и **не** заполнен у счетов и договоров; КПП и номера договоров за ИНН не принимаются. Без колонки ИНН: ИНН берётся из **хвоста названия** («Полиграф, 3528315710», «(ИНН 7700000000)», «ИНН: …») — `counterparty_name` без ИНН и разделителя, `row_value` как в файле; **строка только с ИНН над названием** склеивается со следующей строкой того же уровня и из `rows` исчезает (нумерация `row_number` с пропуском — это нормально); ячейка «ИНН/КПП» даёт ИНН. Приоритет: колонка → строка выше → хвост названия |
| P8 | Числа | суммы — строки с двумя знаками (`"1234567.89"`); текстовые числа «1 234 567,89», «(1 234,00)», «—» разобраны как число, отрицательное, `null`; пустая ячейка — `null`, не `0` |
| P9 | Контрольные суммы | `status = done`: сальдо строки сходится (начало + обороты = конец), «сумма детей = родитель», «Итого» = корню **или** сумме строк уровня 0 (несколько корней 58.01/58.03 без строки «58» — вариант 2, Новоросметалл/Агрокомплекс). `needs_check` при расхождении, в `checks` — какие строки; «подгонки» под сходимость быть не должно |
| P10 | Лишние колонки | «КПП», «Код», «Количество» и прочие попали в `extra` строки, суммы не сдвинулись |
| P11 | Шапка без меток | ОСВ без строки «по счету …» и с колонками «Счет: Код / Наименование» (ОСВ 63 Новоросметалл): счёт берётся из первой строки-счёта таблицы (в `warnings` об этом сказано), период и тип данных («Бухгалтерский») — из отдельных ячеек; строка сумм без подписи принимается за «Итого» с предупреждением |
**Реальные нестандартные файлы** — `../fixtures/osv-dkb/` (пакет ДКБ 06.10.2026: шесть организаций, многолистовые книги,
вариант 2 «сумма», скользящий период, 50 тыс. строк). Ожидаемый статус каждого листа — манифест
`tabular-parser/api/tests/osv_dkb_expectations.yaml` (данные с причиной: чего там нет, должно быть `done`); golden-снимки есть у 22 файлов
до 1 МиБ (`golden/expected_osv_parse/osv-dkb/`), три книги Агрокомплекса по 9–14 МБ проверяются только инвариантами.
`../fixtures/analysis-reports/` («Анализ счета») — ожидается `INVALID_DOCUMENT`. **Готовые кейсы синтетики** — `../fixtures/osv/synthetic/s01…s19` с описанием каждого файла и
ожиданием в `../fixtures/osv/synthetic/README.md`: шапка варианта 2 («Период: …», «Субконто»), уровни без outline (только
отступами, только по кодам счетов), строки валюты и количества, числа текстом, два листа ОСВ и посторонний лист, нет
сальдо на начало и свёрнутое сальдо на конец, колонка КПП, названия субсчетов, сломанная сумма (`needs_check`), повтор
заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков, ИНН в названии / строкой выше / «ИНН/КПП»
(s17–s19). Прогоните их способом Б и сверьте с README.
**Формы ББ/ОФР (Срез 2b).** Та же команда CLI (способ Б) разбирает и формы: в `documents[]` лист формы — отдельный документ
(`document_type` = `BALANCE` / `INCOME` / `DISCLOSURE`, ОСВ идут первыми), с `header`, `columns[]`, `lines[]`, `checks`.
Что смотреть: `document_type` соответствует форме на листе; `columns[].period_date` — даты колонок слева направо (для ОФР —
конец периода); `checks.relations_failed` (жёсткие соотношения §7 с `expected`/`actual` по колонкам — причина `needs_check`),
`checks.soft_relations_failed` (с контрактом 2.0 `duplicate_line_codes` нет: повтор кода законен, все строки в `lines[]`);
у строк — `line_status` (`ok` / `detail` / `unreadable` / `missing`), `row_index`, `ref`, `needs_review`; у документа — `status`
и `warnings[]`, у файла — `metrics.warnings[]`: предупреждения — структуры `{code, severity, message, …}` (коды и тексты —
[`qa-statements-v2.md`](qa-statements-v2.md) §4.2), например `FRACTIONAL_IN_THOUSANDS` («единицы «тыс. руб.», но N из M значений
с дробной частью»: значения ББ/ОФР из Excel берутся как показывает числовой формат ячейки, OQ-138, #42; для xlsb — как в файле
плюс `XLSB_NO_FORMATS`, для неразобранных форматов — `NUMBER_FORMAT_UNPARSED`), `CODE_COLUMN_NOT_FOUND`, `NO_ORGANIZATION_NO_INN`
(страница-продолжение без шапки), `NUMBERS_OUTSIDE_TABLE` (числа правее таблицы, Р-44). Ожидания по реальным файлам — `../tabular-parser/api/tests/statements_manifest.yaml` и
таблица «файл → статус» в README `tabular-parser`; файлы — `../fixtures/statements-xlsx/` (синтетика `st01…st07`).
Постраничные xls из 1С (страница 2 отдельным файлом, ББ только актив) дают `needs_check` — это штатно (OQ-138, OQ-140), не баг:
с контрактом 2.0 соотношение с пустой стороной считается (`1600 = 1700` при отсутствующей 1700 или 1600 — нарушение).
`done` получают формы, где жёсткие соотношения §7 сходятся: Межениновская, НПЗ, ОФР Унагранде (страница 1); мягкие расхождения
(в пределах допуска) статус не меняют. Для xlsb числовой формат не читается (значения как в файле), поэтому при шапке «тыс. руб.»
и рублёвых значениях возможно `needs_check` или расхождение в пределах допуска округления — это следствие правила, не баг.
**Как делать свои файлы.** Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист:
- сдвинуть таблицу: вставить 5–10 пустых строк и 2–3 колонки слева/сверху; перенести шапку организации ниже или выше;
- переименовать лист; добавить лист «Справка» перед листом ОСВ; положить две ОСВ по разным счетам на разные листы;
- убрать колонку ИНН совсем; вставить колонку «КПП» между ИНН и суммами; переставить блоки «Обороты» и «Сальдо на конец»;
- разбить заголовок таблицы на 1–4 строки с объединёнными ячейками; заменить «Сальдо на начало периода» на «Нач. сальдо»,
«Дебет/Кредит» на «Дт/Кт»;
- убрать группировку outline (скопировать значения в новую книгу) — уровни должны восстановиться по отступам, затем по кодам
счетов; убрать и отступы — по шаблону кодов и по суммам;
- суммы текстом с пробелами и запятой, отрицательные в скобках, прочерки вместо нулей;
- сохранить как `.xls`, `.xlsx`, `.xlsb` (xlsb — обязательно из Excel, см. ниже);
- сломать одну сумму на 1 рубль — ожидается `needs_check` с указанием строки, не `done`.
**Что не считать багом (известные границы):**
- Заголовок таблицы без групп «Обороты» или «Сальдо на конец» → `INVALID_FORMAT`: без них ОСВ не ОСВ. Отсутствовать может
только «Сальдо на начало».
- Заголовок с незнакомым синонимом («Нач. остаток») даёт `needs_check`, суммы уходят в `extra`. Это не ошибка кода, а
словарь: присылайте файл, синоним добавляется строкой в `rules/osv_headers.yaml`.
- ИНН внутри текста контрагента («Полиграф, 3528315710»), склеенный «ИНН/КПП» и отдельная строка ИНН рядом с названием (любая из
восьми раскладок: над / под, в дереве / соседом, с суммами / без; `s20`–`s28`, OQ-163, arm-tabular-parser#35) парсер
извлекает: `counterparty_inn` заполнен, `counterparty_name` без ИНН, исходный текст — в `row_value`. Без подсказки в шапке
строка считается строкой ИНН только при верной контрольной сумме; неверная — остаётся строкой, `needs_check`.
- ИНН в колонке и ИНН в хвосте названия различаются — берётся колонка, предупреждения нет; строка-ИНН над счётом или договором
(не контрагентом) склеивается, но с предупреждением «ИНН … отнесён к строке M, но она не контрагент — проверьте» и `needs_check`
(arm-tabular-parser#18, A9-2/A9-3; #35).
- Счёт 91 (и 99): строки аналитики несут только обороты, сальдо закрыто на 91.09 → `needs_check` на каждой ОСВ 91. Решение
владельца по OQ-132 — исключить 91 и 99 из тождества строк (список в `rules/*.yaml`); до реализации (arm-tabular-parser#11
п. 2) `needs_check` — ожидаемое поведение, не баг. Аналитика без сальдо на конец (магазины Агрокомплекса в 62/76) — тоже `needs_check` по данным файла.
- Скользящий период даёт `period_quarter = null` (см. P3) — это не ошибка парсера; что с ним делать, решает ядро (OQ-131).
- xlsb читается без outline level и форматирования: уровни только по отступам пробелами и кодам счетов. Настоящего xlsb из
Excel в фикстурах нет (OQ-102) — если у вас есть Excel, сохраните `s02_no_outline_indent.xlsx` как xlsb и прогоните.
- Строка «Итого» нужна: без неё `needs_check`.
- Файл больше 50 МиБ, распакованный xlsx больше 500 МиБ или разбор дольше 120 с → `UNSUPPORTED_FORMAT` / `TIMEOUT`.
- Парсер не знает ядра: признак «нарастающая» для ОСВ за Q2–Q4 ядро перепроверяет по данным Q1 (6.6, `is_cumulative`), ОСВ за
Q1 ядро считает и нарастающей, и квартальной (OQ-124).
Баг парсера — это: суммы в не тех колонках, потерянные или задвоенные строки, неверный уровень при наличии outline или
отступов, `done` при реально не сходящихся суммах, падение (`RECOGNITION_FAILED`) на любом файле. К багу прикладывайте
файл (или его обезличенную копию) и `result.json`.
### 6.8 Распознавание PDF: scan-gateway и эмулятор ИС Сканирование (Срез 3a)
PDF и изображения обрабатывает `scan-gateway`; ядро с Среза 3b маппит формы из PDF (`statementDocument`) в документы так же, как из Excel
(раздел 6.9), но на стенде это требует профиля `services` с эмулятором, поэтому здесь сервис проверяется отдельно. Сервис и нормализатор проверяются отдельно: поднимите профиль `services`
(`scan-gateway` и `scan-emulator` в `docs/dev/docker-compose.yml`) и пройдите сценарий из README `scan-gateway` («Полный круг против compose и эмулятора»:
файл в MinIO → `files.img_uploaded` в NATS → ack и итоговый конверт).
Что загружать на стенде: **PDF из `fixtures/ocr` — `aep_3.pdf` и `aep_6.pdf`**. Эмулятор узнаёт их **по содержимому**
(sha256), а не по имени: шлюз кладёт файл в горячую папку как `<uuid>.<ext>`, и имя фикстуры в нём не сохраняется.
Файл без пары по содержимому получает ответ-ошибку `INVALID_DOCUMENT` («нет пары по содержимому»), если не задан
`SCAN_EMULATOR_DEFAULT` (в compose по умолчанию пуст); при заданном `SCAN_EMULATOR_DEFAULT` такой файл получает набор по
умолчанию. Исключение — PDF из `fixtures/ocr`, для которых нет JSON-набора (`aep_4`, `aep_7`, `aep_9`, `aep_30`–`aep_34`):
они совпадают по хешу, но «нет пары» и **остаются `INVALID_DOCUMENT` даже при заданном default** (OQ-151).
| Файл | Ожидание |
|---|---|
| `aep_3.pdf` | `needs_check`, два документа (`BALANCE`, `INCOME`), контракт 2.0 (эталон `fixtures/ocr/expected_normalize/aep_3.json`). `metrics.warnings` — `JSON_FAILED` (`error`: второй файл набора `<job_id>_ofr.json` — ответ-ошибка ИС, штатно). ББ (66н): 42 строки — 34 `ok`, 4 `unreadable`, 1 `missing`, 3 `detail`; 24 нарушения §7; предупреждения `HEADER_ROW_NOT_FOUND`, `REPORT_DATE_UNCERTAIN` (`error`), `INN_UNCERTAIN`, `ORGANIZATION_UNCERTAIN` (`info`). ОФР: 17 строк (14 `ok`, 1 `unreadable`, 1 `missing`, 1 `detail`), 5 нарушений, те же предупреждения шапки |
| `aep_6.pdf` | `needs_check`, два документа (ФСБУ 4). ББ: 60 строк — 38 `ok`, 2 `unreadable` (без сумм), 20 `detail`; нарушений нет; `INN_NOT_FOUND`, `REPORT_DATE_UNPARSED` (`info`). ОФР: 28 строк (25 `ok`, 3 `detail`), одно нарушение `2300 = …`; те же предупреждения шапки. Повтор кода 2410 — обе строки `ok` (OQ-152 снят Р-4) |
| `БО 1 кв 2025.PDF`, `Бухгалтерская отчетность 1 кв.25.pdf` | `done`, два документа у каждого, нарушений нет; `ROW_SPLIT` (`info`, склеенная строка разделена); у «БО 1 кв 2025» ещё `REPORT_DATE_UNPARSED` |
| `aep_5.PDF`, `aep_8.pdf` | `error`, `INVALID_DOCUMENT` с сообщением ИС (в фикстурах лежат ответы-ошибки) |
| любой другой файл | `error`, `INVALID_DOCUMENT` эмулятора («нет пары по содержимому»), если не задан `SCAN_EMULATOR_DEFAULT`; PDF без JSON-набора — всегда (OQ-151) |
Что смотреть: `raw/<job_id>.json` (склейка всех файлов набора) и `results/<file_id>/<job_id>/result.json` в MinIO; подписи
колонок разобраны в даты, коды строк, суммы со скобками (минус), `is_recognized`, `string_value` только у неразобранных сумм,
`text` — сырой текст каждой ячейки, `needs_review` у значений, строк и полей шапки. Чистый скан, где все неуверенные значения
покрыты сошедшимися соотношениями §7, получает `done` (так у сканов РБ и Счастливочной за 1 кв. 2025). В dev-compose у шлюза
`SCAN_CLEANUP=true`: после ответа `Output/<job_id>/` и входящий файл удаляются. Что не считать багом: строки с мусорным кодом
и без кода не выбрасываются, а приходят со статусами `unreadable` / `missing` / `detail` (Р-8; восстановления кода по
наименованию нет — OQ-149); при повторе кода остаются все строки (Р-4); сумма с тремя знаками после запятой (`0,125`) —
`amount = null`, текст в `string_value`; `4,200` и `1,234` — разделитель тысяч (4200, 1234); `}` и `]` принимаются как
закрывающая скобка (минус), прочерк «—» — пустое значение. Поведение экрана для этих документов — [`qa-statements-v2.md`](qa-statements-v2.md).
### 6.9 Формы ББ/ОФР: просмотр, правка, акцепт, подтверждение (Срез 3b)
С выкладкой единой нормализации (контракт 2.0, DBML v5.19) ответ формы содержит строки со статусами, `issues[]`,
`edition_lines[]`, `needs_review`, а `PATCH` — части `lines` (код строки, расшифровка, подтверждение кода), `columns` (дата
колонки), `values[].confirmed` и `header.organization_name`; подтверждение — ещё 409 `LINES_UNRESOLVED` и `DUPLICATE_FORM`.
Поля и коды — `frontend-api-guide.md` §5.6.1, сценарии через экран — [`qa-statements-v2.md`](qa-statements-v2.md). F1–F21 ниже
остаются в силе; формы, загруженные до выкладки v5.19, удаляются очисткой (`arm-core/README.md`, «Выкладка v5.19: очистка форм»).
Предусловие (OQ-157): чистая форма после маппинга сразу `CONFIRMED` (`confirmed_by` пуст, `reviewed_by` NULL) и правке не поддаётся (`PATCH` — 409 `DOCUMENT_READONLY`). Поэтому F1–F2 — на чистой форме `1_Форма1_бух_09.xlsx`; затем удалите её (`DELETE /files/{file_id}`, 204) и загрузите копию с нарушением: в Excel сумма строки 1600 первой колонки (30.09.2025) увеличена на 100 (`1600 = 1100 + 1200` нарушено). F3–F10 ведутся на этой копии (`file_id` новой формы); F3 увеличивает сумму от значения копии, F7 возвращает её на верную сумму `1100 + 1200`.
Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), `make seed-dev` (dev-ГК «Агрофирма Межениновская»,
ЮЛ с ИНН `7016006635`, Ростовский РФ — ИНН совпадает с ИНН шапок форм), пользователь `ka_go` (`DOCUMENT_EDIT`, `DOCUMENT_VIEW`,
видит всё) и для негатива `expert` (только `DOCUMENT_VIEW`), `client_manager` (чужой филиал). В UI правка, акцепт и подтверждение
формы из PDF — W19–W28 (раздел 8); комментариев и CSV чек-листа по ЮЛ там нет. Файлы — книги Excel
`../fixtures/statements-xlsx/mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx` (ББ, 3 колонки: 30.09.2025, 31.12.2024, 31.12.2023, 37 строк
с кодом) и `1_Форма2_бух_09.xlsx` (ОФР, 2 колонки). Подробные схемы и коды ошибок — `dev/api-contract-slice-3b.md`; правила —
`context/05-domain-rules.md` §7, §9.3, §9.5. Новый ЮЛ с чужим ИНН для сценария «не совпал контрагент» — создайте через API
справочников (раздел 6.2). Ожидаемое время от загрузки до `PROCESSED` — до 15 секунд.
```bash
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
H="Authorization: Bearer $TOKEN"
LE=$(curl -s "localhost:8000/api/v1/directories/legal-entities?q=7016006635" -H "$H" | python3 -c 'import sys,json;print(json.load(sys.stdin)["items"][0]["id"])')
curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files" -H "$H" \
-F 'files=@../fixtures/statements-xlsx/mezheninovskaya/2025-Q3/1_Форма1_бух_09.xlsx'
FILE=<file_id из ответа>
curl -s "localhost:8000/api/v1/files/$FILE" -H "$H" | python3 -m json.tool # опрос до status = PROCESSED
DOC=<documents[0].id из карточки>
curl -s "localhost:8000/api/v1/documents/$DOC/statement" -H "$H" | python3 -m json.tool
```
| № | Пользователь | Шаги | Ожидание |
|---|---|---|---|
| F1 | ka_go | загрузить `1_Форма1_бух_09.xlsx` для ЮЛ с ИНН 7016006635, дождаться `PROCESSED` | один документ: `document_type = BALANCE`, `osv_account_code` пуст, `period_year = 2025`, `period_quarter = Q3`, `counterparty_check = MATCH`, `comments_count = 0`, `status = CONFIRMED` (автоподтверждение: `confirmed_by` пуст, `reviewed_by` NULL) |
| F2 | ka_go | `GET /documents/{id}/statement` | 200; `edition = fsbu4`; `document.reporting_date = 2025-09-30`; `columns` — 3 (у первой, 30.09.2025, `is_current = true`; с контрактом 2.0 `column_index` — номер столбца листа: 12, 16, 18); `lines` — 37 строк из файла (`row_index` заполнен, `line_status = ok`) и 2 достроенные шаблонные (`id = null`, `origin = template`), по `line_code`, у каждой `values` на все колонки; суммы — строки; `header.inn.value = "7016006635"`, `confidence = "100.00"`, `modified = false`; `checks.failed`, `soft_failed` пусты, `accepted = null`; в `issues` одна запись `NUMBERS_OUTSIDE_TABLE` (`info`: числа в столбцах U–W правее таблицы не учитываются, Р-44) |
| F3 | ka_go | `PATCH …/statement` `{"values":[{"line_id":…,"column_id":…,"amount":"<сумма строки 1600 первой колонки, увеличенная на 100>"}]}` (`line_id`/`column_id` — из `GET …/statement` файла-копии, не из F2) | 200; у ячейки `amount` новая, `original_amount` = прежняя, `is_modified = true`, `modified_by` = пользователь; `checks.failed` непуст (нарушено `1600 = 1100 + 1200` и/или `1600 = 1700`), `document.status = NEEDS_CHECK`; в `audit_event` одно `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`) с `before`/`after` ячейки (6.4) |
| F4 | ka_go | `POST …/confirm` | 409 `CHECKS_NOT_PASSED`, `detail.failures[]` — нарушения (`column_index`, `relation`, `expected`, `actual`) |
| F5 | ka_go | `POST …/statement/checks/accept` `{"comment":"округление в форме клиента"}` | 200; `checks.accepted = {by, at, comment}`, `checks.failed` по-прежнему непуст; статус документа пересчитан (`RECOGNIZED`, если других причин `NEEDS_CHECK` нет); `POST …/confirm` теперь проходит (но F11 делайте после возврата значений) |
| F6 | ka_go | снова `PATCH` той же ячейки другой суммой | 200; `checks.accepted = null` (акцепт сброшен правкой) |
| F7 | ka_go | `PATCH` той же ячейки на верную сумму (`1100 + 1200` по строкам 1100 и 1200 этой формы) | 200; `checks.failed` пуст; `document.status = RECOGNIZED` (ручное подтверждение — F11); `original_amount` остаётся сломанной суммой из копии, `is_modified = true`; `PATCH` с пустым телом `{}` — 200, новых записей `RECOGNIZED_DATA_EDIT` нет |
| F8 | ka_go | `PATCH` `{"header":{"inn":"123"}}` и `{"header":{"inn":null}}`; `{"values":[{"line_id":"<случайный uuid>","column_id":"<случайный uuid>","amount":"1"}]}` | 422 `INVALID_INN` (оба случая); 422 `UNKNOWN_CELL`; данные не изменились |
| F8a | ka_go | `PATCH` `{"values":[{"line_code":"1600","column_id":"<реальный column_id из F2>","amount":"1"}]}` (строка 1600 в форме есть; `column_id` должен быть настоящей колонкой формы, иначе сработает проверка колонок с другим текстом); затем то же с `"line_code":"9999"` (вне справочника) | 422 `UNKNOWN_CELL` в обоих случаях: для 1600 `detail.message` — «строка уже есть в форме — передайте line_id», для 9999 — «строки с таким кодом нет в форме этой редакции»; данные не изменились |
| F8b | ka_go | Новая строка: в API нет операции удаления строки формы — проверять на документе без 1700 (файл «РБ 1 кв. 2025» из прогона q1-2025-run, OQ-156; не фикстура Межениновской) или вручную: `GET …/statement` — строка 1700 есть, `origin = template`, `id = null`; `PATCH` `line_code = "1700"` с суммой | 200; строка получила `id`, `origin = template` (в БД `line_code_original_value IS NULL`; в `StatementOut` это поле не отдаётся), проверки пересчитаны; очистка (`amount: null`) — строка снова пустая, нарушений по ней нет |
| F9 | ka_go | `PATCH` `{"header":{"report_date":"2025-06-30"}}`, затем вернуть `"2025-09-30"` | период пересчитывается: `period_quarter = Q2` (`reporting_date = 2025-06-30`), `header.report_date.modified = true`, `original_value` не меняется (после возврата на `2025-09-30` — `Q3`, `modified` остаётся `true`); дата не на конец квартала (`2025-09-15`) — 200, `period_quarter = null`, статус `NEEDS_CHECK`, `POST …/confirm` — 409 `PERIOD_REQUIRED` (поправьте дату — подтверждение проходит) |
| F10 | ka_go | `POST …/accept` без нарушений | 409 `NOTHING_TO_ACCEPT` |
| F11 | ka_go | `POST …/confirm` для формы в `RECOGNIZED` после F7 и F9 (ручное подтверждение) | 200 `DocumentOut`, `status = CONFIRMED`, `confirmed_by`/`confirmed_at`; повтор — 409 `ALREADY_CONFIRMED`; `PATCH`/`accept` — 409 `DOCUMENT_READONLY`; в БД у шапки `reviewed_by/at` заполнены. Для автоподтверждённой чистой формы (F1, до удаления) `POST …/confirm` — 409 `ALREADY_CONFIRMED`, `confirmed_by` пуст |
| F12 | ka_go | загрузить `1_Форма2_бух_09.xlsx` тому же ЮЛ, дождаться `PROCESSED`; `GET …/statement` | `document_type = INCOME`, `period_quarter = Q3`, 2 колонки, `edition = fsbu4`; у ББ и ОФР поле `checks.cross_form` — либо `null`, либо нарушение `1370 − 1370(начало года) = 2400` (мягкое, не блокирует подтверждение); статус ОФР — `CONFIRMED` (автоподтверждение; мягкие проверки его не отменяют) |
| F13 | ka_go | `GET /files/{file_id}/content?disposition=inline` и без параметра | 200, тело байт в байт равно загруженному `.xlsx`, `Content-Type` xlsx, заголовки `X-Content-Type-Options: nosniff` и `Cache-Control: private, no-store`; `inline` — ещё `Content-Security-Policy: sandbox`, `Content-Disposition: inline; filename="file.xlsx"; filename*=UTF-8''<имя>` (кириллица в percent-кодировке); `disposition=foo` — 422 |
| F14 | ka_go | `POST /documents/{id}/comments` `{"text":"нет подписи руководителя"}`; `{"text":" "}`; затем `GET …/comments` и `GET /files/{file_id}` | 201 с `author.full_name`; пустой после обрезки текст — 422; в списке один комментарий; в карточке файла и `GET /documents/{id}` у документа `comments_count = 1`; комментарий к `CONFIRMED` документу разрешён |
| F15 | ka_go | `GET /users/brief?ids=<uploaded_by файла>,<случайный uuid>`; с 201 id; с `ids=abc` | 200, один элемент `{id, full_name, is_active}`; 422 `TOO_MANY_IDS`; 422 `INVALID_IDS`; то же под `expert` (без `USERS_ADMIN`) — 200 |
| F16 | ka_go | `GET /directories/legal-entities?q=7016006635` после F1 и F12 | `documents_count = 2` (ББ и ОФР) |
| F17 | ka_go | в ГК «Агрофирма Межениновская» завести второе ЮЛ и ГСК из двух ЮЛ (раздел 7, «Подготовка»), создать версию за 2025 Q3; убедиться, что ББ и ОФР ЮЛ с ИНН 7016006635 (файлы F1/F12) `CONFIRMED` (чистые формы автоподтверждаются; при `NEEDS_CHECK` — проверить значения и подтвердить вручную, F11); `GET …/checklist` и `GET …/checklist?format=csv` | ячейки «Бух. баланс» и «ОФР» ЮЛ — `CONFIRMED`; CSV: файл начинается с BOM (`EF BB BF`), первая строка «Организация;ИНН;Бух. баланс;ОФР;ОСВ …», разделитель `;`, у ЮЛ «Загружен»; `Content-Disposition` с `filename="checklist.csv"` и `filename*=UTF-8''checklist-<ГК>-2025-Q3.csv` |
| F18 | expert | `PATCH …/statement`, `POST …/comments`, `POST …/accept` | 403 (нет `DOCUMENT_EDIT`); `GET …/statement` и `GET …/comments` — 200 |
| F19 | client_manager | `GET …/statement`, `GET /files/{id}/content` документа ЮЛ другого филиала | 404 `NOT_FOUND`, тело как у случайного uuid |
| F20 | ka_go | `GET …/statement` документа `OSV` (раздел 6.6) | 409 `DOCUMENT_NOT_STATEMENT` |
| F21 | ka_go | загрузить второй файл с формой ББ за тот же период (книга Form1, пересохранённая в Excel — другой sha256); `PATCH` по документу первого файла (это копия с нарушением из F3–F10; чистая F1 удалена по предусловию §6.9) | старый файл `SUPERSEDED`; в коде (`statements.py`) проверка `FILE_SUPERSEDED` идёт раньше `DOCUMENT_READONLY`, поэтому ответ — 409 `FILE_SUPERSEDED` даже для подтверждённой формы; `PATCH`/`accept` по его документу — 409 `FILE_SUPERSEDED`; чтение `GET …/statement` и комментарии работают |
Что не считать багом: формы из OCR (PDF) с неуверенными значениями, не покрытыми сошедшимися соотношениями §7, —
`NEEDS_CHECK` (`aep_3.pdf`, `aep_6.pdf`, раздел 6.8), а чистый скан может автоподтвердиться; у форм без колонки кодов документ
создаётся без строк из файла — только шаблонные пустые (OQ-139); две формы одного типа и периода в одном файле хранятся обе, ни
одна не автоподтверждается, подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` (Р-18); страницы одной формы из
разных файлов не склеиваются (OQ-140); строки без кода приходят со статусами `unreadable` / `missing` / `detail` (Р-8, OQ-36
закрыт), и при `unreadable` / `missing` с суммами подтверждение — 409 `LINES_UNRESOLVED`; скачивание файла и CSV в журнал не
пишутся (OQ-153); формулы §7 одни для редакций `66n` и `fsbu4` (OQ-146); формы, загруженные до выкладки v5.19, на локальном стенде
удаляются сбросом (раздел 10) или очисткой `scripts/purge_statements.py` — загрузите файл заново. Известные замечания по формам
2.0 (I-1…I-5, M-1…M-9, F-1, F-2) — [`qa-statements-v2.md`](qa-statements-v2.md) раздел 6.
## 7. Консолидация через API (Срез 4)
Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001`
(раздел 6.6); данные — в `../fixtures/osv/`. Токены: `ka_go` (`CONSOLIDATION_VIEW/EDIT`, видит всё), `ka_rf`
(`CONSOLIDATION_VIEW/EDIT`, только ГК Ростовского РФ), `expert` (только `CONSOLIDATION_VIEW`), `client_manager`
(`CONSOLIDATION_VIEW`, Краснодарский РФ), `admin` (прав на консолидации нет). Подробные схемы запросов и ответов, коды ошибок и
нюансы для фронта — `dev/api-contract-slice-4.md`; Swagger — `/docs`. В UI (раздел 8) есть K1 (W5–W8), загрузка в
версию (K8, W13), чек-лист CSV (W11), свод, пробелы и экспорт (K11, K12 частично, K17 — W29–W35); блокировки, периметра,
статусов и сопоставления (K4–K7, K13–K16, K18–K20) в UI нет.
**Подготовка.** Нужна ГК с ГСК не менее чем из двух ЮЛ (ГК — в Ростовском РФ, чтобы её видел `ka_rf`): создайте через API
справочников (раздел 6.2; ГСК — `POST /directories/company-groups/{id}/related-groups`, участники — `POST
/directories/related-groups/{id}/members`) либо импортом ЕЦП (I2). Для данных РБ: ЮЛ «АО "Русский бисквит"» (ИНН 3528052718) и
«ООО "Пекарня Счастливочная"» (в ГСК обе). Ниже `V` — id версии, `G` — id ГК, `S` — id ГСК.
```bash
curl -s -X POST localhost:8000/api/v1/consolidations -H "$H" -H 'Content-Type: application/json' \
-d '{"company_group_id":"'$G'","related_group_id":"'$S'","year":2024,"quarter":"Q3"}'
# 201: VersionCard; V = ответ.id, C = ответ.consolidation_id, status = INITIATED, reporting_date = 2024-10-01
curl -s -X POST localhost:8000/api/v1/consolidations/versions/$V/lock -H "$H" # 201 LockOut
curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE/files" -H "$H" -F 'files=@…'
curl -s localhost:8000/api/v1/consolidations/versions/$V/checklist -H "$H"
curl -s "localhost:8000/api/v1/consolidations/versions/$V/osv?limit=50" -H "$H"
curl -s localhost:8000/api/v1/consolidations/versions/$V/osv/summary -H "$H"
curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/export" -H "$H" # Content-Disposition
```
| № | Пользователь | Шаги | Ожидание |
|---|---|---|---|
| K1 | ka_go | создать консолидацию ГК/ГСК за 2024 Q3 (команда выше) | 201, `status = INITIATED`, `version_no = 1`, `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false`, `members` = текущие члены ГСК, `lock = null`; повтор — 409 `CONSOLIDATION_EXISTS` |
| K2 | ka_go | то же с `year = 1999`; с ГСК другой ГК; с ГСК из одного ЮЛ; с чужим/несуществующим id ГК | 422 (валидация года); 422 `RELATED_GROUP_MISMATCH`; 422 `PERIMETER_TOO_SMALL`; 404 `NOT_FOUND` |
| K3 | ka_go | `GET /company-groups/{G}/consolidations`, затем с `?in_work_only=true`; `GET …/versions/{V}` | период с версией; карточка с `members[]` (у каждого `checklist[]`), `history[0].from_status = null`; `ka_rf` для ГК вне своего филиала — 404 |
| K4 | ka_go | `POST …/lock`; повтор | 201 `LockOut` (`locked_by`, `locked_by_name`); повтор — 200, блокировка та же |
| K5 | ka_rf (ГК его филиала) | `POST …/lock` на версию, занятую `ka_go` | 409 `LOCKED_BY_OTHER` (`locked_by`, `locked_at`, `locked_by_name`); с телом `{"takeover_reason":"…"}` — 201, `taken_over_from` = id `ka_go`, `ka_go` теряет блокировку; в аудите `CONSOLIDATION_LOCK_TAKEOVER` |
| K6 | ka_go | после K5 `PUT …/members` | 409 `LOCKED_BY_OTHER` (чужая блокировка), без блокировки вообще — 409 `LOCK_REQUIRED` |
| K7 | владелец блокировки | `PUT …/members` с составом ≤ 1 ЮЛ, с ЮЛ другой ГК, с тем же составом | 422 `PERIMETER_TOO_SMALL`; 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`); 200 без изменений и без события `MEMBERSHIP_CHANGE` |
| K8 | владелец блокировки | загрузить ОСВ ЮЛ периметра в консолидацию `C` (раздел 6.6) | 201; версия `INITIATED` → `DOCUMENTS_PROCESSING`; ЮЛ вне периметра — 422 `ENTITY_NOT_IN_PERIMETER`; несуществующая консолидация — 404; `GET /consolidations/$C/legal-entities/$LE/files` — загруженный файл в списке, `consolidation_id = C` |
| K9 | ka_go | после `PROCESSED` и `NEEDS_CHECK` документа — `GET …/versions/{V}`; затем `POST /documents/{id}/confirm` | `NEEDS_CHECK` → версия `DATA_CORRECTION` (если файл загружал пользователь); подтверждение переводит `INITIATED`/`DOCUMENTS_PROCESSING` → `DATA_CORRECTION`; документ привязан к версии (виден в чек-листе: ячейка `CONFIRMED`) |
| K10 | ka_go | `GET …/checklist` до и после подтверждения; для квартальных ОСВ — только Q3 | ячейка ОСВ: `MISSING` → `NEEDS_CHECK`/`RECOGNIZED` → `CONFIRMED`; при неполных кварталах — `missing_quarters`; ОСВ по субсчёту (58.03) не закрывает «ОСВ 58» |
| K11 | ka_go | `GET …/osv`, `…/osv/summary` по данным РБ (раздел «Данные для K11») | строки свода по парам, где покрыты все кварталы `Q1..Qn`; пары без квартала — в `gaps[]` с `missing_quarters`; деньги — строки с двумя знаками; `unmapped_total` > 0 |
| K12 | ka_go | фильтры: `legal_entity_id`, `account_root=60`, `counterparty_type=EXTERNAL`, `unmapped=true`, `match_kind=MANUAL&match_kind=PROPAGATED`, `q=бисквит`, `limit=2001` | отфильтрованные страницы с `total`; `limit=2001` — 422 |
| K13 | ka_go | `GET …/counterparty-candidates?q=счастл&entry_id=<entry_ids[0]>`; `scope=all&q=ab` | список ЮЛ с `counterparty_type`; `recommended` — только при точном совпадении нормализованного имени; `scope=all` с `q` короче 3 — 422 `QUERY_TOO_SHORT` |
| K14 | владелец блокировки | `POST …/osv/entries/{entry_id}/counterparty {legal_entity_id}` для строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» | 200: `row.match_kind = MANUAL` (слать **последнюю** `entry_id` строки свода), `propagated` ≥ 0, `alias_created = true`; в сводке `unmapped_total` уменьшился, у других строк с тем же именем — `PROPAGATED`; в аудите `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE` |
| K15 | владелец блокировки | сопоставить другое имя с алиасом, уже закреплённым за другим ЮЛ; затем с `"move_alias": true` | 409 `ALIAS_TAKEN` (`owners[]`); с `move_alias` — 200, `alias_moved_from` = прежний владелец |
| K16 | ka_go | `entry_id` строки `NU`/`ACCOUNT` или чужой версии | 404 `NOT_FOUND`; без блокировки — 409 `LOCK_REQUIRED` |
| K17 | ka_go | `GET …/osv/export` (в т. ч. с фильтрами) | 200, `Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''…`; файл открывается в Excel: лист «Сводная ОСВ» (суммы — числа, формат `#,##0.00`, число строк как в API) и лист «Пробелы» (от фильтров не зависит); в аудите `EXPORT` |
| K18 | владелец блокировки | `POST …/status {"to_status":"CANCELLED"}` без комментария; с комментарием | 422 `COMMENT_REQUIRED`; 200, `status = CANCELLED`, блокировка снята (`lock = null`), `history` пополнена |
| K19 | ka_go | после K18: `PUT …/members`, `POST …/lock`, `POST …/status`, сопоставление | 409 `VERSION_NOT_EDITABLE`; чтение (`GET …/osv`, экспорт, чек-лист) — 200 |
| K19а | ka_go | после K18 (версия `CANCELLED`, другой версии «в работе» нет): загрузить ОСВ ЮЛ периметра в консолидацию `C` (`POST /consolidations/$C/legal-entities/$LE/files`) | 409 `CONSOLIDATION_NOT_EDITABLE` |
| K20 | владелец блокировки | `POST …/status {"to_status":"INITIATED"}` из `DATA_CORRECTION`; затем `{"to_status":"DATA_CORRECTION"}` и `{"to_status":"VERIFIED"}` | 200, привязки документов сохраняются; ручные `DATA_CORRECTION`/`VERIFIED` — 409 `TRANSITION_NOT_ALLOWED` (`from_status`, `to_status`) |
| K21 | expert, client_manager | `GET …/osv`, `…/checklist`; `POST …/lock`, `PUT …/members` | чтение — 200 (client_manager — только ГК своего филиала, иначе 404); правки — 403 `недостаточно прав: требуется CONSOLIDATION_EDIT` |
| K22 | ka_go | заменить файл (загрузить исправленную ОСВ того же счёта и периода) после подтверждения документа | старый файл `SUPERSEDED`, привязка его документов к версии снята (`FILE_REPLACE.after_value.unbound_version_ids` содержит `V`); новый документ привязывается только после подтверждения |
Данные для K11 (РБ 2024, как в сквозном тесте `test_consolidation_fixtures`): ОСВ 60 и 62 РБ и Счастливочной, ОСВ 76
Счастливочной за Q1, Q2 и Q3 2024 (`../fixtures/osv/…`, подтвердить документы), плюс по одному документу за Q3 (РБ: 01, 58.03,
66, 67, 76; Счастливочная: 01, 02). Для версии Q3 пары с одним Q3 попадают в `gaps` (пропущены Q1–Q2); ОСВ за Q1 (нарастающая
по признаку парсера) подходит версии любого квартала (R-14).
Аудит (6.4): K1 — `CONSOLIDATION_CREATE`; K4 — `CONSOLIDATION_LOCK`; K5 — `CONSOLIDATION_LOCK_TAKEOVER`; K7 — `MEMBERSHIP_CHANGE`
(только при изменении состава); переходы статуса — `CONSOLIDATION_STATUS_CHANGE`; K14 — `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE`;
K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокировка не истекает по времени — освободить её можно только `DELETE …/lock`,
перехватом или аннулированием версии (OQ-119).
### 7.1 Полный прогон на данных РБ за 2024 (скрипт)
Для сводной ОСВ нужны все квартальные файлы двух ЮЛ. Их 22: РБ (`../fixtures/osv/rb/2024-Q{1,2,3}/`, 2 + 2 + 7) и
Счастливочная (`../fixtures/osv/schastl/2024-Q{1,2,3}/`, 3 + 3 + 5). Загружать по одному через Swagger долго, поэтому цикл
(`ka_go`, `$H` из 6.6; `LE_RB` и `LE_SCH` — id ЮЛ «АО "Русский бисквит"» и «ООО "Пекарня Счастливочная"», обе в одной ГСК
ГК из раздела 7; `V` — версия за 2024 Q3, созданная и взятая в работу по K1/K4 **до** загрузки; `C` — её консолидация (`consolidation_id` из ответа K1) — тогда переходы статусов
видны на K8/K9):
```bash
F=../fixtures/osv
for f in "$F"/rb/2024-Q1/* "$F"/rb/2024-Q2/* "$F"/rb/2024-Q3/*; do
curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE_RB/files" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])'
done
for f in "$F"/schastl/2024-Q1/* "$F"/schastl/2024-Q2/* "$F"/schastl/2024-Q3/*; do
curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE_SCH/files" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])'
done
# подождать ~30 с, затем убедиться, что все файлы PROCESSED, и подтвердить все документы
for LE in $LE_RB $LE_SCH; do
curl -s "localhost:8000/api/v1/legal-entities/$LE/files?limit=50" -H "$H" | python3 -c '
import sys,json
for f in json.load(sys.stdin)["items"]:
print(f["status"], f["original_name"], [ (d["id"], d["osv_account_code"], d["period_quarter"], d["status"]) for d in f["documents"]])'
done
# подтверждение: для каждого document id из вывода выше
for DOC in <id1> <id2> …; do curl -s -o /dev/null -w "%{http_code} $DOC\n" -X POST "localhost:8000/api/v1/documents/$DOC/confirm" -H "$H"; done
```
Ожидание после распознавания: чистые документы уже `CONFIRMED` (автоподтверждение, OQ-157); `confirm` для них вернёт 409 `ALREADY_CONFIRMED` — в цикле это ожидаемо; `GET …/versions/{V}` — `status = DATA_CORRECTION`; в чек-листе у РБ ОСВ 60 и 62 `CONFIRMED` с
тремя `document_ids`, у Счастливочной — 60, 62, 76; ОСВ 58 у РБ не `CONFIRMED` (есть только 58.03); остальные ячейки ОСВ
Q3-only — `missing_quarters = ["Q1","Q2"]`.
Если файл загружен **до** создания версии — не страшно: подтверждённые документы подхватываются при создании версии
(«подхват»), только переходы `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` по загрузке тогда не увидеть.
### 7.2 Контрольные цифры сводной ОСВ (версия РБ 2024 Q3)
Значения ниже посчитаны вручную по файлам фикстур и совпадают со сквозным тестом `test_consolidation_fixtures`. Они
проверяют самое важное — **приведение квартальных ОСВ к 9 месяцам** (`05-domain-rules.md` §2): сальдо на начало берётся из
ОСВ за Q1, обороты складываются за Q1 + Q2 + Q3, сальдо на конец — из ОСВ за Q3.
`GET …/osv/summary`:
| Поле | Ожидание |
|---|---|
| `rows_total` | 1385 |
| `unmapped_total` | 1236 до ручного сопоставления — строки без ЮЛ **и** без ИНН из файла (отмена П-1, arm-core #74): строки с ИНН, которого нет в справочнике, сюда не входят; строки Счастливочной с ИНН 3528052718 (`60.01`, `60.02`, `62.01`, `62.02`, `76.06`, `76.АВ`) сопоставлены автоматически. Прежнее значение 1260 устарело: пересчитано по экспорту свода 19.10.2026 (вероятная причина — разбор строк ИНН в ОСВ, arm-tabular-parser #33, #35; не проверялась) |
| `documents_total` | 22 |
| `gaps` | ровно 7: РБ — `01`, `58.03`, `66`, `67`, `76`; Счастливочная — `01`, `02`; у всех `missing_quarters = ["Q1","Q2"]` |
Строки свода (`GET …/osv?legal_entity_id=…&q=…`), суммы в рублях:
| Компания, счёт, контрагент | Сальдо нач. Дт | Сальдо нач. Кт | Обороты Дт | Обороты Кт | Сальдо кон. Дт | Сальдо кон. Кт | Признак |
|---|---|---|---|---|---|---|---|
| Счастливочная, `60.01`, «Русский бисквит», ИНН 3528052718 | — | 4 454 151.93 | 128 058 143.65 | 125 950 450.77 | — | 2 346 459.05 | `GSK`, `match_kind = INN` |
| Счастливочная, `60.02`, «Русский бисквит» | 0 | 0 | 9 745 019.62 | 9 745 019.62 | 0 | 0 | `GSK`; контрагента нет в Q2 — квартал считается нулями |
| РБ, `60.01`, «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ», без ИНН | — | 38 806 758.00 | 307 986 284.00 | 375 490 642.46 | — | 106 311 116.46 | `EXTERNAL` до сопоставления, после K14 — `GSK` |
| Счастливочная, `62.01`, «Русский бисквит» | 40 211 938.00 | — | 398 018 012.46 | 330 517 504.00 | 107 712 446.46 | — | `GSK`; в каждом квартальном файле под `62.01` **две** строки с этим контрагентом — свод их суммирует, `entry_ids` = 6 |
| Счастливочная, `62.02`, «Русский бисквит» | — | 24 100 000.00 | 9 900 000.00 | 0 | — | 14 200 000.00 | `GSK` |
Как проверить любую строку самому: откройте три квартальных файла этого ЮЛ по этому счёту, найдите контрагента под тем же
субсчётом и сложите обороты по трём файлам; сальдо на начало сравните с первым файлом, на конец — с третьим. Тождество
«начало + обороты Кт − обороты Дт = конец» (для кредитового сальдо) должно сходиться: 4 454 151.93 + 125 950 450.77 −
128 058 143.65 = 2 346 459.05. Строка свода содержит `entry_ids` всех трёх исходных записей и `is_cumulative = false`;
`period = "2024 Q3"`.
Строка РБ по `60.01` зеркальна одной из двух позиций Счастливочной под `62.01` (в файлах: обороты 375 490 642.46 /
307 986 284.00, сальдо на конец 106 311 116.46) — внутригрупповые обороты сходятся копейка в копейку. В своде эта позиция
видна только в сумме с соседней (строка `62.01` выше); разнесение по позициям и сопоставление ВГО — Срез 5.
Что ещё проверить руками:
- контрагент, которого нет в одном из кварталов (как «Русский бисквит» на `60.02`): в своде одна строка, пропущенный квартал
дал нули, `entry_ids` — две записи;
- `GET …/osv?match_kind=INN` — только строки с ИНН в файле (у Счастливочной есть колонка ИНН, у РБ нет), `match_stored = true`;
- `GET …/osv?unmapped=true&legal_entity_id=$LE_RB&q=счастлив` — две строки (`60.01`, `62.…`) до сопоставления; после K14 —
ни одной, а `summary.unmapped_total` уменьшился ровно на число изменившихся строк свода (на этих данных — на 4, при
`propagated = 17` записей `osv_entry`: записи кварталов и пар-пробелов 66/76 считаются отдельно);
- пробелы не зависят от фильтров свода: `gaps` одинаковы при любом `GET …/osv/summary`.
### 7.3 Сверка экспорта с API и Excel
1. `GET …/osv/export` без фильтров → `svod.xlsx`; лист «Сводная ОСВ»: число строк без заголовка = `rows_total`, порядок — как
в API (ЮЛ по имени, счёт, номер строки). Суммы — числа с двумя знаками (не текст): выделите колонку «Обороты Дт» и
посмотрите сумму в строке состояния Excel; формат ячейки `#,##0.00`.
2. Найдите строку «Русский бисквит» `60.01` Счастливочной и сверьте шесть сумм с таблицей из 7.2 и с ответом `GET …/osv`.
3. Колонки «Сопоставлено с» и «Способ сопоставления»: для сопоставленных — имя ЮЛ из справочника и `INN`/`MANUAL`/`PROPAGATED`,
для несопоставленных — пусто.
4. Лист «Пробелы»: 7 строк, как в `summary.gaps`; с фильтром `?legal_entity_id=` в экспорте лист «Пробелы» всё равно полный.
5. Экспорт с `?unmapped=true` после полного сопоставления — лист «Сводная ОСВ» только с заголовком, лист «Пробелы» заполнен.
6. Текст контрагента, начинающийся с `=`, `+`, `-`, `@` (сделайте такой в своём файле), в Excel показывается как текст, а не
вычисляется как формула.
### 7.4 Произвольные ОСВ в консолидации другого периода (временный режим)
Документ привязывается к версии, только если период из **шапки ОСВ** подходит периоду консолидации: ОСВ за 1 кв. 2025,
загруженная в консолидацию 2026 Q1, после подтверждения **молча** не попадает ни в версию, ни в свод (OQ-161). Для тестов на
произвольных ОСВ есть временный режим `CONSOLIDATION_PERIOD_FROM_VERSION=true` (arm-core#103): в `api/.env` локально, в контуре —
`OPENBAO__APP__CONSOLIDATION_PERIOD_FROM_VERSION=true`; после смены — перезапуск ядра.
В режиме ОСВ, **загруженная в консолидацию** (через версию, а не с рабочего стола), период шапки которой консолидации **не
подходит**, считается нарастающей за год и квартал консолидации: подтверждение привязывает её, свод, чек-лист и колонка «ОСВ»
периметра её видят. ОСВ «своего» периода режим не трогает — квартальные склеиваются как обычно. Что учесть:
1. **Один файл «чужого» периода на счёт ЮЛ.** У подменённых ОСВ склейки кварталов нет: все они по одному счёту одного ЮЛ —
один слот, в своде будет подтверждённая последней. Грузите одну ОСВ на счёт (лучше нарастающую за весь период).
2. **Подтвердить всё нужное.** Документы `NEEDS_CHECK` (в т. ч. из-за чужого ЮЛ в шапке, `counterparty_check = MISMATCH`) в свод
не идут, пока их не подтвердили.
3. **Документы, подтверждённые до включения режима**, сами не перепривязываются: уберите ЮЛ из периметра и верните (подхват
`PUT …/members` под своей блокировкой версии) либо создайте консолидацию заново. Периметр — не меньше двух ЮЛ
(`PERIMETER_TOO_SMALL`), поэтому при двух участниках меняйте ЮЛ на временное третье той же ГК и обратно:
`{А, Б} → {Б, В} → {А, Б} → {А, В} → {А, Б}` (проверено на стенде 12.10.2026: РБ 2026 Q1 из ОСВ за 1 кв. 2025 — 16 документов
привязаны, свод 1421 строка, суммы равны своду РБ 2025 Q1 из тех же файлов).
4. Карточка документа по-прежнему показывает период из шапки — это не баг. ББ/ОФР режим не затрагивает.
## 8. Сквозной сценарий в UI
Основной путь аналитика по макету: группа компаний → новая консолидация → файлы ЮЛ → проверка ОСВ и форм ББ/ОФР →
сводная ОСВ. Номера в квадратных скобках — экраны макета `docs/mockup/screens/<nn>-*.png` (открывать по одному через
`docs/mockup/INDEX.md`): если не ясно, «так ли должно выглядеть», сравнивайте с ними при окне 1600×1000. Права, коды
ошибок и граничные случаи, которых нет в UI, — в разделах 6–7.
**Предусловия.** Стенд по разделу 4: ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`, `make seed-dev` (ГК «Русский
Бисквит» с ГСК из РБ и Счастливочной), фронт — раздел 4.3. Для 8.5 (PDF) — ещё шлюз и эмулятор ИС Сканирование (из
каталога `arm-core`, как в 4.2):
```bash
docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps scan-gateway scan-emulator
```
Пользователь — `ka_go@armka.local`. Начинайте со свежего стенда (раздел 10): тогда цифры совпадут с 7.2 без поправок.
Подписи кнопок, колонок и сообщений ниже — дословно как в UI.
### 8.1 Навигация и группы компаний [03]
| № | Шаги | Ожидание |
|---|---|---|
| W1 | Войти `ka_go`, посмотреть боковое меню | логотип «АРМ КА», кнопка «+ Новая консолидация»; разделы «Отчётность» (Группы компаний, Монозаемщики), «Справочники» (ГК и организации, ЮЛ вне ГК), «Документы» (Единое хранилище), «Система» (Журнал действий, Пользователи и роли); кнопка «‹» сворачивает меню до иконок. Всё, кроме «Группы компаний» и «Пользователи и роли», — заглушки |
| W2 | «Группы компаний» | крошки «Отчётность › Группы компаний», подзаголовок «4 групп компаний» (после `make seed-dev`: Русский Бисквит, Унагранде, Новороссийский прокатный завод, Агрофирма Межениновская); колонки «Группа компаний», «CRM ID», «Основной РФ», «Организаций» |
| W3 | Поиск «бисквит»; затем фильтр в заголовке «Группа компаний» (сортировка, отметки, «OK») | остаётся «Русский Бисквит»; появляется «Сбросить фильтры»; «Настройка таблицы» скрывает и показывает колонки |
| W4 | В строке «Русский Бисквит» — кнопка «→ Консолидации» | страница группы [34]: «Русский Бисквит», «20 организаций», блок «Консолидации 0 шт.»; блок «Организации» пуст, пока версия не выбрана; «← Назад» возвращает к списку |
### 8.2 Новая консолидация [42]
| № | Шаги | Ожидание |
|---|---|---|
| W5 | «+ Новая консолидация» в меню | модалка «Новая консолидация»: «Группа компаний», «ГСК» (неактивна до выбора ГК), «Год» (текущий и три предыдущих), «Квартал»; «Создать» неактивна |
| W6 | ГК «Русский Бисквит», ГСК «ГСК Русский Бисквит», 2024, «3 квартал» | блок «ЮЛ консолидации»: все открытые ЮЛ ГК, РБ (3528052718) и Счастливочная (3528104476) отмечены и стоят первыми, счётчик «Выбрано: 2 / 20»; поиск «Поиск ЮЛ...» фильтрует список |
| W7 | «Создать» | уведомление «Создана консолидация 2024, 3 квартал»; открывается страница группы с выбранной версией (`?version=<id>` в адресе): строка «2024 · К3 · 1 · Инициация», «Дата создания» — сегодня по Москве, «ГСК» и «Дата верификации» — «—» (так в прототипе); в «Организации» — 2 строки, «Бух.баланс» и «ОФР» — «Отсутствует», «Ответственный: Dev: Аналитик ГО» |
| W8 | Повторить W5–W7 с тем же периодом | уведомление «Консолидация ГК за этот период уже создана» (409 `CONSOLIDATION_EXISTS`), модалка остаётся открытой |
| W9 | В модалке оставить отмеченным одно ЮЛ; снять все | одно ЮЛ — уведомление с текстом сервера (422 `PERIMETER_TOO_SMALL`, K2); ни одного — «Создать» неактивна |
### 8.3 Организации, чек-лист, загрузка файлов [34], [39], [40]
| № | Шаги | Ожидание |
|---|---|---|
| W10 | Кликнуть строку версии в «Консолидации» | строка подсвечена зелёным, адрес `?version=<id>`; блок «Организации»: колонки «Наименование», «Документы», «ОФР», «Бух.баланс», «ОСВ», «ИНН», «Тип»; меню «⋮» строки версии: «Добавить версию» → «Будет позже», «История изменений» → заглушка журнала |
| W11 | «Скачать чек-лист» | файл `checklist-Русский Бисквит-2024-Q3.csv`; содержимое — как в F17 (BOM, разделитель `;`, открывается в Excel) |
| W12 | Клик по строке «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» (или по кнопке «Документы») | справа панель «ПЕРВИЧНЫЕ ДОКУМЕНТЫ» [39]: имя ЮЛ, ИНН 3528104476, кнопка «Загрузить документы», «Документов нет»; крестик справа закрывает панель |
| W13 | «Загрузить документы» → «Выбрать документы» → пять файлов `../fixtures/osv/schastl/2024-Q3/*` → «Загрузить» | модалка «Загрузка документов» [40]: зелёный блок «Принято: 5» с именами, кнопка «Загрузить» сменилась на «Готово». В панели файлы сначала «Обработка», через 5–15 с у каждого документ: «ОСВ 60 · XLS · <дата загрузки>», период «2024 К3», проверка «Период —», «Контрагент ✓», статус «Загружен» (чистые ОСВ сразу, автоподтверждение, OQ-157; «Требуется проверка» — только с замечаниями, например шапка другой организации). Под статусом у каждой ОСВ — синий значок «ⓘ 1»: по клику список «Предупреждения документа» с «Информация: иерархия строк — по группировке листа (outline)» (на статус не влияет; [`qa-statements-v2.md`](qa-statements-v2.md), O-2). Панель опрашивает сервер раз в 5 с, пока есть файлы в обработке |
| W14 | Загрузить ещё раз любой из этих файлов | красный блок «Отклонено: 1» со строкой «<имя> — файл «<имя>» уже загружен для этого ЮЛ» (`DUPLICATE_FILE`), уведомление «Ни один файл не принят» |
| W15 | Закрыть панель, обновить страницу (F5) | статус версии в таблице — «Обработка документов» (K8) |
### 8.4 Просмотр ОСВ-документа
| № | Шаги | Ожидание |
|---|---|---|
| W16 | В панели Счастливочной клик по «ОСВ 60» (2024 К3) | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа выбор «Документ» (список документов ЮЛ) и статус «Загружен», кнопка «✓ Подтвердить» неактивна; надпись «Суммы в рублях; строки ОСВ только для чтения»; поиск «Контрагент или ИНН», фильтр «Счёт / субсчёт», справа «Строк: 77»; над таблицей «Предупреждения (1)» — «Информация: иерархия строк — по группировке листа (outline)» (на документах, загруженных до появления записей предупреждений, блока нет — загрузите файл заново). Таблица «как в 1С» (AURORA-1702/1703): все строки файла в его порядке, 77 = счёт `60` (1) + субсчета `60.01`, `60.02`, `60.31` (3) + 73 контрагента; колонки «Субсчёт», «Контрагент», «ИНН» и шесть сумм (сальдо на начало, обороты, сальдо на конец — Дебет / Кредит). Строки счёта и субсчетов — **жирным** и с суммами (`60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08»; `60.01`: «—», «28 220 505,40», «101 313 974,32», «94 382 838,93», «—», «21 289 370,01»; `60.31`: обороты «74 475,84» / «74 475,84»); текст в колонке «Контрагент» сдвинут на 16 px на уровень: `60` — без отступа, `60.01` — 16 px, контрагенты — 32 px (у групп в колонке «Контрагент» повторяется код счёта, как в файле). Колонка «Субсчёт» у контрагента показывает его субсчёт (`60.01`). Строка «Итого по документу» закреплена внизу таблицы (при окне 1600×1000 её край у нижней границы экрана — прокрутите область) и равна строке счёта `60` и «Итого» файла (строка 89): «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт). Итого файла (как и строки счёта и субсчетов) — свёрнутое сальдо 1С, поэтому сумма сальдо контрагентов с дебетовым остатком (начальное Дт «297 503,31») не равна «Итого» — это не ошибка |
| W16а | Фильтр «Счёт / субсчёт»: открыть список; выбрать `60`; затем `60.02`; затем `60.01`, `60.31`; сбросить крестиком | список: `60`, `60.01`, `60.02`, `60.31`; фильтр оставляет счёт и все его субсчета по префиксу (`62` найдёт `62`, `62.01`, но не `620`): `60` — «Строк: 77», `60.01` — «Строк: 50» (группа + 49 контрагентов), `60.02` — «Строк: 23» (группа + 22), `60.31` — «Строк: 3» (группа + 2); при субсчёте первой строкой идёт его жирная группа с отступом 16 px, контрагенты — 32 px; «Итого по документу» от фильтра не меняется; после сброса — «Строк: 77» |
| W16б | Поиск: ввести `бисквит`; затем `3528052718`; затем `60.01`; очистить | и по названию, и по ИНН — «Строк: 2»: «Русский бисквит», ИНН 3528052718, в субсчетах `60.01` и `60.02`; строки-группы при поиске скрываются (жирных строк нет); `60.01` в поле поиска — «Строк: 0» (поиск идёт по контрагенту и ИНН, не по счёту); поиск и фильтр счёта действуют вместе; «Итого по документу» не меняется; после очистки — «Строк: 77» |
| W17 | ka_go, через API (в UI не воспроизводится: загрузка только из панели ЮЛ внутри версии, см. раздел 8.7). Загрузить один файл из `../fixtures/osv/schastl/2024-Q3/` в QA-ЮЛ `$LE_QA` (id ЮЛ, заведённого для QA, не входящего в ГСК демо; без консолидации — маршрутом ЮЛ): `curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_QA/files" -H "$H" -F "files=@<файл>"`; дождаться `PROCESSED` (`GET /files/{file_id}`); `POST /documents/{id}/confirm` | документ `NEEDS_CHECK` («Контрагент ✗», шапка другой организации) до подтверждения; после — 200 `CONFIRMED`, `confirmed_by` заполнен; повтор — 409 `ALREADY_CONFIRMED`. Файл остаётся на стенде вне версий демо: счётчики W18 не затрагивает; `DELETE` после ручного подтверждения даёт 409; в панели [39] версии этот файл виден только по переключателю «Все документы организации» с подписью «вне консолидации» |
| W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (всего 22, список — 7.1): через «Загрузить документы» у каждого ЮЛ и подтверждение каждого ОСВ, либо скриптом из корня `arm-ka`: `docs/scripts/demo-prepare.sh <version_id>` (id — из адреса `?version=`) | скрипт печатает строку на каждый файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает только неподтверждённые ОСВ (чистые уже «Загружен», автоподтверждение, OQ-157) и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1236 пробелов: 7`. Статус версии — «Коррекция данных» (K9) появляется сразу после распознавания (автоподтверждение чистых документов, OQ-157), в «Организации» в колонке «ОСВ» — счета ЮЛ |
`demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API`
(по умолчанию `localhost:8000/api/v1`), каталог фикстур — из `FIXTURES_DIR` (по умолчанию `fixtures` рядом с `docs`).
### 8.5 Проверка формы ББ/ОФР из PDF [41], [41-02]
PDF из `fixtures/ocr` — отчётность других компаний (`aep_6.pdf` — ООО «Дорстроймонтаж», на 30.06.2025), поэтому ячейки
ББ/ОФР чек-листа версии 2024 Q3 они не заполняют; для проверки экрана подойдёт любое ЮЛ версии. Распознаются только PDF,
для которых в эмуляторе есть ответ (раздел 6.8). Экран показывает форму по контракту 2.0: что изменилось (строки со статусами,
подсветка «требует внимания», предупреждения у места) — [`qa-statements-v2.md`](qa-statements-v2.md) разделы 1 и 4.
| № | Шаги | Ожидание |
|---|---|---|
| W19 | В панели Счастливочной загрузить `../fixtures/ocr/aep_6.pdf` | через 10–60 с два документа: «Бух. баланс · PDF» и «ОФР · PDF», статус «Требуется проверка», «Контрагент ✗» (в скане другая организация) |
| W20 | Открыть «Бух. баланс» | экран «Проверка распознавания»: слева «ИСХОДНЫЙ ДОКУМЕНТ» — страницы PDF «Страница 1…8», масштаб «−», «100%», «+» и сброс; справа «РЕЗУЛЬТАТ РАСПОЗНАВАНИЯ»: шапка «Организация» (текст из скана), «ИНН —» и «Дата отчёта —» с синим значком ⓘ (подсказки «ИНН не найден», «Дата отчёта не разобрана»), «Единица измерения тыс. руб.»; заголовок «Бухгалтерский баланс»; «Проверки пройдены»; «Предупреждения (3)» — «Информация» о наименовании строки 1530, дате и ИНН; переключатель «Показать расшифровки (20)»; колонки «На 30 июня 2025 г», «На 31 декабря 2024 г.», «На 31 декабря 2023 г.»; разделы «АКТИВ», «I. Внеоборотные активы» … «ПАССИВ»; итоги разделов жирным; суммы без копеек, пустые — «-»; строка 1600 «БАЛАНС» — 293 709 / 179 791 / 189 847; две розовые строки «код не распознан» без сумм (подтверждение не блокируют). Оранжевые — только ячейки «требует внимания» (сомнительные и не подтверждённые проверкой §7); в подсказке ячейки «Распознано: «…»» и «Уверенность: N%» есть и у неподсвеченных. Подробно — [`qa-statements-v2.md`](qa-statements-v2.md) N15 |
| W21 | «✎ Редактировать» → в строке 1600 первой колонки прибавить 100 → «✓ Сохранить» | уведомление «Изменения сохранены»; ячейка голубая, подсказка «Исходное: 293 709»; блок «Нарушения (N)» со строками «Колонка N: <соотношение> — ожидалось X, получено Y» (F3) |
| W22 | «✓ Подтвердить данные» | красный блок в правой части с текстом сервера и списком нарушений (409 `CHECKS_NOT_PASSED`, F4); документ не подтверждён |
| W23 | «Принять с комментарием» → в окне «Принять нарушения» сначала пустой комментарий, затем «округление в форме клиента» → «Принять» | с пустым комментарием «Принять» неактивна; после — синий блок «Нарушения приняты» с датой и комментарием (F5) |
| W24 | «✎ Редактировать», ввести в ячейку `abc` или `1.234` → «✓ Сохранить» | ячейка в красной рамке, подсказка «Неверный формат суммы»; уведомление «Исправьте суммы с неверным форматом», запрос не уходит. «Отменить» сбрасывает правки |
| W25 | Вернуть в 1600 исходную сумму, сохранить | «Проверки пройдены», принятие нарушений сброшено правкой (F6) |
| W26 | В шапке «Дата отчёта» ввести `2025-09-15` → «✓ Сохранить» → «✓ Подтвердить данные» | красный блок «<текст сервера>. Исправьте дату отчёта в шапке и сохраните.» (409 `PERIOD_REQUIRED`: дата не на конец квартала, F9). Дата вводится строго `ГГГГ-ММ-ДД` |
| W27 | Дата `2025-06-30` → «✓ Сохранить» → «✓ Подтвердить данные» | уведомление «Документ подтверждён», статус «Загружен», «Редактировать» и «Подтвердить данные» неактивны |
| W28 | Открыть «ОФР» того же файла [41-02] | заголовок «Отчёт о финансовых результатах», колонки периодов ОФР; остальное — как W20 |
| W28a | В «Бух. балансе» посмотреть строки 1600 и 1700 | строки выделены фиолетовым фоном и жирным шрифтом |
| W28b | «✎ Редактировать» на форме, где есть достроенные (в источнике отсутствующие) строки | пустые достроенные строки видны, редактируются и не подсвечены как ошибки; ввод суммы → «✓ Сохранить» создаёт строку, проверки пересчитываются |
Формы из Excel (`fixtures/statements-xlsx`, раздел 6.9) открываются тем же экраном, но слева вместо скана — «Превью
недоступно для этого формата» и кнопка «Скачать исходник». Документы «Расшифровка» и «Прочее» в панели не
открываются. Строки без кода, расшифровки, правка кодов строк и дат колонок, подтверждение значения без правки, новые
блокировки подтверждения — сценарии N23–N49 в [`qa-statements-v2.md`](qa-statements-v2.md).
### 8.6 Сводная ОСВ [30]
Предусловие — W18: все 22 ОСВ загружены и подтверждены.
| № | Шаги | Ожидание |
|---|---|---|
| W29 | В строке версии «К отчётности →» | экран «Русский Бисквит — 2024 К3», крошки «Отчётность › Русский Бисквит › 2024 · К3», справа «Статус отчёта» «Корректировка данных» и «Изменить статус»; активна вкладка «ОСВ», «Шахматка» доступна; «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий», «Подтверждение КФО клиента» неактивны (подсказка «Будет в следующих срезах») |
| W30 | Сводка | под таблицей «Строк: 1385» (= `rows_total`, 7.2); жёлтый баннер «Требуется указать соответствия … Осталось: 1236.» (= `unmapped_total`); свёрнутый блок «Пробелы: 7 — по этим счетам нет ОСВ за часть кварталов, их строк в своде нет», раскрыть — 7 строк «<ЮЛ> — счёт <код> — нет кварталов: Q1, Q2»: РБ 01, 58.03, 66, 67, 76; Счастливочная 01, 02 |
| W31 | Колонка «Период», суммы | у всех строк «9 мес. 2024 г.»; суммы с запятой и пробелами, ноль — «—»; по 500 строк на страницу |
| W31a | После W32 над таблицей: «Единицы» = «тыс. руб.», затем «Знаков после запятой» = 0; затем «млн руб.» и 3 знака | строка `60.01` Счастливочной: «4 454,15», «128 058,14», «125 950,45», «2 346,46»; с 0 знаков — «4 454», «128 058», «125 950», «2 346»; в млн и 3 знака — «4,454», «128,058», «125,950», «2,346»; «—» остаётся «—». Меняется только показ: «Строк», фильтры и экспорт те же (суммы в xlsx — в рублях) |
| W32 | Фильтр «Компания ГК» (воронка в заголовке) = «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» → «Применить»; фильтр «Контрагенты» = `бисквит` → «Применить» | «Строк: 6» — `60.01`, `60.02`, `62.01`, `62.02`, `76.06`, `76.АВ`, контрагент «РУССКИЙ БИСКВИТ, АО», ИНН 3528052718. Суммы — как в 7.2, на экране с запятой: `60.01` — «—», «4 454 151,93», «128 058 143,65», «125 950 450,77», «—», «2 346 459,05»; `60.02` — «—» везде, кроме оборотов «9 745 019,62» / «9 745 019,62»; `62.01` — «40 211 938,00», «—», «398 018 012,46», «330 517 504,00», «107 712 446,46», «—»; `62.02` — «—», «24 100 000,00», «9 900 000,00», «—», «—», «14 200 000,00»; в последней колонке «Тип контрагента» у всех шести — синий бейдж «ГСК». Окно фильтра закрывается после «Применить», иконка активного фильтра зелёная |
| W32a | Сбросить фильтры («Сбросить» в окне каждого активного фильтра; общей кнопки нет, как и в макете [30]); прокрутить таблицу вправо; фильтр на колонке «Тип контрагента» по очереди «ГСК», «ГК», «Внешний» → «Применить» | у каждой строки бейдж: «ГСК» — синий, «ГК» — зелёный, «Внешний» — серый (как в макете [30]); «Строк»: «ГСК» — 10, «ГК» — 26, «Внешний» — 1349, в сумме 1385. «Внешний» больше «Осталось» в баннере (1236): строки с ИНН из файла, которого нет в справочнике, — «Внешний», но не «несопоставленные» (отмена П-1, 7.2); «Осталось» = W34. «Сбросить» в окне фильтра возвращает все строки и закрывает окно |
| W33 | Сбросить фильтры (как в W32a); «Счёт» = `62` | «Строк: 387»; `60.01` — 0 строк (фильтр принимает только корень счёта, ограничение прототипа) |
| W34 | «Показать ЮЛ без ИНН» | только строки без связи с ЮЛ и без ИНН, «Строк» = «Осталось»; в ячейке ИНН — только красное «Указать» на подсвеченном фоне (макет [32-osv-no-inn]); воронка колонки «ИНН» зелёная; кнопка сменилась на «Показать все строки» |
| W34a | Ячейка ИНН без фильтров (arm-web #17) | строка с ИНН в файле — просто ИНН моноширинным шрифтом, «Указать» нет (даже если такой организации нет в справочнике); строка, связанная без ИНН в файле (например, РБ → «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» на данных 2025 К1), — серая «таблетка» с ИНН организации справочника; клик по ней открывает «Выбор организации для связи с …», у текущей организации — неактивное «Связано», у остальных — «Связать» |
| W35 | «Экспорт» (с фильтрами и без) | файл `svod-osv_Русский Бисквит_2024-Q3_v1.xlsx` по текущим фильтрам; содержимое — по 7.3. Имя `Сводная_ОСВ_Русский_Бисквит_2024_Q3.xlsx` — запасное: ядро без arm-core #173 не открывает фронту `Content-Disposition` (видно только при фронте на другом порту, `npm run start`) |
Если на стенде раньше делали ручное сопоставление контрагентов (K14), «Осталось» меньше 1236: алиасы переживают версии.
Остальные цифры от этого не меняются.
### 8.7 Что в UI не считать багом (прототип)
- Нет смены периметра, экрана чек-листа, комментариев, удаления файлов, «Добавить версию» — это проверяется через API
(разделы 6.6, 6.9, 7). «Взять в работу» и «Изменить статус» (arm-core #166) и сопоставление контрагентов в своде (W34a) —
уже в UI.
- В своде не показываются «документов: 22» и колонка «Способ сопоставления» (её нет и в макете [30]; значение — в
экспорте); фильтр «Тип контрагента» — одно значение, а не галочки, как в макете (API принимает один признак); фильтр
«Счёт» принимает только корень (`60`, не `60.01`).
- Таблица консолидаций и статус версии обновляются по F5: после создания консолидации на странице той же ГК новой строки
может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов.
- «Распознан» (старые `RECOGNIZED` и формы после правки) и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка».
- «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—».
- Панель документов показывает первые 100 файлов рабочего стола консоли (или ЮЛ — по переключателю); загрузка — только из панели ЮЛ внутри версии «в работе».
- Шапку формы (ИНН, дату, единицу, с v5.19 и наименование организации) нельзя очистить, только заменить; время принятия
нарушений — в часовом поясе браузера, остальные даты — по Москве.
- Формы ББ/ОФР по контракту 2.0: известные замечания экрана (номер «Колонка N» = столбец листа, «Показать» в нарушениях без
рамки, значок ⓘ на разной высоте, сырое значение в поле правки и др.) и что так задумано — [`qa-statements-v2.md`](qa-statements-v2.md)
разделы 6 и 7. Значок с числом под статусом документа в панели считает и принятые нарушения; «Предупреждения обработки» в число
не входят.
- Ссылка `?version=` с несуществующим id «выбирает» пустую версию; при 404 на своде может прийти до трёх сообщений;
после истечения токена кроме «Сессия истекла» бывают лишние уведомления.
- Предупреждения antd в консоли браузера («Static function can not consume context…», deprecated) ни на что не влияют.
- Уведомления в `/admin` не появляются — дефект arm-web#7 (раздел 6.1).
Баг UI — раздел 11: номер сценария W, скриншот, вкладка Network с запросом и ответом.
## 9. Известные ограничения (не заводить как баги)
- Справочники — только через API; в UI — администрирование и путь консолидации (раздел 8, ограничения прототипа — 8.7).
- Вход без пароля, открытый `/docs`, CORS `*` — dev-режим.
- Email `*.local` через API/UI отклоняется, при этом dev-пользователи именно такие (созданы seed напрямую).
- Удалять алиасы и пустые ГСК/ГСЗ может только ADMIN (право `DIRECTORY_DELETE`).
- Граница «дата перевода не раньше сегодня» считается по UTC: до 03:00 МСК «сегодня» ещё вчера. Будет исправлено.
- Повторный `make seed-dev` возвращает права, снятые администратором в матрице.
- Профиль в шапке не перечитывается после правки собственных ролей.
- В консоли браузера сообщение `Store does not have a valid reducer` и предупреждения antd — каркас, не влияют.
- Таблица пользователей показывает первые 200.
- При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт `INVALID_INN`.
- Консолидация (Срез 4): блокировка, периметр, статусы и сопоставление — только API (раздел 8.7); маршрут согласования (`CONSOLIDATION_RF` и далее), «Добавить версию», гудвилл, шахматка — следующие
срезы; формы ББ/ОФР в сводной ОСВ не участвуют (только в чек-листе); блокировка версии не истекает по времени; `period_check` документов остаётся `UNKNOWN`;
`PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122).
- ОСВ, период которой (из шапки) не подходит периоду консолидации, после подтверждения молча не попадает в свод (OQ-161);
для тестов на произвольных ОСВ — временный режим из раздела 7.4.
- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только
при точном совпадении нормализованного имени (OQ-127).
- PDF-путь (Срез 3a): эмулятор отвечает только на PDF из `fixtures/ocr`, остальные —
`INVALID_DOCUMENT` (раздел 6.8, OQ-151).
- Формы ББ/ОФР по контракту 2.0 (единая нормализация, выкладка 19.10.2026): известные замечания I-1…I-5, M-1…M-9, O-2, F-1, F-2
и «что так задумано» — [`qa-statements-v2.md`](qa-statements-v2.md) разделы 6 и 7.
- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (счёт 91 — OQ-132; скользящий период — OQ-131).
- Удаление алиаса одновременно с его переносом ручным сопоставлением даёт 404 вместо 204 (arm-core #26) — так задумано.
- Код `60.АВ` (синтетика s09) и буквенные забалансовые вида `РВ.1` в план счетов не резолвятся (arm-core #28, C23-1/C23-2).
Полный список отложенных находок — Gitea: `RSHB/arm-core` issues «Follow-ups from #5», «Follow-ups from #18» (#21), #28;
`RSHB/arm-tabular-parser` #18.
## 10. Сброс стенда
```bash
docker compose -f ../docs/dev/docker-compose.yml down -v # БД и бакет с нуля
make up && docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio
make migrate && make seed-dev
```
Бэкенд и фронт перезапускать не нужно, но сессии в браузере лучше начать заново.
Файлы, загруженные в 6.6, удаляются из БД командой `DELETE /files/{id}`, но объекты в MinIO остаются (им ставится только
`expires_at`) — `down -v` стирает и их.
## 11. Как заводить баги
Gitea: бэкенд, API и экраны — `RSHB/arm-core`, документация — `RSHB/arm-ka-docs`. Один дефект — одна issue.
Не уверены, бэкенд это или фронт, — откройте вкладку Network: ответ сервера не тот, что в гайде, — бэкенд; ответ верный, а
экран показывает другое — фронт (`web/`); в issue укажите, что именно.
Шаблон:
```
Срез: 1 / Сценарий: D16 (номер из гайда, если есть)
Пользователь: ka_go@armka.local (роль KA_GO)
Шаги: 1. … 2. …
Ожидание: 409 MEMBER_OF_OTHER_GROUP
Факт: 500 (тело ответа ниже)
Запрос: метод, путь, тело (без токена)
Ответ: статус и тело
Лог бэкенда: строки с трейсбэком, если есть
Окружение: ОС, браузер, версии docker/node, коммит arm-core (`git -C arm-core rev-parse --short HEAD`)
Скриншот: для багов UI — экран и вкладка Network с запросом
```
Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке.
## 12. Типичные проблемы при запуске
| Симптом | Причина и решение |
|---|---|
| `make up` → порт занят | локальный PostgreSQL/MinIO; остановить или сменить `*_PORT` в `docs/dev/.env` и те же порты в `arm-core/api/.env` |
| `docker compose up --wait` без списка сервисов возвращает 1 | нормально из-за `minio-init`; используйте команду со списком из 4.1 |
| `make migrate` → connection refused | PostgreSQL ещё не `healthy`; подождать и повторить |
| `make venv` не находит Python 3.11 | `uv` скачает его сам при наличии интернета; проверьте `uv python list` |
| Белый экран на `localhost:3000` | фронт запущен `npm run start` без `REACT_APP_ROOT_PATH=/` (4.3) |
| После входа «Нет связи с сервером» или ошибки сети | ядро не запущено на `:8000`; если на другом адресе — задайте `ARM_CORE_URL` (4.3) |
| Страница «Вход выполняется через РАИСА» | это production-сборка; запускайте `npm run start` (4.3), а не `npm run build` |
| `npm run start`: порт 3000 занят | запущен второй фронт (например, из worktree); остановите его |
| Скан PDF не отображается («Не удалось открыть PDF») | старый браузер — обновите его |
| PDF завис в «Обработка» | не запущены `scan-gateway`/`scan-emulator` (раздел 8, предусловия) |
| PDF в статусе «Ошибка» (подсказка `INVALID_DOCUMENT`) | эмулятор знает только PDF из `fixtures/ocr` с JSON-ответом (6.8); свой PDF так и должен падать |
| Импорт падает на записи файла в S3 | MinIO не поднят или не создан бакет: `docker compose logs minio-init` |
| `/healthcheck.subjects` ядра и сервиса различаются (или файл при `nats: ok` стоит в `UPLOADED`, а через ~2 мин становится `ERROR` — ack не пришёл) | у сервисов разные `OPENBAO__NATS__NATS_*_SUBJECT`; выровняйте настройки у всех трёх |
| `/healthcheck` показывает `nats: down` | NATS не поднят; на сценарии Среза 1 не влияет, но загрузка ОСВ (6.6) без NATS не дойдёт до парсера |
| Файл завис в `UPLOADED`/`PROCESSING` | не запущен tabular-parser (`curl localhost:8001/healthcheck`) или ядро запущено без `NATS_ENABLED=true`; без ack файл через ~2 мин станет `ERROR` |
Подробнее об инфраструктуре — `docs/dev/README.md`, о бэкенде — `arm-core/README.md`, о фронте — `arm-core/web/README.md`.