docs(#195): change counter-pairs — пары участников без встречных записей (AURORA-1746)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69cc52025a
commit
e9977fb221
2
openspec/changes/counter-pairs/.openspec.yaml
Normal file
2
openspec/changes/counter-pairs/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-27
|
||||
93
openspec/changes/counter-pairs/design.md
Normal file
93
openspec/changes/counter-pairs/design.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Дизайн: пары участников без встречных записей (AURORA-1746, arm-core #195)
|
||||
|
||||
## Контекст и зачем
|
||||
|
||||
Перед шахматкой аналитику важно одно: все ли внутригрупповые связи видны с обеих сторон. Шахматка строится по строкам
|
||||
свода с признаком «ГСК». Если у компании Б контрагент А лежит строкой без ИНН и без сопоставления, он «Внешний»,
|
||||
и пара А–Б в шахматке выглядит однобокой.
|
||||
|
||||
Сейчас это подменено счётчиком всех строк без ИНН. Баннер требует «сопоставьте все», модалка [31-00] всплывает при каждом
|
||||
уходе с «ОСВ», хотя переход не блокируется (AURORA-1388). Заказчик (Циликов, 23.10.2026) прочитал это как обязанность
|
||||
прокликать каждую строку, а счётчик на реальных данных до нуля не доходит.
|
||||
|
||||
Успех: аналитик видит только существенное — пары участников без встречных записей — и разбирает их в одном месте, видя
|
||||
обе стороны. Строки без ИНН, не влияющие на пары, ему не мешают.
|
||||
|
||||
Антицель: экран, который снова заставляет «прокликать всё», или модалка, которая всплывает при каждом клике.
|
||||
|
||||
## Решения владельца (27.10.2026, обсуждение в чате)
|
||||
|
||||
1. Проверка — пары участников периметра: у А есть записи с контрагентом Б, у Б нет записей с контрагентом А. Уровень —
|
||||
компании, не счета.
|
||||
2. У Б может быть настоящий пропуск в учёте, поэтому переход никогда не блокируется.
|
||||
3. Модалка — вариант «короткая сводка»: только цифры, разбор — в отдельном представлении («Показать пары»).
|
||||
4. Представление — отдельный режим вкладки «ОСВ» с таблицами («они любят таблицы»): список пар, строки А с Б, строки Б
|
||||
без ИНН и без сопоставления.
|
||||
5. Подсветка кандидата по совпадению суммы — отдельная задача.
|
||||
6. В БД ничего не хранить. Модалка — один раз за сессию или после исправлений в сводной ОСВ.
|
||||
|
||||
## Устройство
|
||||
|
||||
### Ядро
|
||||
|
||||
- `domain/consolidated_osv.py`: `counter_pairs(rows, members) -> list[CounterPair]` — чистая функция по строкам
|
||||
`Consolidated.rows` (после дорезолва) и периметру. Множество «кто кого видит» — пары `(row.legal_entity_id,
|
||||
row.counterparty_id)`, где оба в периметре и различны. Пара (А, Б) в списке ⇔ (А, Б) есть, (Б, А) нет. Счётчики:
|
||||
`holder_rows` — строки А с контрагентом Б; `missing_unmapped_rows` — строки Б с `needs_mapping`; `missing_rows_total` —
|
||||
все строки Б. Сортировка — имя Б, имя А (затем id для стабильности).
|
||||
- `OsvFilter.counterparty_id` + `_matches`; `OsvFilters.counterparty_id` в схеме; экспорт принимает тот же фильтр
|
||||
автоматически (общая модель фильтров).
|
||||
- `OsvSummaryOut.counter_pairs: list[CounterPairOut]`; `ConsolidatedOsvService.summary` считает их из того же `_build`,
|
||||
второго прохода по БД нет.
|
||||
- Аудит не пишется (чтение).
|
||||
|
||||
### Фронт
|
||||
|
||||
- `useConsolidatedOsv` / `ConsolidatedOsvContent`: режим из `?view=pairs` (`useSearchParams`, `replace`).
|
||||
- Новый виджет `widgets/ConsolidatedOsv/CounterPairs*.tsx`: список пар + две таблицы (antd `Table`), колонки — счёт,
|
||||
контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт; суммы — через существующий `useNumberDisplay` (единицы и знаки
|
||||
из шапки). Правая таблица — постранично, `OSV_PAGE_SIZE`, «Указать» → `CounterpartyMapModal` (существующая).
|
||||
- `ConsolidationReportPage` / `OsvTabs`: вместо `unmappedTotal` получают сводку (пары и `unmapped_total`); правило показа
|
||||
модалки — чистая функция в `utils/consolidations.ts` (`counterPairsKey(pairs)`, `shouldWarnCounterPairs(versionId,
|
||||
pairs, storage)`), с тестами vitest. Ключ `sessionStorage` — `armka:counter-pairs:<version_id>`, значение — ключ списка
|
||||
пар (`holder_id>missing_id`, отсортированные, через запятую). Чтение и запись — в `try/catch`.
|
||||
- `UnmappedInnModal` заменяется модалкой «Проверьте встречные записи участников»; склонение «пара/пары/пар»,
|
||||
«участника/участников» — существующая `pluralRu` (`web/src/utils/labels.ts`).
|
||||
- Над правой таблицей — поле поиска по контрагенту (`q` существующего фильтра `GET …/osv`, пауза ввода как в своде):
|
||||
у РБ на фикстуре около тысячи строк без ИНН, листать страницы в поисках одной строки — та же «прокликай всё».
|
||||
- Модалка не показывается для версий в завершённых статусах (`VERIFIED`, `CANCELLED`, `ARCHIVED`, `REJECTED` —
|
||||
`FINISHED_STATUSES`): править там нечего. Режим «Встречные записи» доступен в любом статусе, «Указать» ведёт себя как
|
||||
в своде (ошибки 409 — от сервера).
|
||||
- Баннер — новый текст (спека `consolidated-osv-ui`).
|
||||
|
||||
### Документы (отдельный PR в docs)
|
||||
|
||||
- `05-domain-rules.md` §3 п. 9 «Пары участников без встречных записей» — определение из спеки `consolidated-osv`.
|
||||
- `10-user-journey.md` строка 3.1 — новый текст баннера, режим «Встречные записи», модалка вместо [31-00].
|
||||
- `dev/api-contract-slice-4.md` — `counter_pairs[]` в сводке, фильтр `counterparty_id`.
|
||||
|
||||
## Что оставлено исполнителю
|
||||
|
||||
Раскладка трёх колонок и ширины, тексты пустых состояний в пределах спеки, имена компонентов, вынос общих колонок сумм
|
||||
между сводом и новым режимом.
|
||||
|
||||
## Риски
|
||||
|
||||
- Объём: `GET …/osv/summary` теперь считает пары — линейный проход по уже собранным строкам, без новых запросов к БД.
|
||||
- Дорезолв на чтение влияет на пары: появление алиаса может закрыть пару без «Связать». Это корректно — признак считается
|
||||
от того же свода, что видит аналитик.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|
|
||||
| 27.10.2026 | «После исправлений» = список пар изменился (ключ списка пар в `sessionStorage`), а не любое «Связать» | Повтор модалки нужен, когда есть что сказать нового; «Связать» вне пар модалку не меняет | Низкая: правило в одной функции фронта |
|
||||
| 27.10.2026 | Строки А и Б берутся существующим `GET …/osv` с фильтрами (`counterparty_id` — новый), в сводке — только пары и счётчики | Без нового эндпоинта; пагинация и экспорт работают как в своде | Низкая |
|
||||
| 27.10.2026 | Учитываются все строки свода, без ограничения счетами правил элиминации и без порога по суммам | Владелец согласовал уровень компаний; строка с нулями тоже говорит, что контрагент виден | Средняя: возможны «лишние» пары из-за случайных счетов — правится фильтром в функции |
|
||||
| 27.10.2026 | Колонки таблиц режима — счёт, контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт (без сальдо на начало) | Три колонки на экране; для поиска пары важнее обороты и итог | Низкая: добавить колонки |
|
||||
| 27.10.2026 | Режим — параметр адреса `?view=pairs` (с `holder`/`missing`), переключение без записи в историю | Как вкладки отчёта (AURORA-1851); ссылку на пару можно переслать | Низкая |
|
||||
| 27.10.2026 | Тексты баннера и модалки — из спек этого change | Владелец согласовал смысл, формулировки мои | Низкая |
|
||||
| 27.10.2026 | Поиск `q` над правой таблицей режима (находка builder check) | У РБ ~1000 строк без ИНН; без поиска разбор пары — листание страниц | Низкая: существующий фильтр API |
|
||||
| 27.10.2026 | Модалка не показывается для версий в `FINISHED_STATUSES` (находка builder check) | Замороженную версию не исправить — предупреждение было бы шумом | Низкая |
|
||||
|
||||
Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.
|
||||
67
openspec/changes/counter-pairs/proposal.md
Normal file
67
openspec/changes/counter-pairs/proposal.md
Normal file
@ -0,0 +1,67 @@
|
||||
## Why
|
||||
|
||||
AURORA-1746 и замечание заказчика (Циликов А.В., 23.10.2026, строка 10 чек-листа, AURORA-1865): «при загрузке ОСВ без
|
||||
ИНН необходимо сопоставить все организации без ИНН… нужно, чтобы система несопоставленные организации считала внешними и
|
||||
позволяла формировать консолидацию без прокликивания каждой».
|
||||
|
||||
Расчёт уже так работает: несопоставленный контрагент — «Внешний» (§3 п. 5), переход к шахматке не блокируется (решение
|
||||
заказчика 16.10.2026, AURORA-1388). Ощущение обязательности создают баннер над сводом «Для перехода к следующему шагу
|
||||
сопоставьте все внешние организации без ИНН» и модалка [31-00], которая всплывает при каждом уходе с «ОСВ», пока
|
||||
`unmapped_total > 0`. Счётчик до нуля на реальных данных не доходит: в него попадают зарплата, подотчётные лица и прочие
|
||||
строки без организации.
|
||||
|
||||
Для шахматки важны не все строки без ИНН, а только те, где прячется участник периметра. Признак этого — пара участников,
|
||||
где у одного есть записи с другим, а у другого встречных записей нет. Такие пары и нужно показать аналитику перед
|
||||
шахматкой, с местом для разбора (решение владельца 27.10.2026).
|
||||
|
||||
## What Changes
|
||||
|
||||
Репозиторий — arm-core (`api/`, `web/`). Срез 4 (сводная ОСВ), доработка. Правило — `05-domain-rules.md` §3 п. 9
|
||||
(отдельный PR в docs).
|
||||
|
||||
- **Ядро.** Чистая функция «пары без встречных записей» в `domain/consolidated_osv.py`: для участников периметра А ≠ Б
|
||||
пара (А, Б) — у А есть строка свода с `counterparty_id = Б`, у Б нет строки с `counterparty_id = А`. `GET …/osv/summary`
|
||||
отдаёт список `counter_pairs[]`; `GET …/osv` и экспорт получают фильтр `counterparty_id`. Ничего не хранится.
|
||||
- **Фронт, режим «Встречные записи».** Над сводом — переключатель «Свод | Встречные записи (N)». Во втором режиме: слева
|
||||
список пар, в центре — строки А с контрагентом Б, справа — строки Б без ИНН и без сопоставления с «Указать» (та же
|
||||
модалка сопоставления, что в своде).
|
||||
- **Фронт, модалка перед шахматкой.** Вместо [31-00] «Остались неуказанные ИНН» — «Нет встречных записей: N пар у M
|
||||
участников», кнопки «Показать пары» / «Остаться» / «Перейти». Показывается при уходе с «ОСВ», если пары есть, один раз
|
||||
на версию за сессию браузера и снова — если список пар изменился. Строки без ИНН сами по себе модалку не вызывают.
|
||||
- **Фронт, баннер.** Текст без «сопоставьте все»: несопоставленные учитываются как внешние; счётчик «Осталось: N» и
|
||||
кнопка «Показать ЮЛ без ИНН» остаются.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Подсветка строки-кандидата у Б по совпадению суммы с зеркальной суммой у А (правила элиминации) — отдельная задача.
|
||||
- Хранение решения аналитика «встречных записей нет, это пропуск в учёте» — в БД ничего не пишется (решение владельца
|
||||
27.10.2026); DBML не меняется.
|
||||
- Отсев строк, не являющихся организациями (зарплата, подотчётники), из счётчика `unmapped_total`.
|
||||
- Сверка по счетам и суммам внутри пары — это делает шахматка (§6).
|
||||
- Поиск участника, ошибочно сопоставленного с другим ЮЛ: справа показываются только несопоставленные строки без ИНН.
|
||||
- Строки Б с ИНН из файла, которого нет в справочнике (старый ИНН, опечатка): по решению 08.10.2026 они не требуют
|
||||
сопоставления, в правую таблицу не попадают, и такая пара из режима не закрывается. Оставлено как есть (решение
|
||||
владельца 27.10.2026), разбор — отдельная задача Jira; модалка и пустое состояние называют эту причину.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `consolidated-osv`: ADDED «Пары участников без встречных записей»; MODIFIED «Страница свода и сводка» (фильтр
|
||||
`counterparty_id`, `counter_pairs[]` в сводке).
|
||||
- `consolidated-osv-ui`: ADDED «Режим «Встречные записи»», ADDED «Баннер строк без ИНН».
|
||||
- `consolidations-ui`: REMOVED «Предупреждение «Остались неуказанные ИНН»»; ADDED «Предупреждение «Нет встречных
|
||||
записей»».
|
||||
|
||||
## Impact
|
||||
|
||||
- Таблицы DBML v5.23 — только чтение: `consolidation_member`, `osv_entry`, `legal_entity`, `consolidation_document`.
|
||||
- `api/src/domain/consolidated_osv.py`, `api/src/services/consolidated_osv.py`, `api/src/schemas/consolidations.py`,
|
||||
`api/src/api/endpoints/consolidations/osv.py` (+ export), тесты `api/tests/`.
|
||||
- `web/src/widgets/ConsolidatedOsvContent/`, `web/src/widgets/ConsolidatedOsv/` (`OsvTabs.tsx`, новый режим),
|
||||
`web/src/layouts/ConsolidationReportPage.tsx`, `web/src/utils/consolidations.ts`, `web/src/services/api/osv.ts`,
|
||||
`web/src/types/api.ts`.
|
||||
- Документация: `../docs/context/05-domain-rules.md` §3 п. 9, `../docs/context/10-user-journey.md` (строка 3.1),
|
||||
`../docs/dev/api-contract-slice-4.md` — отдельный PR в docs.
|
||||
@ -0,0 +1,50 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### 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 символов, как фильтр свода); постранично, как таблица свода. Если
|
||||
`missing_rows_total = 0` — вместо таблицы текст «У <Б> в своде нет строк: ОСВ не привязаны или по ним пробелы»; если
|
||||
строки у Б есть, а без ИНН и без сопоставления нет (`missing_unmapped_rows = 0`) — текст «Строк без ИНН нет: возможно,
|
||||
ИНН контрагента из файла не найден в справочнике или это пропуск в учёте».
|
||||
|
||||
«Указать» SHALL открывать модалку сопоставления из требования «Ручное сопоставление контрагента» с теми же ответами и
|
||||
ошибками; после «Связать» экран SHALL перечитывать сводку и обе таблицы. Если выбранная пара исчезла из списка, выбирается
|
||||
первая оставшаяся; пустой список — текст «Все участники периметра видят друг друга» (переключатель — «Встречные записи (0)»).
|
||||
|
||||
#### Scenario: Пара Русского бисквита
|
||||
- **WHEN** открыт свод версии РБ 2024 Q3 до сопоставления и выбран режим «Встречные записи»
|
||||
- **THEN** переключатель «Встречные записи (1)»; слева пара «Русский бисквит ← Пекарня Счастливочная»; в центре строки
|
||||
Счастливочной с контрагентом «Русский бисквит»; справа строки РБ без ИНН, среди них «Пекарня Счастливочная ООО» с
|
||||
«Указать»
|
||||
|
||||
#### Scenario: Сопоставление из режима закрывает пару
|
||||
- **WHEN** в правой таблице у «Пекарня Счастливочная ООО» нажато «Указать» и выбрана ООО «Пекарня Счастливочная»
|
||||
- **THEN** после «Связать» список пар пуст, показан текст «Все участники периметра видят друг друга», переключатель —
|
||||
«Встречные записи (0)»
|
||||
|
||||
#### Scenario: Режим из адреса
|
||||
- **WHEN** открыт адрес `…/versions/{id}/osv?view=pairs`
|
||||
- **THEN** показан режим «Встречные записи» с первой парой; кнопка «Назад» браузера не проходит по переключениям режимов
|
||||
@ -0,0 +1,75 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### 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`
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Страница свода и сводка
|
||||
`GET …/osv` (`CONSOLIDATION_VIEW`, policy) SHALL отдавать `Page` строк свода в порядке: имя ЮЛ, `account_code`,
|
||||
`row_number`, первый `entry_id`; `limit` ≤ 2000 (по умолчанию 500). Фильтры: `legal_entity_id`, `account_root`,
|
||||
`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`
|
||||
- **THEN** все строки ответа с `counterparty_id = null` и без `counterparty_inn`, `total` = `summary.unmapped_total`
|
||||
|
||||
#### Scenario: Строка с ИНН вне справочника не требует сопоставления
|
||||
- **WHEN** у строки `counterparty_inn = "7701000001"`, такого ИНН и имени в справочнике нет (`counterparty_id = null`)
|
||||
- **THEN** строки нет в `GET …/osv?unmapped=true`, она не входит в `summary.unmapped_total`
|
||||
|
||||
#### Scenario: ИНН сопоставленного ЮЛ
|
||||
- **WHEN** строка без ИНН в файле сопоставлена с ЮЛ по алиасу (на лету или хранимо)
|
||||
- **THEN** `counterparty_inn = null`, `counterparty_entity_inn` — ИНН этого ЮЛ из справочника
|
||||
|
||||
#### Scenario: Фильтр по способу
|
||||
- **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` пары
|
||||
@ -0,0 +1,49 @@
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Предупреждение «Остались неуказанные ИНН»
|
||||
**Reason**: модалка всплывала при каждом уходе с «ОСВ», пока `unmapped_total > 0`, а счётчик на реальных данных до нуля
|
||||
не доходит (зарплата, подотчётные лица); заказчик воспринимал её как требование сопоставить всё (AURORA-1746, чек-лист
|
||||
Циликова 23.10.2026). Для шахматки важны только пары участников без встречных записей.
|
||||
**Migration**: заменено требованием «Предупреждение «Нет встречных записей»».
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### 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** вкладка «ОСВ» остаётся, открыт режим «Встречные записи» с первой парой
|
||||
Loading…
x
Reference in New Issue
Block a user