docs(openspec): спеки экранов из arm-web@de4e477 перенесены в arm-core (#148)

Спеки consolidated-osv-ui, consolidation-setup, document-review-ui, primary-files-ui
и архив changes demo-prototype, statements-v2. Каталоги архива statement-template-lines
и console-files в arm-core уже заняты другими изменениями и не переносились.
Ссылки на ../arm-web/openspec в CLAUDE.md и web/CLAUDE.md заменены на openspec/specs/*-ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-21 09:58:02 +04:00
parent 7e05cdedf6
commit a2b0a58fd0
18 changed files with 1034 additions and 4 deletions

View File

@ -182,8 +182,8 @@
бэкенд. Отдельный репозиторий `RSHB/arm-web` заморожен на `test` `de4e477` (= arm-core `3157a15`), синхронизация
`make -C ../docs sync-web` больше не используется; правило «`web/` напрямую не править» отменено. Проверки фронта —
`make web-check` (biome + tsc + build, входит в `check-full`) и из `web/`: `npm test` (vitest), `npm run lint`
(ESLint + Prettier), форматирование — `npm run format`. Спеки экранов (`*-ui`) пока лежат в `../arm-web/openspec/specs/`;
новые и изменённые требования к экранам — в `openspec/` этого репозитория.
(ESLint + Prettier), форматирование — `npm run format`. Спеки экранов — `openspec/specs/*-ui` (перенесены из arm-web@de4e477);
новые и изменённые требования к экранам — в `openspec/changes/` этого репозитория.
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
- `api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh` (`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`).
- OpenSpec: `openspec/` здесь; сквозные изменения — change в `../docs`.

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-08

View File

@ -0,0 +1,158 @@
## Context
arm-web выделен из `arm-core/web` командой фронта (3 коммита 06–07.10.2026) и дополнен экранами групп, консолидаций и
загрузки. Обзор 08.10.2026: список консолидаций и карточка версии работают с arm-core; загрузка теряет ошибки (нет
`await`, лишние поля формы); проверка документов (`PDFResultViewer`, `FileViewer`, `FileUploaderModel`) скопирована из
старого АРМ и ходит в `/fsl/...`, `/osv/by-file-id`, `POST /files` мимо клиента API; сводной ОСВ нет; `tsc` — 185 ошибок,
biome — 276.
Демо — 09.10.2026 на локальном стенде; затем прототип уходит в контур на тестирование заказчику и переезжает в
`arm-core/web`, где развивается дальше. Поэтому код держится соглашений шаблона `arm-core/web`.
Источник контракта — `../docs/dev/frontend-api-guide.md` (§5.5–§5.7) и схемы `arm-core/api/src/schemas/`.
## Goals / Non-Goals
**Goals:** аналитик на стенде проходит путь «создание консолидации → загрузка ОСВ и PDF → проверка и подтверждение →
сводная ОСВ» без сбоев и консоли разработчика; `make check` зелёный; все запросы — к API arm-core.
**Non-Goals:** см. proposal.md («Non-goals»).
## Decisions
### D-1. Чинить на месте, новое — по образцу arm-core/web (согласовано 08.10.2026)
Оставляем `api/client.ts`, `auth/*`, список консолидаций, карточку версии, модалку создания (с правками), Redux-слайсы
(типизируем). Новые экраны держат состояние в локальных хуках (`useStatement`, `useDocumentEntries`,
`useConsolidatedOsv`) как `arm-core/web/src/widgets/Admin/hooks.ts`. Альтернативы — генерация типов из OpenAPI (новая
dev-зависимость, доступность в Nexus не проверена) и переписывание с нуля (не успеть) — отклонены владельцем.
### D-2. Типы DTO руками
`src/types/` повторяют схемы arm-core только для используемых эндпоинтов: `Page<T>`, `UploadResult`, `FileOut`,
`DocumentBrief`, `DocumentOut`, `StatementOut` (`HeaderOut`, `ColumnOut`, `LineOut`, `ValueOut`, `ChecksOut`),
`EntryOut`, `ConsolidatedRowOut`, `OsvSummaryOut`, `VersionCard` (исправления: `is_head_entity: boolean`,
`entity_type: 'ЮЛ' | 'ИП' | null`, `osv_accounts: string[]`, `changed_by` — UUID). `Decimal` и даты приходят строками —
в типах `string`.
### D-3. Деньги — строки
Суммы не превращаются в `number`. Чистые функции `src/utils/money.ts`: `formatAmount(s)` — разделители разрядов и два
знака без арифметики над float; `parseAmountInput(s)` — нормализация ввода (пробелы, запятая → точка) и проверка
`^-?\d+(\.\d{1,2})?$`, пустое — `null`. Единицы ББ/ОФР — из `header.units`, ОСВ — рубли.
### D-4. Бинарные ответы — `apiFetchBlob`
`src/api/client.ts` получает `apiFetchBlob(path) → {blob, filename | null}` с той же обработкой токена, 401 и ошибок
(тело ошибки — JSON). Используется для скана (`blob.arrayBuffer()` → pdf.js) и экспорта (сохранение через
`URL.createObjectURL` + `<a download>`). Имя экспорта — из `Content-Disposition` (`filename*=UTF-8''…` или `filename=`),
иначе `exportFilename(group, year, quarter)`.
### D-5. Dev-proxy вместо CORS
`rsbuild.config.ts`: `server.proxy['/api'] → process.env.ARM_CORE_URL ?? 'http://localhost:8000'`; в dev
`REACT_APP_API_URL` пустой. Запросы идут с того же источника: нет CORS и читается `Content-Disposition` (follow-up
docs#58 O-1 на фронте обходится). В контуре SPA отдаёт сам arm-core — тот же источник, код не меняется. Гайд фронта §2
(где `REACT_APP_API_URL=http://localhost:8000`) остаётся верным для варианта без proxy.
### D-6. Просмотр PDF
Из `widgets/PDFResultViewer/IFrameBlock.tsx` сохраняется только постраничный рендер pdf.js (canvas, масштаб, листание);
загрузка — `apiFetchBlob('/files/{id}/content?disposition=inline')` → `arrayBuffer`. Компонент переименовывается в
`widgets/DocumentReview/PdfPages.tsx`. Worker pdf.js подключается из пакета `pdfjs-dist` сборкой (без CDN — в контуре
внешнего интернета нет). `PDFDataViewer.tsx`, `types.ts`, `styles.module.css` старого АРМ удаляются.
### D-7. Экран документа
`layouts/DocumentPage.tsx` (`/documents/:document_id`) → `GET /documents/{id}` → `widgets/DocumentReview/StatementReview`
(BALANCE/INCOME) или `widgets/DocumentReview/OsvDocument` (OSV). Чистые функции: `buildStatementGrid(statement)` —
матрица строк × колонок по `column_id` (не по индексу), `buildEntryTree(entries)` — дерево по `parent_entry_id` с
сохранением `row_number`. Правка ячейки — локальный черновик ячеек и «Сохранить» одним `PATCH` со всеми изменёнными
ячейками; ответ сервера заменяет состояние (сервер пересчитывает проверки и сбрасывает акцепт).
### D-8. Панель файлов
`widgets/FilesDrawer` переписывается: `UploadDocumentsModal` → один запрос со всеми файлами, итог из
`summarizeUpload(result)`; список — `FilesList` с вложенными документами; подписи — `fileStatusLabel`,
`documentTypeLabel`, `periodLabel`, `documentStatusLabel` (чистые функции). Опрос — `useDocumentsPolling` с остановкой,
когда нет `UPLOADED`/`PROCESSING`. Слайс `files` типизируется или заменяется локальным хуком (решает исполнитель по
месту, без смены поведения).
### D-9. Сводная ОСВ
`layouts/ConsolidatedOsvPage.tsx` + `widgets/ConsolidatedOsv/` (`useConsolidatedOsv`, `OsvFilters`, `OsvTable`,
`OsvGaps`). Таблица — antd `Table` с серверной пагинацией (`limit` 500). Фильтры — query-параметры как в API; ЮЛ —
из `VersionCard.members`.
### D-10. Тесты — vitest на чистых функциях
`vitest` (devDependency), `npm test` → `vitest run`, цель `make test` входит в `make check`. Покрытие по TDD: money,
`summarizeUpload`, подписи статусов/типов/периодов, `normalizeData` карточки версии, `exportFilename` и разбор
`Content-Disposition`, `buildStatementGrid`, `buildEntryTree`. Компоненты — ручной демо-сценарий.
## Демо-сценарий (приёмка)
Стенд по `../docs/dev/qa-guide.md` §4 и §6.8 (профиль `services`: tabular-parser, scan-gateway, scan-emulator),
`make seed-dev`, вход `ka_rf@armka.local` (или пользователь из гайда с правами EDIT на ГК «Русский бисквит»).
Данные стенда уже подготовлены: консолидация РБ ГСК 2024 Q3 создана (версия `0437f8be-019c-4dbb-9776-08c306aa38cc`),
`aep_6.pdf` уже загружен Счастливочной; повторы дают штатные 409 `CONSOLIDATION_EXISTS` / `DUPLICATE_FILE`.
1. Кнопка «Новая консолидация» — в боковом меню (действует на всё приложение); ГК «Русский бисквит» выбирается в модалке:
ГСК, период 2024 Q2 или 2025 Q3 (для 2024 Q3 — показать 409 как штатную ошибку) → члены ГСК отмечены → «Создать».
2. Шаги 2–4 — на подготовленной версии `0437f8be-…`. Панель «Организации» → РБ → «Файлы»: ОСВ
`fixtures/osv/rb/2024-Q{1,2,3}/*` и Счастливочной (`fixtures/osv/schastl/2024-Q{1,2,3}/*`) загружены, документы
подтверждены (на новой версии — загрузить и подтвердить на экране ОСВ).
3. ББ/ОФР: открыть уже загруженный ББ Счастливочной (или загрузить `fixtures/ocr/aep_6.pdf` РБ) → скан слева, форма справа,
исправить ячейку; ИНН и дата отчёта не распознаны — ввести в шапке дату `2024-09-30` (ГГГГ-ММ-ДД) и «Сохранить», иначе
409 `PERIOD_REQUIRED`; принять нарушения с комментарием (если есть), подтвердить.
4. «К отчётности» → сводная ОСВ: 1385 строк, 22 документа, 7 пробелов; фильтр Счастливочная + «бисквит» → строка `60.01`
по `qa-guide` §7.2; «Скачать xlsx».
Шаги API (2–4) перед браузерным прогоном проверяются curl-скриптом; браузерный прогон — владелец.
## Risks / Trade-offs
- [Круг «ядро + scan-gateway + эмулятор» на стенде целиком не прогонялся (`qa-guide` §6.8: «сервис проверяется
отдельно»)] → Задача 1 плана — прогон до кода. Если сломан: issue в arm-core/scan-gateway, а форма ББ/ОФР на демо —
из xlsx (`fixtures/statements-xlsx/mezheninovskaya`, `qa-guide` §6.9), скан — заглушка «превью недоступно».
- [Объём к завтра] → порядок задач от критичного к желательному; экспорт и пробелы — последними.
- [Ручные типы расходятся с API] → типы только по используемым полям, сверка со схемами при ревью.
- [Подтверждение 22 ОСВ кликами долго на демо] → в README — скрипт подтверждения из `qa-guide` §7.1; на демо можно
показать подтверждение одного документа, остальные подготовить заранее.
## Migration Plan
Один PR `feat/3-demo-prototype` → `test`. Откат — revert PR. Перенос в `arm-core/web` — отдельная задача после демо.
## Open Questions
- Какой ЮЛ «владеет» `aep_6.pdf` в фикстурах (ИНН в шапке) — влияет только на `counterparty_check` (не блокирует
подтверждение); выясняется в задаче 1.
## Decisions made without discussion
- 08.10.2026 — D-5 dev-proxy `/api` вместо `REACT_APP_API_URL=http://localhost:8000`. Обоснование: снимает CORS и
нечитаемый `Content-Disposition` без правок arm-core. Цена ошибки: низкая — переменная `REACT_APP_API_URL` продолжает
работать, proxy только в dev.
- 08.10.2026 — D-6 worker pdf.js из пакета, без CDN. Обоснование: в контуре нет внешнего интернета. Цена ошибки: низкая.
- 08.10.2026 — D-7 правка ячеек пакетом по «Сохранить», а не по каждой ячейке. Обоснование: меньше запросов, сервер
принимает до 5000 ячеек и пересчитывает проверки один раз. Цена ошибки: низкая — поведение меняется в одном хуке.
- 08.10.2026 — D-8: слайс Redux `files` заменён локальным хуком `useLegalEntityFiles` (список нужен только открытой панели). Цена ошибки: низкая.
- 08.10.2026 — R-1: Task 1 (проверка стенда, без записи в worktree) и Task 2 запущены параллельно. Обоснование: срок демо. Цена ошибки: нулевая.
- 08.10.2026 — R-2: замечание T4.I1 (частичный успех загрузки не обновляет список) отложено в Task 6, где модалка переписывалась; закрыто в Task 6. Цена ошибки: нулевая.
- 08.10.2026 — R-3, R-4: scoped re-review однострочного фикса T5.I1 и фикса T7.I1 (чистая функция с тестами) заменены проверкой контроллера по коду. Цена ошибки: низкая.
- 08.10.2026 — R-5: все находки финального ревью ветки исправлены одной fix-волной (384301e, 0f734e3, 67bc2c8), M-5 (тосты после 401) отложена. Цена ошибки: низкая.
- 08.10.2026 — R-6: `make start` задаёт `REACT_APP_ROOT_PATH=/` (иначе белый экран на dev-сервере, C-1). Цена ошибки: низкая — в контуре переменную задаёт платформа.
- 08.10.2026 — R-7: по замечаниям владельца (H-1, H-2) и сверке всех экранов с PNG макета (`docs/mockup/screens/`) экраны
[03], [30], [34], [39], [40], [41], [42] приведены к макету в той же ветке. Цена ошибки: низкая — только вёрстка.
- 08.10.2026 — R-8: суммы — запятая и разделители разрядов (`12 275,68`); в своде и ОСВ ноль показывается «—», в формах
ББ/ОФР — без копеек и «-» для пустых (как в макете). Цена ошибки: низкая — одна функция `formatAmount`.
- 08.10.2026 — R-9: даты загрузки и создания показываются по Москве (`Europe/Moscow`), а не по UTC-строке. Цена: низкая.
- 08.10.2026 — R-10: [34] «Дата верификации» — «—» (в `VersionBrief` нет поля, до маршрута согласования), «ГСК» — «—»
(П-3). Цена: низкая.
- 08.10.2026 — R-11: [30] вкладки Шахматка/КФО/… показаны неактивными (Срезы 5–6); фильтры свода — в заголовках колонок,
признак контрагента — в фильтре «Контрагенты». Цена: низкая.
- 08.10.2026 — R-12: [41] заголовок колонки формы без распознанной подписи — «На <дата периода>» или «Колонка N»; длинный
список нарушений — с прокруткой (120 px). Цена: низкая.
- 08.10.2026 — R-13 (согласовано с владельцем в чате): форма ББ/ОФР [41] раскладывается «как на бумаге» на фронте —
порядок строк и заголовки разделов восстанавливаются по соотношениям §7 (`formLayout.ts`), итоги XX00 жирные (кроме 2900),
строки вне формы — «Прочие строки»; приклеенные OCR заголовки разделов срезаются только при показе (полное имя — в
подсказке). ИНН — только в рамке кодов; ОКУД — по типу формы, если не распознан; длинное название — 2 строки. Причина:
сервер отдаёт строки по коду и без строк-разделов (§12, OQ-36). Цена ошибки: низкая — только показ, данные не меняются;
при появлении порядка строк в API (issue arm-core) раскладку можно упростить.
- 08.10.2026 — R-14 (по замечанию владельца H-5): шапка формы [41] — как в старом АРМ (`reference-old-arm`
`PDFResultViewer`): компактная таблица ИНН / Дата отчёта / Единица измерения (подсветка уверенности и правки, правка в
режиме «Редактировать»), затем «Бухгалтерский баланс на …». Реквизиты бумажной шапки макета (ОКПО, ОКОПФ, ОКФС, адрес,
название организации) не показываются: распознавание их не отдаёт или отдаёт мусор (arm-scan-gateway#15). Заменяет
шапку из R-13. Цена ошибки: низкая — один компонент `StatementHeaderBlock`.

View File

@ -0,0 +1,57 @@
## Why
В пятницу 09.10.2026 — демо владельцу и бизнесу, затем прототип уходит в контур банка на тестирование заказчику
(и переезжает внутрь `arm-core/web`). Сейчас arm-web не проводит аналитика по пути «консолидация → первичка →
проверка → сводная ОСВ»: загрузка теряет ошибки, проверка документов ходит в эндпоинты старого АРМ, экрана сводной ОСВ
нет, `make check` красный (tsc — 185 ошибок, biome — 276).
## What Changes
- Модалка «Новая консолидация» [42]: предотметка членов ГСК, догрузка всех ЮЛ ГК, ошибки API, переход к новой версии.
- Панель «Файлы ЮЛ» [39]/[40] переписана: загрузка в контексте версии с итогом «принято / отклонено», файлы со
вложенными документами, переход к документу, опрос до окончания распознавания.
- Новый экран документа `/documents/:document_id`: для ББ/ОФР [41]/[41-02] — скан PDF рядом с формой, правка ячеек,
проверки и их акцепт, подтверждение; для ОСВ — дерево строк только для чтения и подтверждение.
- Новый экран «Сводная ОСВ» [30] `/groups/:group_id/versions/:version_id/osv`: таблица, фильтры, пробелы, счётчик
несопоставленных, экспорт xlsx.
- `api/client.ts`: `apiFetchBlob` для бинарных ответов (скан, xlsx); dev-proxy `/api` в `rsbuild.config.ts`.
- **BREAKING (внутреннее):** удалены `layouts/FileViewer.tsx`, `components/FileUploaderModel.tsx`,
`widgets/PDFResultViewer/PDFDataViewer.tsx`, `services/api/mocks.ts`; маршрут `/files/:file_id` заменён на
`/documents/:document_id`.
- `make check` зелёный: biome + tsc + vitest + build; добавлен `vitest` (dev) для чистых функций.
## Capabilities
### New Capabilities
- `consolidation-setup`: создание консолидации ГК за период из модалки и переход к её версии.
- `primary-files-ui`: загрузка первичных файлов ЮЛ в контексте версии и список файлов/документов со статусами.
- `document-review-ui`: проверка и подтверждение документа — форма ББ/ОФР со сканом PDF, ОСВ-документ.
- `consolidated-osv-ui`: сводная ОСВ версии — просмотр, фильтры, пробелы, экспорт.
### Modified Capabilities
(нет — `openspec/specs/` пуст)
## Impact
- Экраны макета: [03], [34], [39], [40], [41], [41-02], [42], [30].
- Эндпоинты arm-core (`/api/v1`), только существующие:
`GET /directories/company-groups`, `GET /directories/company-groups/{id}/related-groups`,
`GET /directories/related-groups/{id}`, `GET /directories/legal-entities`, `POST /consolidations`,
`GET /company-groups/{id}/consolidations`, `GET /consolidations/versions/{id}`,
`POST /legal-entities/{id}/files`, `GET /legal-entities/{id}/files`, `GET /files/{id}/content`,
`GET /documents/{id}`, `GET /documents/{id}/entries`, `POST /documents/{id}/confirm`,
`GET|PATCH /documents/{id}/statement`, `POST /documents/{id}/statement/checks/accept`,
`GET /consolidations/versions/{id}/osv`, `GET …/osv/summary`, `GET …/osv/export`.
- Зависимости: `vitest` (devDependency).
- Код: `src/api/`, `src/services/api/`, `src/types/`, `src/widgets/{NewConsoleModal,FilesDrawer,ConsoleTable,…}`,
новые `src/widgets/DocumentReview/`, `src/widgets/ConsolidatedOsv/`, `src/layouts/`, `src/App.tsx`, `rsbuild.config.ts`,
`Makefile`, `README.md`.
## Non-goals
- Блокировка версии («Взять в работу»), сопоставление контрагентов [32-*], экран чек-листа [37], комментарии,
удаление файлов, смена периметра и ручные переходы статусов.
- Правка строк ОСВ (в API нет), картинки страниц и координаты ячеек в скане (в API нет).
- Генерация типов из OpenAPI; автотесты компонентов.
- Перенос в `arm-core/web` и сборка под контур — отдельной задачей после демо.
- Изменения arm-core / scan-gateway: найденный пробел — отдельная issue в том репозитории.

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Сводка версии
Экран «Сводная ОСВ» [30] SHALL показывать шапку версии (ГК, период, номер, статус) и сводку
`GET /consolidations/versions/{id}/osv/summary`: строк всего, документов, несопоставленных (`unmapped_total` — счётчик
«Осталось N» без перехода к сопоставлению), автоматически сопоставленных.
#### Scenario: Контрольные цифры РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3 после подтверждения 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}`
- **THEN** «строк» = 1385, «документов» = 22, «несопоставленных» = 1381
### Requirement: Таблица свода и фильтры
Экран SHALL показывать строки `GET /consolidations/versions/{id}/osv` постранично (размер страницы ≤ 2000): ЮЛ, счёт,
контрагент, ИНН контрагента, признак контрагента (ГСК/ГК/Внешний), способ сопоставления, сальдо на начало Дт/Кт,
обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с разделителями разрядов). Фильтры: ЮЛ (`legal_entity_id`),
корень счёта (`account_root`), признак контрагента (`counterparty_type`), только несопоставленные (`unmapped`), поиск по
контрагенту (`q`, ≤ 500 символов). Смена фильтра SHALL перечитывать первую страницу.
#### Scenario: Строка из контрольных цифр
- **WHEN** фильтр ЮЛ «Пекарня Счастливочная» и поиск «бисквит»
- **THEN** строка `60.01` показывает сальдо нач. Кт 4 454 151.93, обороты Дт 128 058 143.65, Кт 125 950 450.77,
сальдо кон. Кт 2 346 459.05, признак «ГСК»
### Requirement: Пробелы
Экран SHALL показывать `gaps[]` сводки списком «ЮЛ — счёт — недостающие кварталы» с пояснением, что строки этих пар в
своде отсутствуют.
#### Scenario: Пробелы РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3
- **THEN** показано 7 пробелов, у каждого недостающие кварталы «Q1, Q2»
### Requirement: Экспорт
Кнопка «Скачать xlsx» SHALL получать `GET /consolidations/versions/{id}/osv/export` с текущими фильтрами через клиент API
с токеном и сохранять файл; имя — из `Content-Disposition`, а если заголовок недоступен —
`Сводная_ОСВ_<ГК>_<год>_<квартал>.xlsx`.
#### Scenario: Экспорт с фильтром
- **WHEN** аналитик скачивает свод с фильтром по ЮЛ
- **THEN** сохраняется xlsx, а ошибка сервера показывается текстом, а не скачивается

View File

@ -0,0 +1,39 @@
## ADDED Requirements
### Requirement: Состав модалки «Новая консолидация»
Модалка [42] SHALL предлагать выбор ГК (`GET /directories/company-groups`), ГСК этой ГК с `kind = gsk`
(`GET /directories/company-groups/{id}/related-groups`), года и квартала, и показывать **все** незакрытые ЮЛ ГК
(`GET /directories/legal-entities?company_group_id=&closed=false`, страницы дочитываются по `offset`, пока не получены
все `total`). Члены выбранной ГСК (`GET /directories/related-groups/{id}` → `members[]`) SHALL быть отмечены
автоматически; пользователь может снять или добавить отметки.
#### Scenario: Выбор ГСК отмечает её членов
- **WHEN** аналитик выбирает ГК «Русский бисквит» и её ГСК
- **THEN** в списке ЮЛ видны все ЮЛ ГК, а члены ГСК («АО "Русский бисквит"», «ООО "Пекарня Счастливочная"») отмечены
#### Scenario: ЮЛ больше одной страницы
- **WHEN** у ГК больше ЮЛ, чем `limit` одной страницы
- **THEN** модалка показывает все ЮЛ ГК, а не первую страницу
### Requirement: Создание и переход к версии
По «Создать» модалка SHALL отправить `POST /consolidations {company_group_id, related_group_id, year, quarter,
legal_entity_ids}` с отмеченными ЮЛ. При 201 модалка SHALL закрыться, список консолидаций ГК — перечитаться, а созданная
версия (`VersionCard.id`) — открыться в карточке. При ошибке модалка SHALL остаться открытой и показать `message` из
`ApiError` (коды `CONSOLIDATION_EXISTS`, `RELATED_GROUP_MISMATCH`, `ENTITY_NOT_IN_GROUP`, `PERIMETER_TOO_SMALL` и 422
валидации).
#### Scenario: Успешное создание
- **WHEN** аналитик создаёт консолидацию 2024 Q3 с периметром из ГСК
- **THEN** открыта карточка версии №1 со статусом «Инициация» и двумя ЮЛ в панели «Организации»
#### Scenario: Консолидация за период уже есть
- **WHEN** сервер отвечает 409 `CONSOLIDATION_EXISTS`
- **THEN** модалка не закрывается и показывает текст ошибки сервера
### Requirement: Переход к сводной ОСВ из карточки
Кнопка «К отчётности» карточки версии SHALL вести на `/groups/:group_id/versions/:version_id/osv` и быть доступна в
любом статусе версии.
#### Scenario: Переход к своду
- **WHEN** аналитик нажимает «К отчётности» у версии
- **THEN** открывается экран сводной ОСВ этой версии

View File

@ -0,0 +1,64 @@
## ADDED Requirements
### Requirement: Экран документа
Маршрут `/documents/:document_id` SHALL загрузить `GET /documents/{id}` и показать вид по `document_type`: `BALANCE` и
`INCOME` — проверка формы, `OSV` — просмотр ОСВ; для прочих типов — карточку документа без данных. Шапка экрана SHALL
показывать тип, ЮЛ-владельца, период и статус документа, кнопку «Назад».
#### Scenario: Открытие формы баланса
- **WHEN** аналитик открывает документ `BALANCE`
- **THEN** показан экран проверки формы
### Requirement: Скан рядом с формой
Для ББ/ОФР экран SHALL показывать исходный файл документа: PDF — постранично через pdf.js из байт
`GET /files/{file_id}/content?disposition=inline`, полученных клиентом API с токеном (не через `<iframe>` и не прямой
ссылкой); изображение (png/jpg) — тегом `<img>` из blob; xlsx — без превью, с кнопкой «Скачать исходник».
#### Scenario: PDF под CSP sandbox
- **WHEN** сервер отдаёт PDF с `Content-Security-Policy: sandbox`
- **THEN** страницы PDF отрисованы на canvas и листаются
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`. Ячейки с
`confidence < 100` или `is_recognized = false` SHALL подсвечиваться как «требует внимания», с `is_modified = true` — как
«исправлено» с исходным значением `original_amount` в подсказке. Единицы (`header.units`) SHALL выводиться над таблицей.
Пока документ не `CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после
точки, знак минус допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values:
[{line_id, column_id, amount}]}` суммой-строкой, ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта,
единицы) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется.
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Проверки и их акцепт
Экран SHALL показывать `checks`: жёсткие (`failed`), мягкие (`soft_failed`), межформенную (`cross_form`) и акцепт
(`accepted`: кто, когда, комментарий). При непустом `failed` и `accepted = null` SHALL быть доступно «Принять с
комментарием»: комментарий 1–2000 символов → `POST /documents/{id}/statement/checks/accept {comment}`.
#### Scenario: Акцепт нарушений
- **WHEN** есть жёсткие нарушения и аналитик принимает их с комментарием
- **THEN** блок проверок показывает акцепт, нарушения остаются видимыми
### Requirement: Подтверждение документа
Кнопка «Подтвердить» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`. При 409
`CHECKS_NOT_PASSED` экран SHALL показать `detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать
исправить дату отчёта; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
### Requirement: ОСВ-документ
Для `OSV` экран SHALL показывать строки `GET /documents/{id}/entries` (все страницы, `limit` ≤ 2000) деревом по
`parent_entry_id` с колонками: счёт/субсчёт, контрагент, ИНН, сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец
Дт/Кт (суммы — строками с разделителями разрядов, рубли), только чтение. Кнопка «Подтвердить» — как для форм.
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`

View File

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /legal-entities/{id}/files?consolidation_version_id={version_id}` (multipart, поле `files` повторяется), без
других полей формы. Тип и период документа не спрашиваются. После ответа панель SHALL показать итог: принятые файлы
(`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом `UploadResult` SHALL
трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail` (`ENTITY_NOT_IN_PERIMETER`)
и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
### Requirement: Список файлов и документов ЮЛ
Панель SHALL показывать файлы ЮЛ из `GET /legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов есть `UPLOADED` или `PROCESSING`, панель SHALL перечитывать список раз в 5 секунд и
SHALL прекратить опрос, когда таких файлов нет или панель закрыта. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы ЮЛ перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-18

View File

@ -0,0 +1,90 @@
## Context
Реализация плана 6 сквозного change `unified-statement-normalization`
(`../docs/openspec/changes/unified-statement-normalization/`, спека `specs/arm-web/spec.md`). Вся логика экрана —
чистые функции в `.ts` с vitest-тестами, компоненты React только рисуют их результат. Черновик правок аналитика —
одна структура `StatementDraft`, из которой одна функция собирает тело единственного `PATCH`. Подробный план —
`docs/superpowers/plans/2026-10-18-statements-v2.md`.
## Decisions made without discussion
Дата: 2026-10-18. Решения плана, принятые без обсуждения с владельцем; если решение неверно, цена ошибки — в
последнем столбце.
| Маркер | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| P6-1 | Подсказка кода — `StatementOut.edition_lines` (справочник строк редакции формы, у формы без даты — всех редакций; план 3 P3-22), затем коды строк `ok` файла, которых в справочнике нет (имя — `reference_name` ?? `line_name`); отдельного запроса нет | справочник приходит с ответом формы | нет |
| P6-2 | Якорь строки `unreadable` / `missing` — строка `ok` из источника (`row_index` не `null`) с наибольшим `row_index` меньше своего; якорь `detail` — строка `ok` из источника с `row_index = parent_row_index`; нет якоря (в том числе `detail` с `parent_row_index = null` или указывающим не на `ok`) — в начало первого раздела (после ведущих заголовков разделов; у ОФР — в начало таблицы). Вложение встаёт сразу после строки-якоря; несколько вложений одного якоря — по `row_index` | спека `arm-web` «Раскладка», S-62 («строка `detail` без родителя на экране — в начале раздела»), P3-17 (тот же порядок вложений в ответе ядра) | нет |
| P6-3 | Скрытие (пустые подстроки, расшифровки) применяется **после** раскладки, фильтром | иначе скрытый якорь сдвинул бы вложения к соседу | нет |
| P6-4 | Справочное имя (`reference_name`) — основное; файловое (`line_name`, после срезания приклеенных заголовков разделов) показывается вторым серым текстом, только если отличается после нормализации: нижний регистр, `ё` → `е`, оставлены только буквы и цифры | переносы OCR («задолжен-ность») не должны давать ложного «отличается» | лишнее или пропущенное второе имя — только показ |
| P6-5 | Маркеры «у места» строятся из **всех** записей `issues` (в том числе `relation` / `soft` — у них `line_ids` + `column_id`); список `IssuesPanel` показывает только `kind = warning`; нарушения §7 остаются списком в `ChecksPanel` (из `checks`, с акцептом) и получают ссылку «Показать» по `line_ids[0]`. Запись с `column_id` без `line_ids` (например, `COLUMN_WITHOUT_DATE`) показывается у заголовка колонки; поле шапки, которого нет в блоке шапки, и строки, которых нет в ответе, — у документа | одна запись не дублируется в двух списках; заголовок колонки — естественное место для предупреждения о дате колонки | нет |
| P6-6 | Подтверждение ячейки — в черновике; доступно, только если у строки есть `id` и у значения есть `id` (у достроенных ячеек — нет, ядро ответит `UNKNOWN_CELL`); сумма в черновике той же ячейки отменяет её подтверждение | `PATCH`: в элементе `values` ровно одно из `amount` / `confirmed`; правка суммы и так снимает `needs_review` | нет |
| P6-7 | Одно действие на строку в черновике (`code` / `detail` / `confirm`), новое заменяет прежнее; пустое поле кода и код, равный текущему, — не действие; у достроенных шаблонных строк (`id = null`) действий строки нет | 422 `INVALID_LINE_PATCH` при двух действиях; `lines[]` адресуется только `line_id` | нет |
| P6-8 | Сумма в шаблонную строку по `line_code` X и назначение кода X другой строке в одном сохранении — ошибка черновика «Код X назначается строке, а в шаблонную строку X введены суммы — сохраните по очереди», запрос не уходит | ядро ответит 422 `UNKNOWN_CELL` (P3-10), а сообщение ядра не говорит, что делать | нет |
| P6-9 | Ошибка элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `LINE_CODE_REQUIRED`) показывается у поля кода строки `detail.line_id` (текст сервера в подсказке поля и `message.error`); строки с этим `id` на экране нет — текстом над таблицей | ядро отдаёт `detail.line_id` и `detail.line_code` (план 3 P3-24) | нет |
| P6-10 | Строка «неразобранная» для кнопки «Подтвердить данные» — `line_status ∈ {unreadable, missing}` и хотя бы у одного значения `amount !== null` или `string_value !== null` (ровно условие ядра для 409 `LINES_UNRESOLVED`) | кнопка и сервер считают одинаково | нет |
| P6-11 | Подтверждение даты отчёта — кнопка «Подтвердить дату» у поля из `needs_review_fields` (уходит `header.report_date` тем же значением); дата колонки — поле `ГГГГ-ММ-ДД` у заголовка колонки (как поле даты отчёта) с кнопкой «Подтвердить дату», если дата есть и у колонки `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` | перепечатывать ту же дату ради подтверждения неочевидно; формат ввода — как у существующего поля даты | нет |
| P6-12 | Панель файлов: у строки документа — значок с числом «предупреждения файла (`job.warnings`) + `issue_counts.error` + `issue_counts.info`» документа, красный при хотя бы одном `error` (в файле или `issue_counts.error > 0`); у строки файла без документов — только `job.warnings`; нуль — значка нет. Клик открывает список: предупреждения файла (`message` и `locator`), `job.mapping_warnings` (как `info`) и `DocumentOut.issues` документа, загружаемые при открытии (`GET /documents/{id}`) | `DocumentBrief.issue_counts` (план 3 P3-23) помечает документ без запроса на каждый; подробности — по клику | акцептованное нарушение тоже даёт значок — подробности в списке |
| P6-13 | `job.warnings` на клиенте нормализуется так же, как ядро: строка → `{code: "LEGACY_WARNING", severity: "info", message, locator: null}`, объект с `code`, `severity ∈ {error, info}`, `message` — как есть, прочее пропускается, не список → `[]` | экран не падает при ошибке порядка выкладки | нет |
| P6-14 | Сценарий T11 — на локальном стенде; строки `unreadable` / `missing` / `detail` и записи `issues` всех видов подставляются перехватом `GET` и `PATCH …/statement` (`page.route`) из фикстуры плана, пока сервисы шлют 1.x; на реальных формах 1.x — регрессия | до шага 5 выкладки 2.0-данных на стенде нет | повторный прогон на `aep_3` после шага 5 — follow-up |
| P6-15 | Цвета: ячейка `needs_review` — `#fff7e6`, исправлено — `#e6f4ff`, строка `unreadable` / `missing` целиком — `#fff1f0`, строка `ok` с `needs_review` — фон ячеек наименования и кода `#fff7e6`; подтверждённая ячейка — маленькая серая галочка перед суммой; уровень `error` — красный значок `ExclamationCircleOutlined` `#cf1322`, `info` — синий `InfoCircleOutlined` `#1677ff` | палитра уже используется на экране и в панели файлов | только вид |
| P6-16 | Ячейка кода: `ok` — код; `unreadable` — серым `line_code_text` (пусто — «—»), подсказка «Код не распознан: «<текст>»»; `missing` — серым «—», подсказка «Кода в строке нет»; `detail` — пусто, подсказка «Расшифровка — в суммы формы не входит»; у `ok` в подсказке «Уверенность кода: N%» (если меньше 100 и код не правлен), «Код подтверждён», «Код назначен аналитиком» | спека: «в ячейке кода серым — сырой текст с подсказкой» | только вид |
| P6-17 | `header.source.<поле>.confidence` — `string \| null` (строка Decimal, как прочие уверенности API, план 3 P3-20), в подсказке — `Number()` и округление до целого | одно правило для всех уверенностей | нет |
| P6-18 | `node_modules` worktree — симлинк на установленный каталог с eslint, prettier и vitest 3.1.4; поэтому вместо `make check` (он начинается с `npm install` и поставил бы пакеты в чужой каталог) гоняется `make lint typecheck test build` — те же проверки без установки | память владельца: симлинк, как в прежних срезах | нет; в описании PR сказано, что install пропущен |
| P6-19 | Предупреждения ОСВ-документа (`DocumentOut.issues`, `OSV_*`) — блок `IssuesPanel` над таблицей экрана ОСВ, все записи — у документа (строки ОСВ не адресуются `line_ids`) | спека: «предупреждения листа ОСВ … из `DocumentOut.issues[]`»; экран документа уже загружает `DocumentOut` | нет |
| R-1 | Шаги «сверка с макетом / прогон в браузере» (T3 s5, T4 s4, T7–T10) перенесены в T11 (дата 2026-10-18) | нет стенда ядра с v5.19, общий Playwright-браузер делят параллельные дорожки | визуальные дефекты найдутся позже, в T11 |
| R-2 | Ссылка «Показать» также у проверок со статусом `soft_failed` (дата 2026-10-18) | согласованное расширение: нарушение так же адресуется строкой | только показ |
| R-3 | Компонент меню строки назван `LineMenuButton.tsx`, а не `LineMenu.tsx` (дата 2026-10-18) | на регистронезависимой ФС (macOS) `LineMenu.tsx` и `lineMenu.ts` дают TS1149 | нет |
| O-1 | Два MODIFIED-требования («Форма ББ/ОФР — просмотр и правка», «Подтверждение документа») составлены исполнителем из текущей спеки: в docs-спеке `arm-web` все требования ADDED. «Предупреждения ОСВ-документа» (P6-19) в docs-спеке нет; написано по плану. Сверить в arm-ka-docs#116 (дата 2026-10-18) | иначе change нельзя было бы описать дельтой | расхождение формулировок со спекой docs при архивации |
| T1.D-1 | `cellFlags` возвращает ещё и `confirmed`; двум старым ожиданиям добавлен `confirmed: false`; тест «внимание по уверенности» заменён на «внимание только по needs_review» (дата 2026-10-18) | `toEqual` падает на новом ключе; так велит бриф | только тесты |
| T1.D-2 | В тестовый хелпер `doc()` добавлен `issue_counts: {error: 0, info: 0}` (дата 2026-10-18) | поле обязательно в `DocumentBrief`, иначе падает tsc | нет |
| T1.D-3 | Значения фикстуры `statement-v2.json`, которых нет в брифе, придуманы исполнителем (форматы сумм, уверенности, `text`, UUID, `codes`, сообщения, `is_current`, пустая колонка c2, порядок `edition_lines`) (дата 2026-10-18) | фикстура должна быть полной | тесты завязаны на выдуманные значения; реальные данные — в T11 |
| T1.D-4 | Фикстура приведена к `StatementOut` через `as` в тесте (дата 2026-10-18) | импорт JSON расширяет литералы до `string`; полноту проверяет рантайм-тест пар «строка × колонка» | слабая проверка типов фикстуры |
| T1.D-5 | Тест «переносит поля v2» проверяет ключи по `l-u7`, `code-1250`, `l-1230`, `l-d2` (дата 2026-10-18) | ровно как в брифе | нет |
| T3.D-1 | Несколько строк подсказки кода склеиваются переводом строки; у `codeModified` уверенность не показывается (дата 2026-10-18) | читаемость подсказки | только вид |
| T3.D-2 | Подсказка сумм принимает необязательный `issues` (добавляет message) (дата 2026-10-18) | задел для T4 | нет |
| T3.D-3 | Уверенность округляется `Math.round(Number())` (P6-17) (дата 2026-10-18) | одно правило для всех уверенностей | нет |
| T3.D-4 | В режиме правки у подтверждённой ячейки галочка — `prefix` поля ввода (дата 2026-10-18) | ячейка в режиме правки — Input | наложение значков, см. T8.D-3 |
| T3.D-5 | `id` строки `row-<key>` выставляется только на строках-линиях, не на заголовках разделов (дата 2026-10-18) | якорь прокрутки нужен только строкам | нет |
| T4.D-1 | Подписи мест полей шапки: organization_name — «Организация», inn — «ИНН», report_date — «Дата отчёта», units — «Единица измерения»; неизвестное поле — своим кодом, запись уходит в `document` (дата 2026-10-18) | P6-5 | только вид |
| T4.D-2 | Запись с `field` из блока шапки всегда в `byField`, независимо от `line_ids` (дата 2026-10-18) | поле шапки — самостоятельное место | нет |
| T4.D-3 | `warning` с `line_ids` без `column_id` — место «строка …», с `column_id` — «<строка>, колонка «…»»; `relation` / `soft` в список не входят (дата 2026-10-18) | P6-5 | только вид |
| T4.D-5 | `onShow` панелей передаёт ключ строки; для достроенных строк (`id = null`) перехода нет — `line_ids` их не содержат (дата 2026-10-18) | нет id — нет адреса | нет ссылки «Показать» у достроенной строки |
| T4.D-6 | Значок у наименования строки — только по `byRow`; записи `byCell` дают значок только в ячейке (дата 2026-10-18) | не дублировать | только вид |
| T5.D-1 | Кнопка «Подтвердить дату» рисуется только при переданном `onConfirmDate` (дата 2026-10-18) | иначе мёртвая кнопка | нет |
| T5.D-2 | У организации в подсказке при modified — «Распознано: «…»»; нет `source` — подсказки нет (дата 2026-10-18) | нечего показывать | нет |
| T5.D-3 | Уверенность в подсказке шапки через `Number()` ('67.00' → 67%) (дата 2026-10-18) | P6-17 | нет |
| T5.D-4 | Placeholder организации — «Наименование организации» (текст не из плана) (дата 2026-10-18) | поле в режиме правки должно быть подписано | только вид |
| T6.D-1 | `buildStatementPatch` для суммы в неизвестную строку возвращает ошибку `amount` (раньше бросалось исключение) (дата 2026-10-18) | экран не должен падать | нет |
| T6.D-2 | Подтверждение ячейки и действие строки у строк без `id` молча пропускаются (дата 2026-10-18) | UI их не создаёт (P6-6, P6-7) | молчаливая потеря действия при ошибке UI |
| T6.D-3 | Код сравнивается после `trim`; пустой или равный текущему — не действие и не ошибка, проверка до формата (дата 2026-10-18) | P6-7 | нет |
| T6.D-4 | Конфликт P6-8 ищется по сумме в шаблонную строку (`id = null`) с тем же кодом; `key` ошибки — `lineId` строки с действием (дата 2026-10-18) | P6-8 | нет |
| T6.D-5 | Тексты ошибок: «Наименование организации — не длиннее 1000 символов» (длина организации); «Исправьте суммы с неверным форматом» (суммы, как раньше) (дата 2026-10-18) | тексты не из плана | только вид |
| T6.D-6 | `confirm_report_date` при отсутствии текущей даты ничего не отправляет (дата 2026-10-18) | нечего подтверждать | пустое тело PATCH, «Сохранить» молчит |
| T6.D-7 | Ошибка `amount` подсвечивает ячейку красной рамкой и сама по себе (дата 2026-10-18) | раньше подсветка шла только по разбору формата | только вид |
| T7.D-2 | Подпись пункта меню: «Это расшифровка» для `unreadable` / `missing`, «Сделать расшифровкой» для `ok` (дата 2026-10-18) | разные смыслы действия | только вид |
| T7.D-3 | Меню строки у `ok` — только при `origin = 'source'`; шаблонные строки с id меню не получают (дата 2026-10-18) | бриф | нет |
| T7.D-4 | `lineErrorTarget` читает `ApiError.body.detail` (отдельного поля `detail` у `ApiError` нет) (дата 2026-10-18) | иначе деталей ошибки не достать | нет |
| T7.D-5 | Ошибка сервера сбрасывается любой правкой черновика (в `setDraft`) (дата 2026-10-18) | иначе устаревшая ошибка висит после правки | нет |
| T7.D-6 | У `unreadable` / `missing` с действием detail / confirm поле кода остаётся, рядом тег (дата 2026-10-18) | бриф: поле всегда | см. T7.2: очистка поля теряет действие |
| T8.D-1 | Подтверждение ячейки без `id` значения молча пропускается в `buildStatementPatch`; кнопка не рисуется (`cellConfirmState → null`) (дата 2026-10-18) | P6-6 | нет |
| T8.D-2 | Ожидающая кнопка даты — disabled с текстом «Дата подтверждена (до сохранения)»; без ожидания «Подтвердить дату»; отменить подтверждение даты можно только отменой всей правки. Для колонки ожидание = черновая дата равна `period_date`. Затем текст подсказки при нажатой кнопке подтверждения ячейки: «Подтверждение будет сохранено — нажмите, чтобы убрать» (дата 2026-10-18) | пользователь должен видеть, что подтверждение в черновике | текст не из плана; ширина колонки (T8.2) проверяется в T11 |
| T8.D-3 | При значке записи у Input `paddingLeft: 14`, чтобы значок не перекрывал галочку (дата 2026-10-18) | без браузера не проверено | визуальное наложение; проверка в T11 |
| T8.D-4 | `cellConfirmState` возвращает `pending-unconfirm` при `confirms[key] === false` (дата 2026-10-18) | нужно третье состояние | нет |
| T8.D-5 | Подтверждение ячейки только строкам с `lineId` (дата 2026-10-18) | P6-6, P6-7 | нет |
| T9.D-1 | «Значения есть» = `amount !== null || string_value !== null` по любой ячейке; `detail` не блокирует независимо от значений (дата 2026-10-18) | P6-10 | расхождение с условием ядра |
| T9.D-2 | Ключ ссылки в 409 `LINES_UNRESOLVED` — `line_id` (дата 2026-10-18) | ключ GridRow для строк из БД | ссылка молчит, если строки нет в таблице (T9.6) |
| T9.D-3 | Описание Alert подтверждения собирается императивно (`confirmDescription`); приоритет: failures > lines > duplicate (дата 2026-10-18) | линтер запрещает вложенные тернарники | только вид |
| T10.D-1 | `toFileWarnings` отбрасывает объекты без `code` / `message` или с `severity` не `error` / `info` (дата 2026-10-18) | P6-13 | нет |
| T10.D-2 | В режиме `document` у `IssuesPanel` колонка «место» не показывается; `OsvDocument` кладёт place '' / targetKey null (дата 2026-10-18) | у документа нет места | нет |
| T10.D-3 | Подробности документа в поповере грузятся при открытии (`trigger=click`), состояние сбрасывается при закрытии (дата 2026-10-18) | P6-12 | лишний запрос при повторном открытии |
| T10.D-4 | Значок — `<button>` под плашкой статуса; без `documentId` раздел «Предупреждения документа» не показывается (дата 2026-10-18) | правила a11y | нет |
| T10.D-5 | Пустой список в разделе «Предупреждения документа» — «Нет» (дата 2026-10-18) | текст не из плана | только вид |
| UI-1 | Тексты, которых нет в плане: «Дата подтверждена (до сохранения)», «Подтверждение будет сохранено — нажмите, чтобы убрать», «Строка не найдена в форме», «Наименование организации — не длиннее 1000 символов» (дата 2026-10-18) | нужны для состояний, плана для них не было | только формулировки |
| T11.D-1 | Колонки значений 128px вместо 112 (поле даты из 10 символов и сумма с галочкой обрезались), `tableLayout='fixed'` (дата 2026-10-18) | замечание T11.1: колонка «прыгала» при появлении кнопки даты | только вид |
| T11.D-2 | Кнопка подтверждения даты — короткая «Подтвердить» / «Подтверждено», длинный текст «Подтвердить дату» / «Дата подтверждена (до сохранения)» в подсказке и `aria-label` (дата 2026-10-18) | фиксированная ширина колонки; отклонение от формулировки «Подтвердить дату» в P6-11 | только подпись |
| T11.D-3 | Ссылка «Разберите строки без кода: N» — `Button type=link` отдельной строкой над шапкой карточки, видна и в режиме правки (дата 2026-10-18) | клавиатурный доступ; строки нужно разбирать именно при правке | только вид |
| T11.D-4 | Список предупреждений документа показывает первые 3 записи (ошибки первыми) и «Показать все (N)»; то же на экране ОСВ (дата 2026-10-18) | таблица уезжала ниже сгиба | только вид |
| T11.D-5 | Значение со `string_value` без суммы показывается серым текстом, а не «-» (дата 2026-10-18) | аналитик должен видеть нераспознанный текст | только вид |
Решения контроллера и исполнителей поверх плана (`R-n`, `O-n`) дописываются сюда по ходу исполнения.

View File

@ -0,0 +1,34 @@
## Why
Сквозной change `unified-statement-normalization` (RSHB/arm-ka-docs#116) переводит ББ/ОФР на единую нормализацию:
ядро (API v5.19, контракт 2.0) отдаёт все строки источника со статусами `ok` / `unreadable` / `missing` / `detail`,
признак `needs_review` у ячеек, строк и полей шапки, предупреждения `document_issue` и структурные `job.warnings`.
Экран формы и карточка файла должны это показывать и давать разобрать. Issue — RSHB/arm-web#43. План —
`docs/superpowers/plans/2026-10-18-statements-v2.md` (план 6 сквозного change).
## What Changes
Экраны: [41] проверка формы ББ/ОФР, [39] панель файлов (карточка файла).
- Раскладка «как на бумаге»: строки `unreadable` / `missing` под ближайшей предыдущей строкой с кодом, `detail` под
переключателем «Показать расшифровки (N)», справочное имя строки основное, файловое — рядом, если отличается.
- Подсветка только по `needs_review` (ячейки, строки, поля шапки); предупреждения и нарушения у места
(`issues`, `checks`).
- Режим «Редактировать»: назначение кода, «Это расшифровка», «Сделать расшифровкой», подтверждение ячеек, кодов и дат,
правка наименования организации, один `PATCH …/statement` (`header`, `values`, `columns`, `lines`).
- «Подтвердить данные» отключена при неразобранных строках; разбор 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`,
`LINE_CODE_TAKEN`.
- Карточка файла читает `job.warnings` структурами `{code, severity, message, locator}`; предупреждения ОСВ-документа
и документа — из `DocumentOut.issues`.
- Спеки: `document-review-ui` (2 MODIFIED, 4 ADDED), `primary-files-ui` (1 ADDED).
## Non-goals
- Переключатель «как в файле» (Р-14: раскладка одна).
- Склейка форм с разных листов/файлов.
- Изменения API ядра и спеки docs: расхождения фиксируются как `O-n` в отчётах задач.
## Dependencies
Выкладка вместе с ядром (план 3 сквозного change, API v5.19): `JobBrief.warnings` меняет тип со строк на структуры.
До выкладки 2.0-данных на стенде строки со статусами подставляются перехватом запросов (P6-14).

View File

@ -0,0 +1,109 @@
## MODIFIED Requirements
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`, включая
строки и ячейки, достроенные сервером по справочнику редакции (`id = null`). Ячейки с `needs_review = true` SHALL
подсвечиваться как «требует внимания» (подсветки напрямую по `confidence < 100` или `is_recognized = false` нет), с
`is_modified = true` — как «исправлено» с исходным значением `original_amount` в подсказке; пустая достроенная ячейка не
подсвечивается. Строки «БАЛАНС» формы `BALANCE` (коды 1600 и 1700) SHALL выделяться фоном `#f9f0ff` и жирным шрифтом;
подсветка ячейки перекрывает фон строки. Единицы (`header.units`) SHALL выводиться над таблицей. Пока документ не
`CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после точки, знак минус
допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values: [...]}` суммой-строкой:
ячейка строки из БД — `{line_id, column_id, amount}`, ячейка достроенной строки (`id = null`) — `{line_code,
column_id, amount}`; ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта, единицы, наименование
организации) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется. Правка строк, колонок и
подтверждений в том же `PATCH` (`columns`, `lines`, `values[].confirmed`) описана в требовании «Правка строк, колонок и
подтверждение в режиме «Редактировать»».
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Заполнение строки, которой не было в распознавании
- **WHEN** у формы нет строки 1700 из источника, аналитик вводит сумму в достроенную строку 1700 и сохраняет
- **THEN** уходит `{line_code: "1700", column_id, amount}`, по ответу строка получила `id`, ячейка помечена «исправлено»
#### Scenario: Строки «БАЛАНС»
- **WHEN** открыта форма `BALANCE`
- **THEN** строки 1600 и 1700 выделены цветом и жирным, у ОФР выделения нет
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Подтверждение документа
Кнопка «Подтвердить данные» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`, а
также при наличии строк `unreadable` / `missing` с суммой или нераспознанным текстом в ячейках (подсказка «Разберите
строки без кода: N» с переходом к первой такой строке). При 409 `CHECKS_NOT_PASSED` экран SHALL показать
`detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать исправить дату отчёта; при 409
`LINES_UNRESOLVED` — показать тот же текст, что у отключённой кнопки; при 409 `DUPLICATE_FORM` — со ссылкой на
подтверждённую форму; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
#### Scenario: Дубль подтверждённой формы
- **WHEN** сервер отвечает 409 `DUPLICATE_FORM`
- **THEN** экран показывает ошибку со ссылкой на уже подтверждённую форму
## ADDED Requirements
### Requirement: Раскладка строк формы со статусами
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
#### Scenario: Код-мусор на своём месте
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
#### Scenario: Расшифровки скрыты
- **WHEN** у формы три строки `detail`
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
### Requirement: Подсветка и предупреждения у места
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
`needs_review` строки, поля шапки — по `needs_review_fields` (в подсказке у поля — `header.source.<поле>`: сырой текст и
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
#### Scenario: Догадка подтверждена проверкой
- **WHEN** значение `is_recognized = false`, `needs_review = false`
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed:
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
`LINE_CODE_TAKEN` — у поля кода. Кнопка «Подтвердить данные» учитывает строки `unreadable` / `missing` с суммой или
нераспознанным текстом в ячейках.
#### Scenario: Назначить код
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
#### Scenario: Подтверждение заблокировано
- **WHEN** у формы две строки `missing` с суммами
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
### Requirement: Предупреждения ОСВ-документа
Экран ОСВ-документа SHALL показывать предупреждения документа из `DocumentOut.issues[]` (коды `OSV_*`) блоком над
таблицей; все записи относятся к документу (строки ОСВ `line_ids` не адресуются); уровень `error` отличается от `info`.
#### Scenario: Предупреждение листа ОСВ
- **WHEN** у ОСВ-документа в `issues` есть запись с кодом `OSV_*`
- **THEN** над таблицей показан блок предупреждений с текстом записи и уровнем

View File

@ -0,0 +1,11 @@
## ADDED Requirements
### Requirement: Предупреждения файла и документа в карточке файла
Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}`
(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать
`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`.
`job.mapping_warnings` остаётся списком строк.
#### Scenario: Потерянный ОФР в карточке
- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]`
- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки

View File

@ -0,0 +1,81 @@
# consolidated-osv-ui Specification
## Purpose
Аналитик видит сводную ОСВ версии [30]: сводку с несопоставленными строками, пробелы по кварталам, таблицу с фильтрами в заголовках колонок и экспорт в xlsx; сопоставляет контрагента строки с ЮЛ справочника [32].
## Requirements
### Requirement: Сводка версии
Экран «Сводная ОСВ» [30] SHALL показывать шапку версии (ГК, период, номер, статус) и сводку
`GET /consolidations/versions/{id}/osv/summary`: строк всего, документов, несопоставленных (`unmapped_total` — счётчик
«Осталось N» — строки без сопоставленного ЮЛ и без ИНН из файла, arm-core #74; сопоставление — «Указать» в строке),
автоматически сопоставленных.
#### Scenario: Контрольные цифры РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3 после подтверждения 22 ОСВ из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}`
- **THEN** «строк» = 1385, «документов» = 22, «несопоставленных» = 1260
### Requirement: Таблица свода и фильтры
Экран SHALL показывать строки `GET /consolidations/versions/{id}/osv` постранично (размер страницы ≤ 2000): ЮЛ, счёт,
контрагент, ИНН контрагента, сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с
разделителями разрядов) и последней колонкой «Тип контрагента» — бейдж ГСК (синий) / ГК (зелёный) / Внешний (серый), как
в макете [30]. Фильтры — в заголовках колонок: ЮЛ (`legal_entity_id`), корень счёта (`account_root`), поиск по
контрагенту (`q`, ≤ 500 символов), тип контрагента (`counterparty_type`, одно значение — на колонке «Тип контрагента»);
только несопоставленные (`unmapped`) — кнопкой «Показать ЮЛ без ИНН» в баннере и воронкой колонки «ИНН» (подсвечена,
пока фильтр включён, макет [32-osv-no-inn]). «Применить» и «Сбросить» SHALL закрывать окно фильтра.
Смена фильтра SHALL перечитывать первую страницу.
#### Scenario: Строка из контрольных цифр
- **WHEN** фильтр ЮЛ «Пекарня Счастливочная» и поиск «бисквит»
- **THEN** строка `60.01` показывает сальдо нач. Кт 4 454 151.93, обороты Дт 128 058 143.65, Кт 125 950 450.77,
сальдо кон. Кт 2 346 459.05, признак «ГСК»
### Requirement: Ручное сопоставление контрагента
Ячейка ИНН (моноширинный шрифт, по центру) SHALL показывать, как в макете [32-osv-no-inn]:
- ИНН из файла (`counterparty_inn`) — просто текстом, сопоставлена строка или нет (строка с ИНН сопоставления не требует,
решение владельца 08.10.2026, отмена П-1);
- строка сопоставлена, ИНН в файле нет — ИНН ЮЛ справочника (`counterparty_entity_inn`) кнопкой-«таблеткой»
(`osv-inn-mapped-button`); нажатие открывает ту же модалку для пересопоставления, у текущего ЮЛ вместо «Связать» —
неактивное «Связано»;
- нет ни ЮЛ, ни ИНН — подсвеченная ячейка со ссылкой «Указать» (`osv-inn-link-button`).
«Указать» и «таблетка» SHALL открывать модалку
«Выбор организации для связи с <имя контрагента из файла>» [32-osv-map-counterparty]: поиск, таблица ИНН / Организация /
ГК / ГСК / ГСЗ / «Связать», рекомендуемые (`recommended`) — первыми под заголовком «Рекомендуемые» с зелёной подсветкой.
Кандидаты — `GET …/counterparty-candidates?scope=&entry_id=&q=` с паузой 300 мс после ввода: по умолчанию `scope=group`
(периметр и ГК); галочка «Искать во всём справочнике» — `scope=all`, при поиске короче 3 символов запрос не отправляется
и показывается подсказка.
«Связать» SHALL отправлять `POST …/osv/entries/{entry_ids.at(-1)}/counterparty {legal_entity_id}`, после успеха —
сообщение (с кем связано, сколько записей ОСВ сопоставлено автоприменением, создан или перенесён алиас), закрывать
модалку и перечитывать страницу свода и сводку («Осталось N» на клиенте не вычисляется). Ответы 409:
- `ALIAS_TAKEN` — подтверждение «Имя уже закреплено за <владельцы>. Перенести?» → повтор с `move_alias = true`;
- `LOCK_REQUIRED` — подтверждение «Взять версию в работу?» → `POST …/lock` → повтор; отказ — ничего не меняется;
- `LOCKED_BY_OTHER` (от правки или от `POST …/lock`) — «Версию держит <имя>» (имя — из ответа `POST …/lock` или из
`lock` карточки версии), без перехвата;
- `VERSION_NOT_EDITABLE` и прочие ошибки — текстом сервера.
#### Scenario: Сопоставление «Пекарня Счастливочная ООО» в ОСВ РБ
- **WHEN** в своде РБ 2024 Q3 у строки контрагента «Пекарня Счастливочная ООО» без сопоставления нажато «Указать» и у
рекомендуемой «Пекарни Счастливочной» — «Связать» (версия не взята в работу → «Взять в работу»)
- **THEN** строка показывает ЮЛ и тип «ГСК», «Осталось N» уменьшается на число сопоставленных строк, у ЮЛ появляется алиас
#### Scenario: Алиас занят
- **WHEN** имя строки уже алиас другого ЮЛ и аналитик выбирает третье ЮЛ
- **THEN** показан вопрос о переносе с именами владельцев; «Перенести» повторяет запрос с `move_alias = true`
### Requirement: Пробелы
Экран SHALL показывать `gaps[]` сводки списком «ЮЛ — счёт — недостающие кварталы» с пояснением, что строки этих пар в
своде отсутствуют.
#### Scenario: Пробелы РБ 2024 Q3
- **WHEN** открыт свод версии РБ 2024 Q3
- **THEN** показано 7 пробелов, у каждого недостающие кварталы «Q1, Q2»
### Requirement: Экспорт
Кнопка «Скачать xlsx» SHALL получать `GET /consolidations/versions/{id}/osv/export` с текущими фильтрами через клиент API
с токеном и сохранять файл; имя — из `Content-Disposition`, а если заголовок недоступен —
`Сводная_ОСВ_<ГК>_<год>_<квартал>.xlsx`.
#### Scenario: Экспорт с фильтром
- **WHEN** аналитик скачивает свод с фильтром по ЮЛ
- **THEN** сохраняется xlsx, а ошибка сервера показывается текстом, а не скачивается

View File

@ -0,0 +1,44 @@
# consolidation-setup Specification
## Purpose
Аналитик создаёт консолидацию ГК за период из модалки [42] (периметр — члены ГСК с возможностью правки), сразу попадает в карточку новой версии и из любой версии переходит к её сводной ОСВ.
## Requirements
### Requirement: Состав модалки «Новая консолидация»
Модалка [42] SHALL предлагать выбор ГК (`GET /directories/company-groups`), ГСК этой ГК с `kind = gsk`
(`GET /directories/company-groups/{id}/related-groups`), года и квартала, и показывать **все** незакрытые ЮЛ ГК
(`GET /directories/legal-entities?company_group_id=&closed=false`, страницы дочитываются по `offset`, пока не получены
все `total`). Члены выбранной ГСК (`GET /directories/related-groups/{id}` → `members[]`) SHALL быть отмечены
автоматически; пользователь может снять или добавить отметки.
#### Scenario: Выбор ГСК отмечает её членов
- **WHEN** аналитик выбирает ГК «Русский бисквит» и её ГСК
- **THEN** в списке ЮЛ видны все ЮЛ ГК, а члены ГСК («АО "Русский бисквит"», «ООО "Пекарня Счастливочная"») отмечены
#### Scenario: ЮЛ больше одной страницы
- **WHEN** у ГК больше ЮЛ, чем `limit` одной страницы
- **THEN** модалка показывает все ЮЛ ГК, а не первую страницу
### Requirement: Создание и переход к версии
По «Создать» модалка SHALL отправить `POST /consolidations {company_group_id, related_group_id, year, quarter,
legal_entity_ids}` с отмеченными ЮЛ. При 201 модалка SHALL закрыться, список консолидаций ГК — перечитаться, а созданная
версия (`VersionCard.id`) — открыться в карточке. При ошибке модалка SHALL остаться открытой и показать `message` из
`ApiError` (коды `CONSOLIDATION_EXISTS`, `RELATED_GROUP_MISMATCH`, `ENTITY_NOT_IN_GROUP`, `PERIMETER_TOO_SMALL` и 422
валидации).
#### Scenario: Успешное создание
- **WHEN** аналитик создаёт консолидацию 2024 Q3 с периметром из ГСК
- **THEN** открыта карточка версии №1 со статусом «Инициация» и двумя ЮЛ в панели «Организации»
#### Scenario: Консолидация за период уже есть
- **WHEN** сервер отвечает 409 `CONSOLIDATION_EXISTS`
- **THEN** модалка не закрывается и показывает текст ошибки сервера
### Requirement: Переход к сводной ОСВ из карточки
Кнопка «К отчётности» карточки версии SHALL вести на `/groups/:group_id/versions/:version_id/osv` и быть доступна в
любом статусе версии.
#### Scenario: Переход к своду
- **WHEN** аналитик нажимает «К отчётности» у версии
- **THEN** открывается экран сводной ОСВ этой версии

View File

@ -0,0 +1,185 @@
# document-review-ui Specification
## Purpose
Аналитик проверяет распознанный документ и подтверждает его: форма ББ/ОФР рядом со сканом PDF (правка значений и шапки, проверки и их акцепт) [41]/[41-02], ОСВ-документ — таблицей строк в порядке файла (как в 1С) только для чтения.
## Requirements
### Requirement: Экран документа
Маршрут `/documents/:document_id` SHALL загрузить `GET /documents/{id}` и показать вид по `document_type`: `BALANCE` и
`INCOME` — проверка формы, `OSV` — просмотр ОСВ; для прочих типов — карточку документа без данных. Шапка экрана SHALL
показывать тип, ЮЛ-владельца, период и статус документа, кнопку «Назад».
#### Scenario: Открытие формы баланса
- **WHEN** аналитик открывает документ `BALANCE`
- **THEN** показан экран проверки формы
### Requirement: Скан рядом с формой
Для ББ/ОФР экран SHALL показывать исходный файл документа: PDF — постранично через pdf.js из байт
`GET /files/{file_id}/content?disposition=inline`, полученных клиентом API с токеном (не через `<iframe>` и не прямой
ссылкой); изображение (png/jpg) — тегом `<img>` из blob; xlsx — без превью, с кнопкой «Скачать исходник».
#### Scenario: PDF под CSP sandbox
- **WHEN** сервер отдаёт PDF с `Content-Security-Policy: sandbox`
- **THEN** страницы PDF отрисованы на canvas и листаются
### Requirement: Форма ББ/ОФР — просмотр и правка
Экран SHALL показывать `GET /documents/{id}/statement`: шапку (ИНН, дата отчёта, единицы — значение и исходное при
правке; наименование, ОКУД/ОКПО) и таблицу «строка (код, наименование) × колонки» с суммами из `values[]`, включая
строки и ячейки, достроенные сервером по справочнику редакции (`id = null`). Ячейки с `needs_review = true` SHALL
подсвечиваться как «требует внимания» (подсветки напрямую по `confidence < 100` или `is_recognized = false` нет), с
`is_modified = true` — как «исправлено» с исходным значением `original_amount` в подсказке; пустая достроенная ячейка не
подсвечивается. Строки «БАЛАНС» формы `BALANCE` (коды 1600 и 1700) SHALL выделяться фоном `#f9f0ff` и жирным шрифтом;
подсветка ячейки перекрывает фон строки. Единицы (`header.units`) SHALL выводиться над таблицей. Пока документ не
`CONFIRMED`, ячейка SHALL быть редактируемой: ввод проверяется по формату суммы (до 2 знаков после точки, знак минус
допустим, пустое — `null`), сохранение отправляет `PATCH /documents/{id}/statement {values: [...]}` суммой-строкой:
ячейка строки из БД — `{line_id, column_id, amount}`, ячейка достроенной строки (`id = null`) — `{line_code,
column_id, amount}`; ответ заменяет состояние экрана. Правка шапки (ИНН, дата отчёта, единицы, наименование
организации) SHALL отправляться тем же `PATCH` в `header`; `inn: null` не отправляется. Правка строк, колонок и
подтверждений в том же `PATCH` (`columns`, `lines`, `values[].confirmed`) описана в требовании «Правка строк, колонок и
подтверждение в режиме «Редактировать»».
#### Scenario: Исправление ячейки
- **WHEN** аналитик меняет значение ячейки и сохраняет
- **THEN** ячейка помечена «исправлено», проверки пересчитаны по ответу сервера, акцепт снят
#### Scenario: Заполнение строки, которой не было в распознавании
- **WHEN** у формы нет строки 1700 из источника, аналитик вводит сумму в достроенную строку 1700 и сохраняет
- **THEN** уходит `{line_code: "1700", column_id, amount}`, по ответу строка получила `id`, ячейка помечена «исправлено»
#### Scenario: Строки «БАЛАНС»
- **WHEN** открыта форма `BALANCE`
- **THEN** строки 1600 и 1700 выделены цветом и жирным, у ОФР выделения нет
#### Scenario: Неверный формат суммы
- **WHEN** аналитик вводит «12,3,4»
- **THEN** запрос не отправляется, поле показывает ошибку формата
### Requirement: Проверки и их акцепт
Экран SHALL показывать `checks`: жёсткие (`failed`), мягкие (`soft_failed`), межформенную (`cross_form`) и акцепт
(`accepted`: кто, когда, комментарий). При непустом `failed` и `accepted = null` SHALL быть доступно «Принять с
комментарием»: комментарий 1–2000 символов → `POST /documents/{id}/statement/checks/accept {comment}`.
#### Scenario: Акцепт нарушений
- **WHEN** есть жёсткие нарушения и аналитик принимает их с комментарием
- **THEN** блок проверок показывает акцепт, нарушения остаются видимыми
### Requirement: Подтверждение документа
Кнопка «Подтвердить данные» SHALL отправлять `POST /documents/{id}/confirm` и быть недоступна для `CONFIRMED` и `ERROR`, а
также при наличии строк `unreadable` / `missing` с суммой или нераспознанным текстом в ячейках (подсказка «Разберите
строки без кода: N» с переходом к первой такой строке). При 409 `CHECKS_NOT_PASSED` экран SHALL показать
`detail.failures[]` и предложить акцепт; при 409 `PERIOD_REQUIRED` — подсказать исправить дату отчёта; при 409
`LINES_UNRESOLVED` — показать тот же текст, что у отключённой кнопки; при 409 `DUPLICATE_FORM` — со ссылкой на
подтверждённую форму; прочие ошибки — `message`. После успеха экран SHALL перейти в режим только чтения.
#### Scenario: Подтверждение с неакцептованными нарушениями
- **WHEN** аналитик подтверждает форму с жёсткими нарушениями без акцепта
- **THEN** показан список нарушений из ответа и кнопка «Принять с комментарием»
#### Scenario: Дубль подтверждённой формы
- **WHEN** сервер отвечает 409 `DUPLICATE_FORM`
- **THEN** экран показывает ошибку со ссылкой на уже подтверждённую форму
### Requirement: ОСВ-документ
Для `OSV` экран SHALL показывать строки `GET /documents/{id}/entries` (все страницы, `limit` ≤ 2000) таблицей в порядке
файла, как в 1С (AURORA-1702): строки счёта, субсчетов и подразделений, у которых есть вложенные строки, — строки-группы
(жирным, со своими сальдо и оборотами), вложенные строки — с отступом по уровню в колонке аналитики. Договоры отдельными
строками не идут — их сумма в строке контрагента; субсчёт или подразделение без вложенных строк и без сумм не выводится.
Строки-показатели колонки «Показатели» и строки «Кол.», «Вал.» (§1.3 правил предметной области: `group_type = OTHER`,
`data_type` не BU/SUM и не равен виду родителя — NU, QUANTITY, CURRENCY или пусто у «ПР»/«ВР») в рублёвую таблицу
SHALL NOT попадать и родителя группой не делают: строка БУ остаётся строкой с наименованием и суммами в рублях
(AURORA-1703). Колонки: субсчёт, подразделение (если в файле есть этот уровень), аналитика (у строки счёта — её текст),
ИНН (если есть контрагенты), сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт (суммы — строками с
разделителями разрядов, рубли), только чтение. Фильтр по счёту оставляет счёт и все его субсчета («62» — 62, 62.01,
62.02), при поиске по аналитике или ИНН строки-группы скрываются. Кнопка «Подтвердить» — как для форм.
Заголовок колонки аналитики SHALL зависеть от вида строк-листьев (структурные строки субсчёта и подразделения,
строки-группы и строки-показатели не учитываются, договоры строками не идут): только `COUNTERPARTY` — «Контрагент»; один другой вид — его `group_name`
(«Основные средства»); смесь видов или вид без `group_name` — «Аналитика», а перечень видов — во всплывающей подсказке
заголовка. Колонка «ИНН» и подсказка поиска «Контрагент или ИНН» SHALL показываться только при наличии контрагентов
(`COUNTERPARTY`); иначе колонки ИНН нет, а поиск подписан «Поиск по аналитике».
#### Scenario: ОСВ по основным средствам
- **WHEN** листья документа — аналитика с `group_name` «Основные средства», контрагентов нет
- **THEN** колонка называется «Основные средства», колонки ИНН нет, поиск подписан «Поиск по аналитике»
#### Scenario: Строки счёта и субсчетов
- **WHEN** аналитик открывает ОСВ по счёту 62 с субсчетами 62.01 и 62.02 и контрагентами под ними
- **THEN** в таблице жирным видны строки 62, 62.01, 62.02 с их сальдо и оборотами, контрагенты — под своим субсчётом с отступом
#### Scenario: ОСВ с колонкой «Показатели»
- **WHEN** у позиции номенклатуры счёта 41 в файле строки «БУ» и «Кол.»
- **THEN** в таблице строка позиции с наименованием и суммами в рублях, строки «Кол.» нет
#### Scenario: Подтверждение ОСВ
- **WHEN** аналитик подтверждает ОСВ-документ `NEEDS_CHECK`
- **THEN** документ становится «Загружен», версия, к которой он подходит, переходит в `DATA_CORRECTION`
### Requirement: Виртуализация таблицы ОСВ-документа
Таблица строк ОСВ-документа SHALL быть виртуализирована: в DOM находятся только видимые строки, остальные не
отрисовываются. Строки по-прежнему загружаются целиком, поиск по аналитике (и ИНН) и фильтр по счёту — клиентские;
серверной пагинации нет. Высота области прокрутки считается числом по высоте окна и пересчитывается при его изменении.
Строка «Итого по документу» закреплена внизу и выровнена по колонкам, двухъярусная шапка и колонка «Субсчёт»
закреплены, горизонтальная прокрутка сохраняется. ОСВ любого размера (десятки тысяч строк) SHALL открываться без
падения вкладки.
#### Scenario: Большая ОСВ
- **WHEN** аналитик открывает ОСВ-документ на ~29 000 строк
- **THEN** документ открывается, число строк в DOM не зависит от размера документа, прокрутка до конца, поиск и фильтр по субсчёту работают, «Итого по документу» совпадает с суммой по всему документу
### Requirement: Раскладка строк формы со статусами
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
#### Scenario: Код-мусор на своём месте
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
#### Scenario: Расшифровки скрыты
- **WHEN** у формы три строки `detail`
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
### Requirement: Подсветка и предупреждения у места
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
`needs_review` строки, поля шапки — по `needs_review_fields` (в подсказке у поля — `header.source.<поле>`: сырой текст и
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
false` напрямую нет. У значений строк `detail` `needs_review` всегда `false` (Р-23): при показанных расшифровках их
сомнительные ячейки не подсвечиваются, но подсказка с `text` и уверенностью есть.
#### Scenario: Догадка подтверждена проверкой
- **WHEN** значение `is_recognized = false`, `needs_review = false`
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
колонки без даты (`period_kind = none`) или с предупреждением о дате (в том числе подтверждение той же даты); снятие подтверждения ячейки и кода (`confirmed:
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
`LINE_CODE_TAKEN` — у поля кода. Кнопка «Подтвердить данные» учитывает строки `unreadable` / `missing` с суммой или
нераспознанным текстом в ячейках.
#### Scenario: Назначить код
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
#### Scenario: Подтверждение заблокировано
- **WHEN** у формы две строки `missing` с суммами
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
### Requirement: Предупреждения ОСВ-документа
Экран ОСВ-документа SHALL показывать предупреждения документа из `DocumentOut.issues[]` (коды `OSV_*`) блоком над
таблицей; все записи относятся к документу (строки ОСВ `line_ids` не адресуются); уровень `error` отличается от `info`.
#### Scenario: Предупреждение листа ОСВ
- **WHEN** у ОСВ-документа в `issues` есть запись с кодом `OSV_*`
- **THEN** над таблицей показан блок предупреждений с текстом записи и уровнем

View File

@ -0,0 +1,77 @@
# primary-files-ui Specification
## Purpose
Аналитик загружает первичные файлы ЮЛ в контексте версии и видит в панели «Первичные документы» [39]/[40] файлы и их документы со статусами распознавания; список обновляется сам, пока идёт распознавание.
## Requirements
### Requirement: Загрузка файлов ЮЛ в контексте версии
Панель «Файлы ЮЛ» [39]/[40], открытая из строки ЮЛ карточки версии, SHALL загружать выбранные файлы одним запросом
`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart, поле `files` повторяется),
где `consolidation_id` — консолидация открытой версии (из списка консолидаций ГК в сторе по `current_version_id`),
без других полей формы и без query `consolidation_version_id`. Тип и период документа не спрашиваются. Кнопка
«Загрузить документы» SHALL быть недоступна с подсказкой «Загрузка доступна только в версии в работе», если статус
открытой версии не `INITIATED`, `DOCUMENTS_PROCESSING` или `DATA_CORRECTION`. После ответа панель SHALL показать итог:
принятые файлы (`accepted[].name`) и отклонённые с причиной (`rejected[].name` + `message`). Ответ 422 с телом
`UploadResult` SHALL трактоваться как «ни один файл не принят» с тем же показом `rejected[]`; ответ 422 с `detail`
(`ENTITY_NOT_IN_PERIMETER`), 409 (`CONSOLIDATION_NOT_EDITABLE`) и прочие ошибки — показом `message`.
#### Scenario: Часть файлов отклонена
- **WHEN** аналитик загружает ОСВ-файл и файл, уже загруженный ранее у этого ЮЛ
- **THEN** первый показан принятым, второй — отклонённым с текстом причины `DUPLICATE_FILE` от сервера
#### Scenario: Все файлы отклонены
- **WHEN** сервер отвечает 422 с телом `UploadResult`
- **THEN** панель показывает каждый отклонённый файл с причиной, а не общую ошибку
#### Scenario: Загрузка идёт в консолидацию
- **WHEN** панель открыта из версии консолидации 2024 Q3 и аналитик загружает файл
- **THEN** запрос уходит на `/consolidations/{id консолидации 2024 Q3}/legal-entities/{id ЮЛ}/files` без query; после
приёма файл появляется в столе панели
#### Scenario: Версия не в работе
- **WHEN** панель открыта из версии в статусе `CANCELLED`
- **THEN** кнопка «Загрузить документы» недоступна, при наведении — подсказка
### Requirement: Список файлов и документов ЮЛ
Панель SHALL по умолчанию показывать рабочий стол консолидации открытой версии —
`GET /consolidations/{consolidation_id}/legal-entities/{id}/files` (новые первыми): имя, статус файла по-русски
(«Обработка» для `UPLOADED`/`PROCESSING`, «Ошибка» с причиной из `job` для `ERROR`) и вложенные `documents[]` — тип
(«ОСВ <счёт>», «Бух. баланс», «ОФР», «Расшифровка», «Прочее»), период («2024 К1», «2024 · 9 мес.», «период не
определён»), статус документа («Требуется проверка», «Загружен» для `CONFIRMED`, «Ошибка»). Документы `OSV`, `BALANCE`,
`INCOME` SHALL быть ссылками на `/documents/:document_id`. Переключатель «Все документы организации» SHALL переключать
панель на `GET /legal-entities/{id}/files`; в этом режиме у файла не из текущей консолидации в подписи SHALL стоять его
консолидация: «<год> К<n>» по списку консолидаций ГК, «вне консолидации» при `consolidation_id = null`, «другая
консолидация», если id нет в списке. Если консолидация открытой версии не определена (версии нет в списке ГК), панель
SHALL работать в режиме «все» без переключателя.
#### Scenario: PDF с двумя формами
- **WHEN** файл `aep_6.pdf` распознан
- **THEN** под файлом видны два документа — «Бух. баланс» и «ОФР» со статусом «Требуется проверка», каждый открывает
экран проверки
#### Scenario: Стол и все документы
- **WHEN** у ЮЛ есть файлы, загруженные в консолидации 2024 Q3 и 2024 Q4, и файл, загруженный без контекста; панель
открыта из версии 2024 Q3
- **THEN** по умолчанию виден только файл 2024 Q3; после переключателя «Все документы организации» видны все три, у
файла 2024 Q4 подпись «2024 К4», у файла без контекста — «вне консолидации», у файла 2024 Q3 подписи нет
### Requirement: Опрос до окончания распознавания
Пока панель открыта и среди файлов активного списка (стол или все документы) есть `UPLOADED` или `PROCESSING`, панель
SHALL перечитывать этот список раз в 5 секунд и SHALL прекратить опрос, когда таких файлов нет или панель закрыта.
Переключение режима или смена ЮЛ SHALL сбрасывать список и перечитывать его. При закрытии панели карточка версии SHALL
перечитываться (обновить ячейки чек-листа).
#### Scenario: Опрос останавливается
- **WHEN** все файлы активного списка перешли в `PROCESSED` или `ERROR`
- **THEN** запросы списка файлов прекращаются
### Requirement: Предупреждения файла и документа в карточке файла
Карточка файла (панель файлов) SHALL читать `job.warnings` как список структур `{code, severity, message, locator}`
(после выкладки ядра v5.19 это не строки; у старых заданий ядро отдаёт строки, обёрнутые в `LEGACY_WARNING`), показывать
`message` и `locator`, отличая `error` от `info`; предупреждения листа ОСВ и документа — из `DocumentOut.issues[]`.
`job.mapping_warnings` остаётся списком строк.
#### Scenario: Потерянный ОФР в карточке
- **WHEN** у задания `job.warnings = [{code: "JSON_FAILED", severity: "error", message: "…", locator: "file:x_ofr.json"}]`
- **THEN** карточка показывает предупреждение уровня «ошибка» с текстом и местом; экран не падает на объекте вместо строки

View File

@ -18,8 +18,8 @@ SPA кредитного аналитика АРМ КА: справочники
`../../docs/mockup/INDEX.md`).
3. Права — данные: доступ решает сервер (403). Меню пока не скрывается по правам (`Sidebar` не применяет permission);
коды прав из `/me` лежат в `src/auth/permissions.ts`.
- Требования к экранам: действующие — `../../arm-web/openspec/specs/*-ui/spec.md` (пока не перенесены), новые и
изменённые — `../openspec/` arm-core.
- Требования к экранам: действующие — `../openspec/specs/*-ui/spec.md` (перенесены из arm-web@de4e477), новые и
изменённые — `../openspec/changes/` этого репозитория.
- `../../reference-old-arm/` — только чтение: инфраструктурные идеи (PDFResultViewer) можно смотреть, эндпоинты старого
АРМ (`/fsl/...`, `/osv/by-file-id`, `POST /files`) в arm-core не существуют.