Merge pull request 'OpenSpec: архив counter-pairs (#195, AURORA-1746)' (#199) from chore/195-archive-counter-pairs into main

This commit is contained in:
aep 2026-10-27 22:11:02 +03:00
commit 8d69c9f9a0
9 changed files with 166 additions and 25 deletions

View File

@ -104,3 +104,76 @@ xlsx не меняются. Пересчёт SHALL выполняться над
#### Scenario: Малая сумма в крупных единицах #### Scenario: Малая сумма в крупных единицах
- **WHEN** выбраны «млн руб.» и 2 знака, сумма — `-0.40` - **WHEN** выбраны «млн руб.» и 2 знака, сумма — `-0.40`
- **THEN** в ячейке «0,00» - **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** показан режим «Встречные записи» с первой парой; кнопка «Назад» браузера не проходит по переключениям режимов

View File

@ -74,13 +74,15 @@ MUST NOT агрегироваться между контрагентами. `co
### Requirement: Страница свода и сводка ### Requirement: Страница свода и сводка
`GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`, `GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`,
`row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`, `row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`,
`counterparty_type`, `unmapped=true` (строка требует сопоставления: `counterparty_id` пуст после дорезолва **и** `counterparty_type`, `counterparty_id` (строка с этим `counterparty_id` после дорезолва; AURORA-1746), `unmapped=true`
`counterparty_inn` пуст или из пробелов; строка с ИНН из файла, которого нет в справочнике, сопоставления не требует — (строка требует сопоставления: `counterparty_id` пуст после дорезолва **и** `counterparty_inn` пуст или из пробелов;
решение владельца 08.10.2026, отмена П-1; `unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q` строка с ИНН из файла, которого нет в справочнике, сопоставления не требует — решение владельца 08.10.2026, отмена П-1;
(подстрока без регистра в `counterparty_name` или префикс `counterparty_inn`). `GET …/osv/summary` SHALL отдавать `unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q` (подстрока без регистра в `counterparty_name`
`rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} независимо от или префикс `counterparty_inn`). Те же фильтры SHALL принимать `GET …/osv/export`. `GET …/osv/summary` SHALL отдавать
`match_stored`), `documents_total` (привязанные ОСВ-документы) и `gaps[]` (`legal_entity_id`, `legal_entity_name`, `rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`}
`account_code`, `missing_quarters`). Версия в любом статусе, включая `CANCELLED`, SHALL читаться. независимо от `match_stored`), `documents_total` (привязанные ОСВ-документы), `gaps[]` (`legal_entity_id`,
`legal_entity_name`, `account_code`, `missing_quarters`) и `counter_pairs[]` — пары участников без встречных записей
(требование «Пары участников без встречных записей»). Версия в любом статусе, включая `CANCELLED`, SHALL читаться.
#### Scenario: Только несопоставленные #### Scenario: Только несопоставленные
- **WHEN** запрошено `GET …/osv?unmapped=true` - **WHEN** запрошено `GET …/osv?unmapped=true`
@ -98,6 +100,15 @@ MUST NOT агрегироваться между контрагентами. `co
- **WHEN** запрошено `GET …/osv?match_kind=NAME&match_kind=ALIAS` - **WHEN** запрошено `GET …/osv?match_kind=NAME&match_kind=ALIAS`
- **THEN** в ответе только строки с этими `match_kind`, `total` = `summary.auto_matched_total` - **THEN** в ответе только строки с этими `match_kind`, `total` = `summary.auto_matched_total`
#### Scenario: Строки одной стороны пары
- **WHEN** в `summary.counter_pairs` есть пара `holder_id = А`, `missing_id = Б` и запрошено
`GET …/osv?legal_entity_id=А&counterparty_id=Б`
- **THEN** `total` = `holder_rows` пары, у всех строк ответа ЮЛ А и `counterparty_id = Б`
#### Scenario: Кандидаты другой стороны пары
- **WHEN** для той же пары запрошено `GET …/osv?legal_entity_id=Б&unmapped=true`
- **THEN** `total` = `missing_unmapped_rows` пары
### Requirement: Кандидаты для сопоставления ### Requirement: Кандидаты для сопоставления
`GET …/counterparty-candidates` (`CONSOLIDATION_VIEW`, policy) с параметрами `q`, `scope` ∈ {`group` (по умолчанию), `GET …/counterparty-candidates` (`CONSOLIDATION_VIEW`, policy) с параметрами `q`, `scope` ∈ {`group` (по умолчанию),
`all`}, `entry_id` SHALL возвращать до 100 ЮЛ: `scope=group` — ЮЛ периметра версии и остальные ЮЛ той же ГК, `q` `all`}, `entry_id` SHALL возвращать до 100 ЮЛ: `scope=group` — ЮЛ периметра версии и остальные ЮЛ той же ГК, `q`
@ -280,3 +291,37 @@ SHALL считать исходные строки ОСВ пары с `account_i
#### Scenario: Правило не найдено #### Scenario: Правило не найдено
- **WHEN** `rule_id` не существует - **WHEN** `rule_id` не существует
- **THEN** 404, файл не отдаётся, событие аудита не пишется - **THEN** 404, файл не отдаётся, событие аудита не пишется
### Requirement: Пары участников без встречных записей
Ядро SHALL вычислять по своду версии пары участников без встречных записей (§3 п. 9 `05-domain-rules.md`, AURORA-1746):
для участников периметра А ≠ Б (`consolidation_member` версии) пара (А, Б) входит в список, если в своде есть хотя бы одна
строка ЮЛ А с `counterparty_id = Б` (сохранённым или дорезолвленным на чтение) и нет ни одной строки ЮЛ Б с
`counterparty_id = А`. А — сторона, у которой записи есть (`holder`), Б — сторона без встречных записей (`missing`).
Пара, где записей нет ни у А, ни у Б, в список MUST NOT входить. Строки ЮЛ с контрагентом — самим собой не
учитываются. Счета, суммы и тип строки (`data_type`) на признак не влияют: учитываются все строки свода. Расчёт —
чистая функция в `domain/` от строк свода и периметра; результат MUST NOT сохраняться. Порядок списка — по имени Б,
затем по имени А.
У каждой пары SHALL быть: `holder_id`, `holder_name`, `missing_id`, `missing_name`, `holder_rows` (число строк А с
контрагентом Б), `missing_unmapped_rows` (число строк Б с `unmapped = true`), `missing_rows_total` (число всех строк Б в
своде; 0 — у Б нет строк, например не привязано ни одной ОСВ или все её пары счетов — пробелы).
#### Scenario: Русский бисквит до сопоставления
- **WHEN** версия ГК «Русский бисквит» 2024 Q3 с периметром {АО «Русский бисквит», ООО «Пекарня Счастливочная»}, 22 ОСВ
из `fixtures/osv/{rb,schastl}/2024-Q{1,2,3}` подтверждены; у Счастливочной контрагент «Русский бисквит» с ИНН
3528052718 сопоставлен по ИНН; у РБ контрагент «Пекарня Счастливочная ООО» без ИНН не сопоставлен
- **THEN** список содержит одну пару: `holder` — ООО «Пекарня Счастливочная», `missing` — АО «Русский бисквит»,
`holder_rows` ≥ 1, `missing_unmapped_rows` ≥ 1
#### Scenario: Пара закрыта ручным сопоставлением
- **WHEN** в той же версии строка РБ «Пекарня Счастливочная ООО» связана с ООО «Пекарня Счастливочная»
(`POST …/osv/entries/{entry_id}/counterparty`)
- **THEN** список пар пуст
#### Scenario: Компании не связаны
- **WHEN** в периметре три участника; у А и Б нет записей друг с другом ни в одну сторону
- **THEN** пары (А, Б) и (Б, А) в списке нет
#### Scenario: У стороны нет строк
- **WHEN** у А есть строки с контрагентом Б, а у Б в своде нет ни одной строки (ОСВ не привязаны)
- **THEN** пара (А, Б) в списке, `missing_rows_total = 0`, `missing_unmapped_rows = 0`

View File

@ -1,7 +1,7 @@
# consolidations-ui Specification # consolidations-ui Specification
## Purpose ## Purpose
Аналитик работает со списком консолидаций ГК [34] (флажок «Только в работе», «Версия», меню «⋯»), берёт версию в работу или перехватывает её [35], создаёт консолидацию [42] и меняет статус версии из шапки страницы отчёта [30], [33-01]; при уходе со «Сводной ОСВ» с несопоставленными строками — предупреждение [31-00]. Аналитик работает со списком консолидаций ГК [34] (флажок «Только в работе», «Версия», меню «⋯»), берёт версию в работу или перехватывает её [35], создаёт консолидацию [42] и меняет статус версии из шапки страницы отчёта [30], [33-01]; при уходе со «Сводной ОСВ», если есть пары участников без встречных записей, — предупреждение «Проверьте встречные записи участников» (AURORA-1746, вместо [31-00]).
## Requirements ## Requirements
@ -226,23 +226,6 @@ SHALL быть неактивной с подсказкой «Возьмите
- **WHEN** версия переведена в «Отменена» из модалки «Изменить статус» - **WHEN** версия переведена в «Отменена» из модалки «Изменить статус»
- **THEN** пояснение под заголовком сменилось на пояснение статуса `CANCELLED` без перезагрузки страницы - **THEN** пояснение под заголовком сменилось на пояснение статуса `CANCELLED` без перезагрузки страницы
### Requirement: Предупреждение «Остались неуказанные ИНН»
При уходе со вкладки «ОСВ» на другую вкладку отчёта версии, если в сводке `unmapped_total > 0`, фронт SHALL показывать
модалку [31-00] «Остались неуказанные ИНН»: «В ОСВ остались неуказанные ИНН», «Не заполнено ИНН: N. Точно хотите перейти
в другой раздел?», кнопки «Остаться» / «Перейти». «Остаться» SHALL закрывать модалку без перехода, «Перейти» — выполнять
переход. При `unmapped_total = 0` переход SHALL выполняться без модалки. Пока шагов после ОСВ нет, их вкладки
неактивны («Будет в следующих срезах») и модалка не вызывается; вкладка становится активной, когда для неё задан переход
(первой — «Шахматка»).
#### Scenario: Есть несопоставленные строки
- **WHEN** в сводке `unmapped_total = 2` и аналитик выбирает другую вкладку отчёта
- **THEN** показана модалка «Не заполнено ИНН: 2. Точно хотите перейти в другой раздел?»; «Остаться» оставляет вкладку
«ОСВ», «Перейти» открывает выбранную вкладку
#### Scenario: Все строки сопоставлены
- **WHEN** `unmapped_total = 0` и аналитик выбирает другую вкладку отчёта
- **THEN** вкладка открывается без модалки
### Requirement: Предупреждение о несохранённых правках в модалках консолидаций ### Requirement: Предупреждение о несохранённых правках в модалках консолидаций
Пока открыта модалка с начатым вводом, браузер SHALL спрашивать подтверждение при закрытии или перезагрузке вкладки Пока открыта модалка с начатым вводом, браузер SHALL спрашивать подтверждение при закрытии или перезагрузке вкладки
(стандартный диалог `beforeunload`, текст задаёт браузер): «Взять консолидацию в работу?» — введена причина перехвата (стандартный диалог `beforeunload`, текст задаёт браузер): «Взять консолидацию в работу?» — введена причина перехвата
@ -270,3 +253,43 @@ SHALL быть неактивной с подсказкой «Возьмите
#### Scenario: Название роли недоступно #### Scenario: Название роли недоступно
- **WHEN** в `role_details` нет названия для роли пользователя - **WHEN** в `role_details` нет названия для роли пользователя
- **THEN** в блоке показан код этой роли - **THEN** в блоке показан код этой роли
### Requirement: Предупреждение «Нет встречных записей»
При уходе со вкладки «ОСВ» на другую вкладку отчёта версии фронт SHALL показывать модалку «Проверьте встречные записи
участников», если в сводке есть пары `counter_pairs` и этот список пар ещё не показывался в модалке для этой версии в
текущей сессии браузера. Текст: «Нет встречных записей: N пар у M участников.» (N — число пар, M — число разных `missing_id`;
слова согласуются с числом: «1 пара у 1 участника», «2 пары у 2 участников», «5 пар у 3 участников»)
и «Возможно, контрагент среди строк без ИНН, его ИНН не найден в справочнике или это пропуск в учёте. Ещё K строк без
ИНН учтены как внешние.» (K —
`unmapped_total`; при K = 0 вторая фраза без последнего предложения). Кнопки:
- «Показать пары» — закрыть модалку без перехода и открыть на вкладке «ОСВ» режим «Встречные записи» с первой парой;
- «Остаться» — закрыть модалку без перехода;
- «Перейти» — выполнить переход.
Показ запоминается в `sessionStorage` браузера по версии вместе со списком пар (упорядоченные пары `holder_id → missing_id`);
повторный уход с «ОСВ» в той же сессии модалку не показывает, пока список пар не изменился (например, сопоставление
закрыло одну из пар — модалка покажет оставшиеся). Недоступное хранилище — модалка показывается при каждом уходе, ошибкой это
не считается. Без пар (`counter_pairs` пуст) переход SHALL выполняться без модалки при любом `unmapped_total`. В БД ничего
не пишется (решение владельца 27.10.2026). Для версии в завершённом статусе (`VERIFIED`, `CANCELLED`, `ARCHIVED`,
`REJECTED`) модалка не показывается. Пока шагов после ОСВ нет, их вкладки неактивны и модалка не вызывается.
#### Scenario: Пара без встречных записей
- **WHEN** в сводке одна пара (Русский бисквит ← Пекарня Счастливочная), `unmapped_total = 1260`, и аналитик выбирает
вкладку «Шахматка»
- **THEN** показана модалка «Нет встречных записей: 1 пара у 1 участника…»; «Перейти» открывает шахматку
#### Scenario: Повтор в той же сессии
- **WHEN** аналитик вернулся на «ОСВ» и снова выбирает «Шахматку», список пар не изменился
- **THEN** шахматка открывается без модалки
#### Scenario: Список пар изменился
- **WHEN** после показа модалки аналитик сопоставил контрагента, закрыв одну из двух пар, и выбирает «Шахматку»
- **THEN** модалка показана снова с «Нет встречных записей: 1 пара у 1 участника…»
#### Scenario: Только строки без ИНН
- **WHEN** `counter_pairs` пуст, `unmapped_total = 214`, и аналитик выбирает «Шахматку»
- **THEN** шахматка открывается без модалки
#### Scenario: Показать пары
- **WHEN** в модалке нажато «Показать пары»
- **THEN** вкладка «ОСВ» остаётся, открыт режим «Встречные записи» с первой парой