W16 переписан под таблицу «как в 1С»: все строки файла, группы жирным с суммами, отступ 16 px на уровень, «Строк: 77», фильтр «Счёт / субсчёт», строка «Итого». Добавлены W16а (фильтр по счёту) и W16б (поиск). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 KiB
Гайд тестировщика: локальный стенд АРМ КА (Срезы 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.
Единая нормализация ББ/ОФР (контракт результата распознавания 2.0, выкладка 19.10.2026): что изменилось, справка по
предупреждениям, чек-лист N1–N66 и известные замечания — 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.
Нет (это не баги):
- Экранов справочников ГК/ЮЛ/ГСК (пункты меню «ГК и организации», «ЮЛ вне ГК» — заглушки) — проверяются только через
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/...):
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:
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)
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 Бэкенд
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):
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)
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»).
| Роль | Филиал | Что видит | |
|---|---|---|---|
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:
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": "..."}}: 404NOT_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 |
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 Аудит (БД)
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-ГК «Унагранде» — Головной офис, «Агрофирма Межениновская» —
Ростовский РФ).
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 в нём):
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 |
| 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 §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.
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. 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 секунд.
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 раздел 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 ГСК.
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):
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
GET …/osv/exportбез фильтров →svod.xlsx; лист «Сводная ОСВ»: число строк без заголовка =rows_total, порядок — как в API (ЮЛ по имени, счёт, номер строки). Суммы — числа с двумя знаками (не текст): выделите колонку «Обороты Дт» и посмотрите сумму в строке состояния Excel; формат ячейки#,##0.00.- Найдите строку «Русский бисквит»
60.01Счастливочной и сверьте шесть сумм с таблицей из 7.2 и с ответомGET …/osv. - Колонки «Сопоставлено с» и «Способ сопоставления»: для сопоставленных — имя ЮЛ из справочника и
INN/MANUAL/PROPAGATED, для несопоставленных — пусто. - Лист «Пробелы»: 7 строк, как в
summary.gaps; с фильтром?legal_entity_id=в экспорте лист «Пробелы» всё равно полный. - Экспорт с
?unmapped=trueпосле полного сопоставления — лист «Сводная ОСВ» только с заголовком, лист «Пробелы» заполнен. - Текст контрагента, начинающийся с
=,+,-,@(сделайте такой в своём файле), в 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; после смены — перезапуск ядра.
В режиме ОСВ, загруженная в консолидацию (через версию, а не с рабочего стола), период шапки которой консолидации не подходит, считается нарастающей за год и квартал консолидации: подтверждение привязывает её, свод, чек-лист и колонка «ОСВ» периметра её видят. ОСВ «своего» периода режим не трогает — квартальные склеиваются как обычно. Что учесть:
- Один файл «чужого» периода на счёт ЮЛ. У подменённых ОСВ склейки кварталов нет: все они по одному счёту одного ЮЛ — один слот, в своде будет подтверждённая последней. Грузите одну ОСВ на счёт (лучше нарастающую за весь период).
- Подтвердить всё нужное. Документы
NEEDS_CHECK(в т. ч. из-за чужого ЮЛ в шапке,counterparty_check = MISMATCH) в свод не идут, пока их не подтвердили. - Документы, подтверждённые до включения режима, сами не перепривязываются: уберите ЮЛ из периметра и верните (подхват
PUT …/membersпод своей блокировкой версии) либо создайте консолидацию заново. Периметр — не меньше двух ЮЛ (PERIMETER_TOO_SMALL), поэтому при двух участниках меняйте ЮЛ на временное третье той же ГК и обратно:{А, Б} → {Б, В} → {А, Б} → {А, В} → {А, Б}(проверено на стенде 12.10.2026: РБ 2026 Q1 из ОСВ за 1 кв. 2025 — 16 документов привязаны, свод 1421 строка, суммы равны своду РБ 2025 Q1 из тех же файлов). - Карточка документа по-прежнему показывает период из шапки — это не баг. ББ/ОФР режим не затрагивает.
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):
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, 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 разделы 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 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.
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 строк на страницу |
| 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]); «Строк»: «ГСК» — 10, «ГК» — 26, «Внешний» — 1349, в сумме 1385. «Внешний» больше «Осталось» в баннере (1236): строки с ИНН из файла, которого нет в справочнике, — «Внешний», но не «несопоставленные» (отмена П-1, 7.2); «Осталось» = W34. «Сбросить» в окне фильтра возвращает все строки и закрывает окно |
| W33 | Сбросить фильтры; «Счёт» = 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 |
Если на стенде раньше делали ручное сопоставление контрагентов (K14), «Осталось» меньше 1236: алиасы переживают версии. Остальные цифры от этого не меняются.
8.7 Что в UI не считать багом (прототип)
- Нет «Взять в работу», смены периметра, ручных переходов статусов, сопоставления контрагентов, экрана чек-листа, комментариев, удаления файлов, «Добавить версию» — это проверяется через API (разделы 6.6, 6.9, 7).
- В своде не показываются «документов: 22» и колонка «Способ сопоставления» (её нет и в макете [30]; значение — в
экспорте); фильтр «Тип контрагента» — одно значение, а не галочки, как в макете (API принимает один признак); фильтр
«Счёт» принимает только корень (
60, не60.01). - Таблица консолидаций и статус версии обновляются по F5: после создания консолидации на странице той же ГК новой строки может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов.
- «Распознан» (старые
RECOGNIZEDи формы после правки) и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка». - «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—».
- Панель документов показывает первые 100 файлов рабочего стола консоли (или ЮЛ — по переключателю); загрузка — только из панели ЮЛ внутри версии «в работе».
- Шапку формы (ИНН, дату, единицу, с v5.19 и наименование организации) нельзя очистить, только заменить; время принятия нарушений — в часовом поясе браузера, остальные даты — по Москве.
- Формы ББ/ОФР по контракту 2.0: известные замечания экрана (номер «Колонка N» = столбец листа, «Показать» в нарушениях без
рамки, значок ⓘ на разной высоте, сырое значение в поле правки и др.) и что так задумано —
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разделы 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. Сброс стенда
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.