diff --git a/openspec/changes/counter-pairs/.openspec.yaml b/openspec/changes/archive/2026-10-27-counter-pairs/.openspec.yaml similarity index 100% rename from openspec/changes/counter-pairs/.openspec.yaml rename to openspec/changes/archive/2026-10-27-counter-pairs/.openspec.yaml diff --git a/openspec/changes/counter-pairs/design.md b/openspec/changes/archive/2026-10-27-counter-pairs/design.md similarity index 100% rename from openspec/changes/counter-pairs/design.md rename to openspec/changes/archive/2026-10-27-counter-pairs/design.md diff --git a/openspec/changes/counter-pairs/proposal.md b/openspec/changes/archive/2026-10-27-counter-pairs/proposal.md similarity index 100% rename from openspec/changes/counter-pairs/proposal.md rename to openspec/changes/archive/2026-10-27-counter-pairs/proposal.md diff --git a/openspec/changes/counter-pairs/specs/consolidated-osv-ui/spec.md b/openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidated-osv-ui/spec.md similarity index 100% rename from openspec/changes/counter-pairs/specs/consolidated-osv-ui/spec.md rename to openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidated-osv-ui/spec.md diff --git a/openspec/changes/counter-pairs/specs/consolidated-osv/spec.md b/openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidated-osv/spec.md similarity index 100% rename from openspec/changes/counter-pairs/specs/consolidated-osv/spec.md rename to openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidated-osv/spec.md diff --git a/openspec/changes/counter-pairs/specs/consolidations-ui/spec.md b/openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidations-ui/spec.md similarity index 100% rename from openspec/changes/counter-pairs/specs/consolidations-ui/spec.md rename to openspec/changes/archive/2026-10-27-counter-pairs/specs/consolidations-ui/spec.md diff --git a/openspec/specs/consolidated-osv-ui/spec.md b/openspec/specs/consolidated-osv-ui/spec.md index 38ba723..9d5ec00 100644 --- a/openspec/specs/consolidated-osv-ui/spec.md +++ b/openspec/specs/consolidated-osv-ui/spec.md @@ -104,3 +104,76 @@ xlsx не меняются. Пересчёт SHALL выполняться над #### 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=&missing=`); +- **первая таблица** — «У <А> есть записи с <Б>»: строки `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** показан режим «Встречные записи» с первой парой; кнопка «Назад» браузера не проходит по переключениям режимов diff --git a/openspec/specs/consolidated-osv/spec.md b/openspec/specs/consolidated-osv/spec.md index 2033bd5..61a9661 100644 --- a/openspec/specs/consolidated-osv/spec.md +++ b/openspec/specs/consolidated-osv/spec.md @@ -74,13 +74,15 @@ MUST NOT агрегироваться между контрагентами. `co ### Requirement: Страница свода и сводка `GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`, `row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`, -`counterparty_type`, `unmapped=true` (строка требует сопоставления: `counterparty_id` пуст после дорезолва **и** -`counterparty_inn` пуст или из пробелов; строка с ИНН из файла, которого нет в справочнике, сопоставления не требует — -решение владельца 08.10.2026, отмена П-1; `unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q` -(подстрока без регистра в `counterparty_name` или префикс `counterparty_inn`). `GET …/osv/summary` SHALL отдавать -`rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} независимо от -`match_stored`), `documents_total` (привязанные ОСВ-документы) и `gaps[]` (`legal_entity_id`, `legal_entity_name`, -`account_code`, `missing_quarters`). Версия в любом статусе, включая `CANCELLED`, SHALL читаться. +`counterparty_type`, `counterparty_id` (строка с этим `counterparty_id` после дорезолва; AURORA-1746), `unmapped=true` +(строка требует сопоставления: `counterparty_id` пуст после дорезолва **и** `counterparty_inn` пуст или из пробелов; +строка с ИНН из файла, которого нет в справочнике, сопоставления не требует — решение владельца 08.10.2026, отмена П-1; +`unmapped=false` — все остальные строки), `match_kind` (повторяемый), `q` (подстрока без регистра в `counterparty_name` +или префикс `counterparty_inn`). Те же фильтры SHALL принимать `GET …/osv/export`. `GET …/osv/summary` SHALL отдавать +`rows_total`, `unmapped_total` (строки `unmapped=true`), `auto_matched_total` (строки с `match_kind` ∈ {`NAME`, `ALIAS`} +независимо от `match_stored`), `documents_total` (привязанные ОСВ-документы), `gaps[]` (`legal_entity_id`, +`legal_entity_name`, `account_code`, `missing_quarters`) и `counter_pairs[]` — пары участников без встречных записей +(требование «Пары участников без встречных записей»). Версия в любом статусе, включая `CANCELLED`, SHALL читаться. #### Scenario: Только несопоставленные - **WHEN** запрошено `GET …/osv?unmapped=true` @@ -98,6 +100,15 @@ MUST NOT агрегироваться между контрагентами. `co - **WHEN** запрошено `GET …/osv?match_kind=NAME&match_kind=ALIAS` - **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: Кандидаты для сопоставления `GET …/counterparty-candidates` (`CONSOLIDATION_VIEW`, policy) с параметрами `q`, `scope` ∈ {`group` (по умолчанию), `all`}, `entry_id` SHALL возвращать до 100 ЮЛ: `scope=group` — ЮЛ периметра версии и остальные ЮЛ той же ГК, `q` @@ -280,3 +291,37 @@ SHALL считать исходные строки ОСВ пары с `account_i #### Scenario: Правило не найдено - **WHEN** `rule_id` не существует - **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` diff --git a/openspec/specs/consolidations-ui/spec.md b/openspec/specs/consolidations-ui/spec.md index dae7501..8b7f26b 100644 --- a/openspec/specs/consolidations-ui/spec.md +++ b/openspec/specs/consolidations-ui/spec.md @@ -1,7 +1,7 @@ # consolidations-ui Specification ## Purpose -Аналитик работает со списком консолидаций ГК [34] (флажок «Только в работе», «Версия», меню «⋯»), берёт версию в работу или перехватывает её [35], создаёт консолидацию [42] и меняет статус версии из шапки страницы отчёта [30], [33-01]; при уходе со «Сводной ОСВ» с несопоставленными строками — предупреждение [31-00]. +Аналитик работает со списком консолидаций ГК [34] (флажок «Только в работе», «Версия», меню «⋯»), берёт версию в работу или перехватывает её [35], создаёт консолидацию [42] и меняет статус версии из шапки страницы отчёта [30], [33-01]; при уходе со «Сводной ОСВ», если есть пары участников без встречных записей, — предупреждение «Проверьте встречные записи участников» (AURORA-1746, вместо [31-00]). ## Requirements @@ -226,23 +226,6 @@ SHALL быть неактивной с подсказкой «Возьмите - **WHEN** версия переведена в «Отменена» из модалки «Изменить статус» - **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: Предупреждение о несохранённых правках в модалках консолидаций Пока открыта модалка с начатым вводом, браузер SHALL спрашивать подтверждение при закрытии или перезагрузке вкладки (стандартный диалог `beforeunload`, текст задаёт браузер): «Взять консолидацию в работу?» — введена причина перехвата @@ -270,3 +253,43 @@ SHALL быть неактивной с подсказкой «Возьмите #### Scenario: Название роли недоступно - **WHEN** в `role_details` нет названия для роли пользователя - **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** вкладка «ОСВ» остаётся, открыт режим «Встречные записи» с первой парой