Refs #24 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
66 KiB
Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 4)
Документ для ручного тестирования текущего main. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34,
Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов.
1. Что сейчас есть и чего нет
Есть (Срез 0 + Срез 1 + Срез 2c):
- Бэкенд
arm-core(FastAPI,/api/v1): вход по email в dev-режиме, профиль пользователя, права как данные. - API справочников: группы компаний (ГК), юрлица и ИП (ЮЛ) с историей, переводом между ГК и закрытием, алиасы контрагентов, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Видимость по филиалу: головной офис видит всё, филиал — только свои ГК.
- Импорт справочников из выгрузки ЕЦП CRM (xlsx/csv) с пробным прогоном и отчётом.
- Администрирование: пользователи, роли, матрица «роль × право», защита последнего администратора.
- Фронтенд: страница
/admin(«Пользователи и роли»). Другого UI нет. - Журнал аудита в БД (
dkb.audit_event). - Загрузка файлов ОСВ через API (
POST /legal-entities/{id}/files), распознавание реальнымtabular-parser, разбор результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ — раздел 6.7. Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. - Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и контрольные цифры — 7.1–7.3). UI консолидации — другой исполнитель.
Нет (это не баги):
- Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (
/docs) или curl. - Экрана загрузки и таблицы ОСВ (подсрез 2d), экранов консолидации (API есть, раздел 7), ББ/ОФР, распознавания PDF, шахматки, КФО — следующие срезы.
- Настоящего входа через 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 (бэкенд), 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).
3. Клонирование
Три репозитория должны лежать рядом, в одном каталоге, под именами docs, arm-core, fixtures (в коде пути
вида ../docs/...):
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
Без 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-пользователей
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; поднимайте
только его, без второго экземпляра ядра (он забирает часть сообщений recognition.result):
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 Фронтенд (второй терминал)
cd arm-core/web
npm install
REACT_APP_API_URL=http://localhost:8000 REACT_APP_ROOT_PATH=/ npm run start
Откроется http://localhost:3000/ с формой «Вход для разработки». Две переменные окружения обязательны: без них фронт ищет API на своём порту и путь приложения как на платформе банка.
Не используйте npm run build для тестирования: в production-сборке dev-формы входа нет, страница показывает
«Вход выполняется через РАИСА».
Настройки из 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.
4.4 Вход
В форме введите email dev-пользователя (не имя и не логин) и нажмите «Войти (dev)». Пароля нет. Это единственная форма
входа АРМ: страница с полями «Имя пользователя / пароль» — это Gitea, а не стенд. Токен живёт в текущей вкладке
(sessionStorage), новая вкладка — новый вход. Токен действует 8 часов.
| Роль | Филиал | Что видит | |
|---|---|---|---|
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
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)
| № | Шаги | Ожидание |
|---|---|---|
| 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 |
6.3 Импорт из ЕЦП CRM (пользователь admin, curl или Swagger)
Файл: fixtures/directories/ecp-export-russkiy-biskvit.xlsx (21 строка). Выполняйте на свежем стенде или после
сброса (раздел 9), иначе счётчики будут другими.
| № | Шаги | Ожидание |
|---|---|---|
| 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 |
6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)
Предусловия: раздел 4.2 (ядро с NATS_ENABLED=true, tabular-parser на :8001), файлы из ../fixtures/osv/. Выполняйте на
свежем стенде (раздел 9) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до
PROCESSED — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды).
Подготовка: пользователь ka_go (права DOCUMENT_EDIT, DOCUMENT_VIEW, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ —
АО "Русский бисквит", ИНН 3528052718 (если вы делали импорт I2 — найдите его в GET /directories/legal-entities?q=бисквит
и используйте существующее, иначе создайте как в D9). Для проверки «чужого филиала» понадобится ГК с филиалом.
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 -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 = RECOGNIZED, 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 |
200, status = CONFIRMED, confirmed_by/confirmed_at заполнены; повторный вызов — 409 ALREADY_CONFIRMED |
| U9 | ka_go | DELETE /files/{file_id} файла с подтверждённым документом |
409 FILE_HAS_CONFIRMED_DOCUMENTS, файл и строки на месте |
| 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 |
Аудит (6.4 — запрос из psql): после U1 — FILE_UPLOAD; U8 — 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 кв.») тоже |
| P4 | Колонки сумм | шесть сумм разнесены верно: сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт. Проверьте 2–3 строки с ненулевыми значениями во всех шести колонках и строку «Итого» |
| P5 | Иерархия | уровни строк (level, parent_row_number) повторяют дерево в Excel: счёт → субсчёт → контрагент → договор. Стратегия — в warnings `hierarchy=outline |
| P6 | Типы строк | group_type: ACCOUNT (код счёта), COUNTERPARTY, CONTRACT, DEPARTMENT, OTHER, TOTAL (строка «Итого», одна на лист). Строки «Вал.», «Кол.», «USD» → data_type CURRENCY/QUANTITY; «НУ» → NU |
| P7 | ИНН | если в таблице есть колонка ИНН (по заголовку или по содержимому, когда ≥ 80 % значений — 10/12 цифр), counterparty_inn заполнен у контрагентов и не заполнен у счетов и договоров; КПП и номера договоров за ИНН не принимаются |
| P8 | Числа | суммы — строки с двумя знаками ("1234567.89"); текстовые числа «1 234 567,89», «(1 234,00)», «—» разобраны как число, отрицательное, null; пустая ячейка — null, не 0 |
| P9 | Контрольные суммы | status = done: сальдо строки сходится (начало + обороты = конец), «сумма детей = родитель», «Итого» = корень. needs_check при расхождении, в checks — какие строки; «подгонки» под сходимость быть не должно |
| P10 | Лишние колонки | «КПП», «Код», «Количество» и прочие попали в extra строки, суммы не сдвинулись |
Реальные нестандартные файлы — ../fixtures/osv-dkb/ (пакет ДКБ 06.10.2026: шесть организаций, многолистовые книги,
вариант 2 «сумма», скользящий период, 50 тыс. строк; ожидаемые статусы и находки — osv-dkb/README.md) и
../fixtures/analysis-reports/ («Анализ счета», ожидается INVALID_DOCUMENT). Готовые кейсы синтетики — синтетика ../fixtures/osv/synthetic/s01…s16 с описанием каждого файла и
ожиданием в ../fixtures/osv/synthetic/README.md: шапка варианта 2 («Период: …», «Субконто»), уровни без outline (только
отступами, только по кодам счетов), строки валюты и количества, числа текстом, два листа ОСВ и посторонний лист, нет
сальдо на начало и свёрнутое сальдо на конец, колонка КПП, названия субсчетов, сломанная сумма (needs_check), повтор
заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков. Прогоните их способом Б и сверьте с README.
Как делать свои файлы. Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист:
- сдвинуть таблицу: вставить 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») и ИНН в ячейке над названием сейчас не извлекаются — задача
RSHB/arm-tabular-parser#9 (OQ-100, OQ-130). До неё строка уходит в ядро с именем целиком,
counterparty_innпуст. - 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.
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.
Подготовка. Нужна ГК с ГСК не менее чем из двух ЮЛ (ГК — в Ростовском РФ, чтобы её видел 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
Ожидание после подтверждения: 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 |
1381 до ручного сопоставления: 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
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 показывается как текст, а не вычисляется как формула.
8. Известные ограничения (не заводить как баги)
-
UI только для администрирования; справочники — через API.
-
Вход без пароля, открытый
/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— каркас, не влияет. -
Таблица пользователей показывает первые 200.
-
При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт
INVALID_INN. -
Консолидация (Срез 4): только API; маршрут согласования (
CONSOLIDATION_RFи далее), «Добавить версию», гудвилл, шахматка — следующие срезы; ББ/ОФР в своде не участвуют; блокировка версии не истекает по времени;period_checkдокументов остаётсяUNKNOWN;PROCESSINGв чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122). -
Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только при точном совпадении нормализованного имени (OQ-127).
-
Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (ИНН в тексте строки — arm-tabular-parser#9).
Полный список отложенных находок — Gitea RSHB/arm-core, issues «Follow-ups from #5» и «Follow-ups from #18» (#21).
9. Сброс стенда
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 стирает и их.
10. Как заводить баги
Gitea, репозиторий RSHB/arm-core (для документации — RSHB/arm-ka-docs). Один дефект — одна issue.
Шаблон:
Срез: 1 / Сценарий: D16 (номер из гайда, если есть)
Пользователь: ka_go@armka.local (роль KA_GO)
Шаги: 1. … 2. …
Ожидание: 409 MEMBER_OF_OTHER_GROUP
Факт: 500 (тело ответа ниже)
Запрос: метод, путь, тело (без токена)
Ответ: статус и тело
Лог бэкенда: строки с трейсбэком, если есть
Окружение: ОС, версии docker/node, коммит arm-core (`git rev-parse --short HEAD`)
Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке.
11. Типичные проблемы при запуске
| Симптом | Причина и решение |
|---|---|
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 |
| Фронт открылся, но после входа пусто или ошибки сети | забыты REACT_APP_API_URL/REACT_APP_ROOT_PATH при npm run start |
| Страница «Вход выполняется через РАИСА» | это production-сборка; запускайте npm run start, а не build |
| Импорт падает на записи файла в 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.