arm-ka-docs/dev/qa-guide.md
Andrey Epifancev b55db422f8 docs: исправить примеры F8a/F8b в qa-guide (#81)
F8a: line_code 1600 (LINE_EXISTS) и 9999 (вне справочника) с реальным column_id;
F8b: уточнены формулировки; в гайде фронта — текст ошибки при отсутствии редакции.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 12:23:48 +04:00

130 KiB
Raw Blame History

Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 3b, 4 и фронт arm-web)

Документ для ручного тестирования текущего main бэкенда и ветки test фронта 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 не доходит.

1. Что сейчас есть и чего нет

Есть (Срез 0 + Срез 1 + Срез 2c):

  • Бэкенд arm-core (FastAPI, /api/v1): вход по email в dev-режиме, профиль пользователя, права как данные.
  • API справочников: группы компаний (ГК), юрлица и ИП (ЮЛ) с историей, переводом между ГК и закрытием, алиасы контрагентов, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Видимость по филиалу: головной офис видит всё, филиал — только свои ГК.
  • Импорт справочников из выгрузки ЕЦП CRM (xlsx/csv) с пробным прогоном и отчётом.
  • Администрирование: пользователи, роли, матрица «роль × право», защита последнего администратора.
  • Фронтенд arm-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 arm-web — раздел 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).

Нет (это не баги):

  • Экранов справочников ГК/ЮЛ/ГСК (пункты меню «ГК и организации», «ЮЛ вне ГК» — заглушки) — проверяются только через 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, arm-web (в коде пути вида ../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
git clone -b test ssh://git@git.aepif.ru:2222/RSHB/arm-web.git arm-web

Фронт тестируется с ветки test репозитория arm-web (интеграционная ветка прототипа; main там — исходная копия без экранов консолидации). Обновление: git -C arm-web 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-web (второй терминал, из корня arm-ka)

cd arm-web
make install        # npm install; первый раз — несколько минут
make 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 make start.

Запускайте именно make start: он задаёт REACT_APP_ROOT_PATH=/. Голый npm run start ждёт путь приложения как на платформе банка (/aurora/apps/fastapi-dkb-armka) и показывает белый экран. Не используйте и make build/npm run build для тестирования: в production-сборке dev-формы входа нет, страница показывает «Вход выполняется через РАИСА».

Проверка сборки фронта (линтер, типы, юнит-тесты, сборка) — make check в arm-web; ручному тестированию она не нужна.

Старый фронт arm-core/web (только /admin) по-прежнему запускается cd arm-core/web && npm install && REACT_APP_API_URL=http://localhost:8000 REACT_APP_ROOT_PATH=/ npm run start — на том же порту 3000, поэтому одновременно с arm-web не поднимайте. Он нужен, пока в arm-web не исправлены уведомления администрирования (arm-web#7, раздел 6.1).

Настройки из api/.env.example (DEBUG=true, AUTH_DEV_ISSUER=true, CORS_ORIGINS=, FORWARDED_ALLOW_IPS=127.0.0.1) для локального стенда менять не нужно: DEBUG=true включает документацию API на /docs, dev-вход и разрешает make seed-dev.

make seed-dev (arm-core #31) создаёт, кроме пользователей, dev-ГК под документы из fixtures: ГК «Русский Бисквит» ровно как в выгрузке ЕЦП CRM (20 ЮЛ/ИП, филиал «Санкт-Петербургский РФ», ГСК из АО «Русский бисквит» 3528052718 и ООО «Пекарня Счастливочная» 3528104476, алиасы под имена контрагентов из ОСВ) и ГК владельцев форм пакета ДКБ («Унагранде», «Новороссийский прокатный завод», «Агрофирма Межениновская»). Поэтому ОСВ и формы из ../fixtures загружаются сразу на «свои» ЮЛ (6.6, 7.1), а импорт выгрузки ЕЦП (6.3) на таком стенде отвечает GK_EXISTS/LE_EXISTS. Стенд без dev-ГК (пустые справочники, как в тестах ядра): cd api && .venv/bin/python -m scripts.seed --dev --no-dev-groups.

4.4 Вход

В форме введите email dev-пользователя (не имя и не логин) и нажмите «Войти (dev)». Пароля нет. Это единственная форма входа АРМ: страница с полями «Имя пользователя / пароль» — это Gitea, а не стенд. Токен живёт в текущей вкладке (sessionStorage), новая вкладка — новый вход. Токен действует 8 часов; после истечения форма входа появится снова с сообщением «Сессия истекла». Кнопки «Выйти» нет: чтобы сменить пользователя, откройте новую вкладку (или закройте текущую). Главная / после входа показывает ФИО dev-пользователя («Dev: Аналитик ГО») и его роли («Роли: KA_GO»).

Email Роль Филиал Что видит
admin@armka.local ADMIN Головной офис всё; единственный с правом администрирования и импорта
ka_go@armka.local KA_GO Головной офис все ГК и ЮЛ, может создавать и менять
curator_go@armka.local CURATOR_GO Головной офис все ГК и ЮЛ, может создавать и менять
expert@armka.local EXPERT Головной офис всё, только чтение
ka_rf@armka.local KA_RF Ростовский РФ только ГК своего филиала, может создавать и менять
rka@armka.local RKA Ростовский РФ только ГК своего филиала
client_manager@armka.local CLIENT_MANAGER Краснодарский РФ только ГК своего филиала, чтение

4.5 Токен для Swagger и curl

В Swagger (/docs) токен подставляется кнопкой Authorize (замок справа вверху): вставьте значение access_token без слова Bearer, дальше все защищённые ручки отправляют заголовок сами. Кнопка появилась с arm-core #29; если её нет — бэкенд старше, используйте curl:

TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
  -d '{"email":"ka_go@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"

В Swagger: кнопка Authorize → вставить токен (без слова Bearer) → все запросы пойдут от этого пользователя. Чтобы проверить видимость разными ролями, получите второй токен другим email и переавторизуйтесь.

5. Как читать ответы

  • Успех: 200/201, тело JSON. Удаление — 204 без тела.
  • Нет токена или он плохой — 401. Нет права — 403 с текстом недостаточно прав: требуется <КОД>.
  • Доменные ошибки — объект {"detail": {"code": "...", "message": "..."}}: 404 NOT_FOUND (в том числе для чужой записи — так задумано, чужое неотличимо от несуществующего), 409 конфликты (INN_EXISTS, LAST_ADMIN, …), 422 валидация (INVALID_INN, OFFICE_NOT_ALLOWED, …).
  • Ошибки валидации тела (не тот тип поля) — 422 со списком от pydantic.
  • Списки — {"items": [...], "total": N, "limit": 50, "offset": 0}, limit ≤ 200.

Полный каталог кодов — arm-core/README.md, раздел «Ошибки API».

6. Чек-лист сценариев

Отмечайте: ✅ ожидание совпало / ❌ баг (с номером issue) / ⚠️ вопрос. Пустые поля ожиданий — проверьте сами и зафиксируйте результат.

6.1 Администрирование (UI /admin, вход admin@armka.local)

Пункт меню «Система → Пользователи и роли». Известный дефект arm-web#7: в arm-web на этом экране не появляются уведомления (ни об успехе, ни об ошибке), в консоли браузера — message.error is not a function, модалка создания при ошибке остаётся открытой без текста. Пока он не исправлен, сценарии с ожиданием «уведомлением» (A3, A6, A8, A10) проходите на старом фронте arm-core/web (раздел 4.3) или проверяйте код ответа во вкладке 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). Пункт меню в arm-web виден всем ролям — так в прототипе, в arm-core/web он скрыт
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

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 загрузить ОСВ ЮЛ с consolidation_version_id=V, где 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 и duplicate_line_codes; в metrics.warnings — предупреждение об единицах («значений с дробной частью при единице тыс. руб.: N из M»: значения как в файле, не нормализованы, OQ-138), «колонка кодов не найдена», «страница-продолжение без шапки». Ожидания по реальным файлам — ../tabular-parser/api/tests/statements_manifest.yaml и таблица «файл → статус» в README tabular-parser; файлы — ../fixtures/statements-xlsx/ (синтетика st01…st07). Постраничные xls из 1С (страница 2 отдельным файлом, ББ только актив) и формы в рублях при шапке «тыс. руб.» дают needs_check — это штатно (OQ-138, OQ-140), не баг; done ожидается только у форм, где §7 сходится (Межениновская).

Как делать свои файлы. Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист:

  • сдвинуть таблицу: вставить 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»), в ячейке над названием и склеенный «ИНН/КПП» парсер извлекает (OQ-100, OQ-130, arm-tabular-parser#9): counterparty_inn заполнен, counterparty_name без ИНН, исходный текст — в row_value.
  • ИНН в колонке и ИНН в хвосте названия различаются — берётся колонка, предупреждения нет; строка-ИНН над счётом или договором (не контрагентом) склеивается, но её ИНН теряется молча (arm-tabular-parser#18, A9-2/A9-3).
  • Счёт 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). В metrics.warnings (по эталону fixtures/ocr/expected_normalize/aep_3.json): файл — ИНН распознан неуверенно; таблица 0 — шапка синтезирована: колонки без дат, строк с нечисловым кодом пропущено: 8, строк без кода с суммами пропущено: 2, повторы кодов 1320/1350/1360/1370; таблица 1 — шапка синтезирована, пропущено 2 и 2; строка <job_id>_ofr.json: INVALID_DOCUMENT: … (второй файл набора — ответ-ошибка, штатно; понижает статус до needs_check)
aep_6.pdf needs_check, два документа. Предупреждения: файл — ИНН не распознан, дата отчёта не разобрана; таблица 0 — пропущено 2 (нечисловой код) и 19 (без кода с суммами), редакция fsbu4 не заполнена (форма 2025 года, fsbu4 пока пуста — OQ-150), повторы кодов 1220/1520; таблица 1 — пропущено 1 и 2, fsbu4 не заполнена, повтор кода 2410 (OQ-152)
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 только у неразобранных сумм. done на реальных сканах почти не бывает — это ожидаемо. В dev-compose у шлюза SCAN_CLEANUP=true: после ответа Output/<job_id>/ и входящий файл удаляются. Что не считать багом: строки с мусорным или пустым кодом не попадают в документ, но есть предупреждения строк с нечисловым кодом пропущено / строк без кода с суммами пропущено (OQ-149); при повторе кода остаётся первая строка (OQ-152); минус не поставлен при } вместо ) (OQ-147); is_recognized = false у прочерка «—» (OQ-148); сумма с тремя знаками после запятой (1,234) — amount = null, текст в string_value.

6.9 Формы ББ/ОФР: просмотр, правка, акцепт, подтверждение (Срез 3b)

Предусловие (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 колонки 0 увеличена на 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 (у колонки 0 is_current = true); lines — 37, отсортированы по line_code, у каждой values на все колонки; суммы — строки; header.inn.value = "7016006635", confidence = "100.00", modified = false; checks.failed, soft_failed пусты, accepted = null
F3 ka_go PATCH …/statement {"values":[{"line_id":…,"column_id":…,"amount":"<сумма строки 1600, колонки 0, увеличенная на 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) почти всегда NEEDS_CHECK — уверенность значений меньше 100, ИНН распознан неуверенно (aep_3.pdf, aep_6.pdf, раздел 6.8), аналитик правит значения и подтверждает; у форм без колонки кодов документ создаётся без строк (OQ-139); повтор одной формы в одном файле — вторая не создаётся, первая NEEDS_CHECK; страницы одной формы из разных файлов не склеиваются (OQ-140); строк без кода в ответе нет (OQ-36); скачивание файла и CSV в журнал не пишутся (OQ-153); формулы §7 одни для редакций 66n и fsbu4 (OQ-146); уже смапленные до Среза 3b файлы форм не получают — загрузите файл заново (раздел 10 — сброс стенда).

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 arm-web (раздел 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, 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/legal-entities/$LE/files?consolidation_version_id=$V" -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 владелец блокировки загрузить ОСВ ЮЛ периметра с ?consolidation_version_id=V (раздел 6.6) 201; версия INITIATED → DOCUMENTS_PROCESSING; ЮЛ вне периметра — 422 ENTITY_NOT_IN_PERIMETER; несуществующая версия — 404
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
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 до загрузки — тогда переходы статусов видны на 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/legal-entities/$LE_RB/files?consolidation_version_id=$V" -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/legal-entities/$LE_SCH/files?consolidation_version_id=$V" -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 1260 до ручного сопоставления — строки без ЮЛ и без ИНН из файла (отмена П-1, arm-core #74): строки с ИНН, которого нет в справочнике, сюда не входят; 4 строки Счастливочной с ИНН 3528052718 (60.01, 60.02, 62.01, 62.02) сопоставлены автоматически
documents_total 22
gaps ровно 7: РБ — 01, 58.03, 66, 67, 76; Счастливочная — 01, 02; у всех missing_quarters = ["Q1","Q2"]

Строки свода (GET …/osv?legal_entity_id=…&q=…), суммы в рублях:

Компания, счёт, контрагент Сальдо нач. Дт Сальдо нач. Кт Обороты Дт Обороты Кт Сальдо кон. Дт Сальдо кон. Кт Признак
Счастливочная, 60.01, «Русский бисквит», ИНН 3528052718 — 4 454 151.93 128 058 143.65 125 950 450.77 — 2 346 459.05 GSK, match_kind = INN
Счастливочная, 60.02, «Русский бисквит» 0 0 9 745 019.62 9 745 019.62 0 0 GSK; контрагента нет в Q2 — квартал считается нулями
РБ, 60.01, «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ», без ИНН — 38 806 758.00 307 986 284.00 375 490 642.46 — 106 311 116.46 EXTERNAL до сопоставления, после K14 — GSK
Счастливочная, 62.01, «Русский бисквит» 40 211 938.00 — 398 018 012.46 330 517 504.00 107 712 446.46 — GSK; в каждом квартальном файле под 62.01 две строки с этим контрагентом — свод их суммирует, entry_ids = 6
Счастливочная, 62.02, «Русский бисквит» — 24 100 000.00 9 900 000.00 0 — 14 200 000.00 GSK

Как проверить любую строку самому: откройте три квартальных файла этого ЮЛ по этому счёту, найдите контрагента под тем же субсчётом и сложите обороты по трём файлам; сальдо на начало сравните с первым файлом, на конец — с третьим. Тождество «начало + обороты Кт − обороты Дт = конец» (для кредитового сальдо) должно сходиться: 4 454 151.93 + 125 950 450.77 − 128 058 143.65 = 2 346 459.05. Строка свода содержит entry_ids всех трёх исходных записей и is_cumulative = false; period = "2024 Q3".

Строка РБ по 60.01 зеркальна одной из двух позиций Счастливочной под 62.01 (в файлах: обороты 375 490 642.46 / 307 986 284.00, сальдо на конец 106 311 116.46) — внутригрупповые обороты сходятся копейка в копейку. В своде эта позиция видна только в сумме с соседней (строка 62.01 выше); разнесение по позициям и сопоставление ВГО — Срез 5.

Что ещё проверить руками:

  • контрагент, которого нет в одном из кварталов (как «Русский бисквит» на 60.02): в своде одна строка, пропущенный квартал дал нули, entry_ids — две записи;
  • GET …/osv?match_kind=INN — только строки с ИНН в файле (у Счастливочной есть колонка ИНН, у РБ нет), match_stored = true;
  • GET …/osv?unmapped=true&legal_entity_id=$LE_RB&q=счастлив — две строки (60.01, 62.…) до сопоставления; после K14 — ни одной, а summary.unmapped_total уменьшился ровно на число изменившихся строк свода (на этих данных — на 4, при propagated = 17 записей osv_entry: записи кварталов и пар-пробелов 66/76 считаются отдельно);
  • пробелы не зависят от фильтров свода: gaps одинаковы при любом GET …/osv/summary.

7.3 Сверка экспорта с API и Excel

  1. GET …/osv/export без фильтров → svod.xlsx; лист «Сводная ОСВ»: число строк без заголовка = rows_total, порядок — как в API (ЮЛ по имени, счёт, номер строки). Суммы — числа с двумя знаками (не текст): выделите колонку «Обороты Дт» и посмотрите сумму в строке состояния Excel; формат ячейки #,##0.00.
  2. Найдите строку «Русский бисквит» 60.01 Счастливочной и сверьте шесть сумм с таблицей из 7.2 и с ответом GET …/osv.
  3. Колонки «Сопоставлено с» и «Способ сопоставления»: для сопоставленных — имя ЮЛ из справочника и INN/MANUAL/PROPAGATED, для несопоставленных — пусто.
  4. Лист «Пробелы»: 7 строк, как в summary.gaps; с фильтром ?legal_entity_id= в экспорте лист «Пробелы» всё равно полный.
  5. Экспорт с ?unmapped=true после полного сопоставления — лист «Сводная ОСВ» только с заголовком, лист «Пробелы» заполнен.
  6. Текст контрагента, начинающийся с =, +, -, @ (сделайте такой в своём файле), в Excel показывается как текст, а не вычисляется как формула.

8. Сквозной сценарий в UI (arm-web)

Основной путь аналитика по макету: группа компаний → новая консолидация → файлы ЮЛ → проверка ОСВ и форм ББ/ОФР → сводная ОСВ. Номера в квадратных скобках — экраны макета docs/mockup/screens/<nn>-*.png (открывать по одному через docs/mockup/INDEX.md): если не ясно, «так ли должно выглядеть», сравнивайте с ними при окне 1600×1000. Права, коды ошибок и граничные случаи, которых нет в UI, — в разделах 6–7.

Предусловия. Стенд по разделу 4: ядро с NATS_ENABLED=true, tabular-parser на :8001, make seed-dev (ГК «Русский Бисквит» с ГСК из РБ и Счастливочной), фронт — make start. Для 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; «Требуется проверка» — только с замечаниями, например шапка другой организации). Панель опрашивает сервер раз в 5 с, пока есть файлы в обработке
W14 Загрузить ещё раз любой из этих файлов красный блок «Отклонено: 1» со строкой «<имя> — файл «<имя>» уже загружен для этого ЮЛ» (DUPLICATE_FILE), уведомление «Ни один файл не принят»
W15 Закрыть панель, обновить страницу (F5) статус версии в таблице — «Обработка документов» (K8)

8.4 Просмотр ОСВ-документа

№ Шаги Ожидание
W16 В панели Счастливочной клик по «ОСВ 60» экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Загружен», кнопка «✓ Подтвердить» неактивна; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка 60: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт)
W17 ka_go, через API (в UI не воспроизводится: загрузка только из панели ЮЛ внутри версии, см. раздел 8.7). Загрузить один файл из ../fixtures/osv/schastl/2024-Q3/ в QA-ЮЛ $LE_QA (id ЮЛ, заведённого для QA, не входящего в ГСК демо; без consolidation_version_id): 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
W18 Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (всего 22, список — 7.1): через «Загрузить документы» у каждого ЮЛ и подтверждение каждого ОСВ, либо скриптом из каталога arm-web: scripts/demo-prepare.sh <version_id> (id — из адреса ?version=) скрипт печатает строку на каждый файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает только неподтверждённые ОСВ (чистые уже «Загружен», автоподтверждение, OQ-157) и в конце печатает строк: 1385 документов: 22 несопоставленных: 1260 пробелов: 7. Статус версии — «Коррекция данных» (K9) появляется сразу после распознавания (автоподтверждение чистых документов, OQ-157), в «Организации» в колонке «ОСВ» — счета ЮЛ

demo-prepare.sh берёт пользователя вторым аргументом (по умолчанию ka_go@armka.local), адрес ядра — из API (по умолчанию localhost:8000/api/v1), каталог фикстур — из FIXTURES_DIR (по умолчанию fixtures рядом с arm-web).

8.5 Проверка формы ББ/ОФР из PDF [41], [41-02]

PDF из fixtures/ocr — отчётность других компаний (aep_6.pdf — ООО «Дорстроймонтаж», на 30.06.2025), поэтому ячейки ББ/ОФР чек-листа версии 2024 Q3 они не заполняют; для проверки экрана подойдёт любое ЮЛ версии. Распознаются только PDF, для которых в эмуляторе есть ответ (раздел 6.8).

№ Шаги Ожидание
W19 В панели Счастливочной загрузить ../fixtures/ocr/aep_6.pdf через 10–60 с два документа: «Бух. баланс · PDF» и «ОФР · PDF», статус «Требуется проверка», «Контрагент ✗» (в скане другая организация)
W20 Открыть «Бух. баланс» экран «Проверка распознавания»: слева «ИСХОДНЫЙ ДОКУМЕНТ» — страницы PDF «Страница 1…8», масштаб «−», «100%», «+» и сброс; справа «РЕЗУЛЬТАТ РАСПОЗНАВАНИЯ»: шапка «ИНН —», «Дата отчёта —» (оранжевый фон — не распознано), «Единица измерения тыс. руб.»; заголовок «Бухгалтерский баланс»; колонки «На 30 июня 2025 г», «На 31 декабря 2024 г.», «На 31 декабря 2023 г.»; разделы «АКТИВ», «I. Внеоборотные активы» … «ПАССИВ»; итоги разделов жирным; суммы без копеек, пустые — «-»; строка 1600 «БАЛАНС» — 293 709 / 179 791 / 189 847. Ячейки с уверенностью распознавания ниже 100 % — оранжевые, подсказка «Уверенность: N%»
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) открываются тем же экраном, но слева вместо скана — «Превью недоступно для этого формата» и кнопка «Скачать исходник». Документы «Прочий документ» (расшифровки) в панели не открываются.

8.6 Сводная ОСВ [30]

Предусловие — W18: все 22 ОСВ загружены и подтверждены.

№ Шаги Ожидание
W29 В строке версии «К отчётности →» экран «Русский Бисквит — 2024 К3», крошки «Отчётность › Русский Бисквит › 2024 · К3», справа «Статус отчёта» «Коррекция данных»; активна вкладка «ОСВ»; «Шахматка», «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий», «Подтверждение КФО клиента» неактивны (подсказка «Будет в следующих срезах»)
W30 Сводка под таблицей «Строк: 1385» (= rows_total, 7.2); жёлтый баннер «Требуется указать соответствия … Осталось: 1260.» (= 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]); «Строк» при «ГСК» + «ГК» + «Внешний» = 1385, «Строк» при «Внешний» = «Осталось» в баннере. «Сбросить» в окне фильтра возвращает все строки и закрывает окно
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), «Осталось» меньше 1260: алиасы переживают версии. Остальные цифры от этого не меняются.

8.7 Что в UI не считать багом (прототип)

  • Нет «Взять в работу», смены периметра, ручных переходов статусов, сопоставления контрагентов, экрана чек-листа, комментариев, удаления файлов, «Добавить версию» — это проверяется через API (разделы 6.6, 6.9, 7).
  • В своде не показываются «документов: 22» и колонка «Способ сопоставления» (её нет и в макете [30]; значение — в экспорте); фильтр «Тип контрагента» — одно значение, а не галочки, как в макете (API принимает один признак); фильтр «Счёт» принимает только корень (60, не 60.01).
  • Таблица консолидаций и статус версии обновляются по F5: после создания консолидации на странице той же ГК новой строки может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов.
  • «Распознан» (старые RECOGNIZED и формы после правки) и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка».
  • «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—».
  • Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии.
  • Шапку формы (ИНН, дату, единицу) нельзя очистить, только заменить; время принятия нарушений — в часовом поясе браузера, остальные даты — по Москве.
  • Ссылка ?version= с несуществующим id «выбирает» пустую версию; при 404 на своде может прийти до трёх сообщений; после истечения токена кроме «Сессия истекла» бывают лишние уведомления.
  • Предупреждения antd в консоли браузера («Static function can not consume context…», deprecated) ни на что не влияют.
  • Уведомления в /admin не появляются — дефект arm-web#7 (раздел 6.1).

Баг UI — issue в RSHB/arm-web (раздел 11): номер сценария W, скриншот, вкладка Network с запросом и ответом.

9. Известные ограничения (не заводить как баги)

  • Справочники — только через API; в UI arm-web — администрирование и путь консолидации (раздел 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-123); рекомендация кандидата — только при точном совпадении нормализованного имени (OQ-127).

  • PDF-путь (Срез 3a): эмулятор отвечает только на PDF из fixtures/ocr, остальные — INVALID_DOCUMENT (раздел 6.8, OQ-151); редакция fsbu4 не заполнена (OQ-150).

  • Парсер ОСВ: границы и «не баги» — в конце раздела 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-web, документация — RSHB/arm-ka-docs. Один дефект — одна issue. Не уверены, чей баг, — откройте вкладку Network: ответ сервера не тот, что в гайде, — arm-core; ответ верный, а экран показывает другое — arm-web.

Шаблон:

Срез: 1 / Сценарий: D16 (номер из гайда, если есть)
Пользователь: ka_go@armka.local (роль KA_GO)
Шаги: 1. … 2. …
Ожидание: 409 MEMBER_OF_OTHER_GROUP
Факт: 500 (тело ответа ниже)
Запрос: метод, путь, тело (без токена)
Ответ: статус и тело
Лог бэкенда: строки с трейсбэком, если есть
Окружение: ОС, браузер, версии docker/node, коммиты arm-core и arm-web (`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=/; запускайте make start (4.3)
После входа «Нет связи с сервером» или ошибки сети ядро не запущено на :8000; если на другом адресе — ARM_CORE_URL=… make start. Для старого arm-core/web — забыты REACT_APP_API_URL/REACT_APP_ROOT_PATH
Страница «Вход выполняется через РАИСА» это production-сборка; запускайте make start, а не make build
make start: порт 3000 занят запущен второй фронт (например, старый arm-core/web); остановите его
Скан 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 показывает 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-web/README.md.