Andrey Epifancev 1cce1575b8 openspec: архив counter-pairs, Purpose consolidations-ui (#195, AURORA-1746, M-7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 23:10:42 +04:00

180 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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, а ошибка сервера показывается текстом, а не скачивается
### Requirement: Единицы и знаки после запятой
Над таблицей сводной ОСВ [30] SHALL быть два выпадающих списка: «Единицы» — «руб.», «тыс. руб.», «млн руб.», «млрд руб.»
(по умолчанию «руб.»: суммы API — в рублях) и «Знаков после запятой» — 0, 1, 2, 3 (по умолчанию 2). Выбор SHALL менять
только показ сумм шести денежных колонок (сальдо на начало, обороты, сальдо на конец, Дт/Кт); запрос, фильтры и экспорт в
xlsx не меняются. Пересчёт SHALL выполняться над строкой суммы из API без арифметики с плавающей точкой: запятая
сдвигается на 3/6/9 разрядов, округление до выбранного числа знаков — половина от нуля, разряды разделяются пробелом,
дробная часть — через запятую. Нулевая сумма SHALL показываться «—» при любых единицах; ненулевая сумма, округлившаяся до
нуля, — нулём с выбранным числом знаков, без знака «минус».
#### Scenario: Тысячи рублей
- **WHEN** выбраны «тыс. руб.» и 3 знака, сумма строки — `24000.00`
- **THEN** в ячейке «24,000»
#### Scenario: Округление без потери разрядов
- **WHEN** выбраны «тыс. руб.» и 2 знака, сумма — `4454151.93`
- **THEN** в ячейке «4 454,15»
#### Scenario: Ноль
- **WHEN** сумма ячейки — `0.00`, выбраны любые единицы и число знаков
- **THEN** в ячейке «—»
#### Scenario: Малая сумма в крупных единицах
- **WHEN** выбраны «млн руб.» и 2 знака, сумма — `-0.40`
- **THEN** в ячейке «0,00»
### Requirement: Баннер строк без ИНН
Пока в сводке `unmapped_total > 0`, над таблицей свода SHALL показываться баннер (стиль `osv-mapping-alert` макета [30]):
заголовок «Есть организации без ИНН и без сопоставления», текст «Осталось: N. Несопоставленные учитываются как внешние;
при необходимости укажите организацию кнопкой «Указать».» и кнопка «Показать ЮЛ без ИНН» / «Показать все строки»
(фильтр `unmapped`, как прежде). Баннер MUST NOT требовать сопоставить все строки для перехода к следующему шагу:
переход не блокируется (решение заказчика 16.10.2026, AURORA-1388). При `unmapped_total = 0` баннера нет.
#### Scenario: Есть строки без ИНН
- **WHEN** открыт свод версии РБ 2024 Q3 до сопоставления (`unmapped_total` = 1260)
- **THEN** баннер «Есть организации без ИНН и без сопоставления», «Осталось: 1260. Несопоставленные учитываются как
внешние…»; слов «Для перехода к следующему шагу сопоставьте все» на экране нет
### Requirement: Режим «Встречные записи»
Над сводом на вкладке «ОСВ» SHALL быть переключатель режимов «Свод | Встречные записи (N)», N — число пар
`summary.counter_pairs`. Режим — параметр адреса `?view=pairs` (без параметра — «Свод»); переключение режима MUST NOT
добавлять запись в историю браузера. Шапка свода (единицы, знаки после запятой) действует в обоих режимах.
В режиме «Встречные записи» экран SHALL показывать сверху список пар, под ним на всю ширину две таблицы одна под другой:
- **сверху** — список пар одной строкой «таблеток» с переносом, в порядке ответа, строка пары «Б ← А» (имена `missing_name` и `holder_name`) с числами
`holder_rows` и `missing_unmapped_rows`; выбранная пара подсвечена; при входе выбрана первая пара либо пара из адреса
(`?view=pairs&holder=<id>&missing=<id>`);
- **первая таблица** — «У <А> есть записи с <Б>»: строки `GET …/osv?legal_entity_id=А&counterparty_id=Б` — счёт, контрагент,
обороты Дт/Кт, сальдо на конец Дт/Кт;
- **вторая таблица** — «У <Б> — строки без ИНН и без сопоставления»: строки `GET …/osv?legal_entity_id=Б&unmapped=true` — счёт,
контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт и «Указать» в каждой строке; над таблицей — поиск по контрагенту
(`q`, ≤ 500 символов, как фильтр свода); постранично, как таблица свода. Все четыре колонки
сумм MUST быть видны без горизонтальной прокрутки при ширине окна 1440 px (на узком окне допустима прокрутка таблицы). Если
`missing_rows_total = 0` — вместо таблицы текст «У <Б> в своде нет строк: ОСВ не привязаны или по ним пробелы»; если
строки у Б есть, а без ИНН и без сопоставления нет (`missing_unmapped_rows = 0`) — текст «Строк без ИНН нет: возможно,
ИНН контрагента из файла не найден в справочнике или это пропуск в учёте».
Обе таблицы и список пар MUST оставаться внутри белой карточки экрана: тело режима прокручивается по вертикали внутри
карточки, а не выходит за её границу.
Пока сводки нет, вместо тела режима показывается индикатор загрузки, а подпись переключателя — «Встречные записи» без «(N)».
Если запрос сводки не удался, показывается ошибка с кнопкой «Повторить», повторяющей запрос.
«Указать» SHALL открывать модалку сопоставления из требования «Ручное сопоставление контрагента» с теми же ответами и
ошибками, но список кандидатов сразу сужен до участника пары А: одна строка под заголовком «Участник пары» с «Связать»
(кандидаты запрашиваются с `scope=group` и `q` = имя А), поле поиска и «Искать во всём справочнике» скрыты; флажок
«Показать все организации» возвращает обычный список модалки (решение владельца 27.10.2026). Если А среди кандидатов не
нашёлся — текст «Участник пары не найден среди кандидатов — включите «Показать все организации»». В обычном своде
«Указать» не меняется. После «Связать» экран SHALL перечитывать сводку и обе таблицы. Если выбранная пара исчезла из списка, выбирается
первая оставшаяся; пустой список — текст «Все участники периметра видят друг друга» (переключатель — «Встречные записи (0)»).
#### Scenario: Пара Русского бисквита
- **WHEN** открыт свод версии РБ 2024 Q3 до сопоставления и выбран режим «Встречные записи»
- **THEN** переключатель «Встречные записи (1)»; сверху пара «Русский бисквит ← Пекарня Счастливочная»; в первой таблице строки
Счастливочной с контрагентом «Русский бисквит»; во второй таблице строки РБ без ИНН, среди них «Пекарня Счастливочная ООО» с
«Указать»
#### Scenario: Сопоставление из режима закрывает пару
- **WHEN** во второй таблице у «Пекарня Счастливочная ООО» нажато «Указать» и выбрана ООО «Пекарня Счастливочная»
- **THEN** после «Связать» список пар пуст, показан текст «Все участники периметра видят друг друга», переключатель —
«Встречные записи (0)»
#### Scenario: Модалка сопоставления сужена до участника пары
- **WHEN** в паре «Русский бисквит ← Пекарня Счастливочная» во второй таблице нажато «Указать»
- **THEN** в модалке одна строка — ООО «Пекарня Счастливочная» под заголовком «Участник пары» с «Связать»; после
«Показать все организации» — поиск и обычный список кандидатов
#### Scenario: Сводка ещё не получена
- **WHEN** открыт режим «Встречные записи», запрос сводки в пути
- **THEN** показан индикатор загрузки, подпись переключателя — «Встречные записи» без числа
#### Scenario: Ошибка запроса сводки
- **WHEN** запрос сводки в режиме «Встречные записи» завершился ошибкой
- **THEN** показан Alert с текстом ошибки и кнопкой «Повторить»; нажатие повторяет запрос
#### Scenario: Режим из адреса
- **WHEN** открыт адрес `…/versions/{id}/osv?view=pairs`
- **THEN** показан режим «Встречные записи» с первой парой; кнопка «Назад» браузера не проходит по переключениям режимов