Andrey Epifancev 2bb5683cde docs(#195): таблица решений без разрыва
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:57:03 +04:00

14 KiB
Raw Blame History

Дизайн: пары участников без встречных записей (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, status, 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 Колонки таблиц режима — счёт, контрагент, обороты Дт/Кт, сальдо на конец Дт/Кт (без сальдо на начало) Колонки должны уместиться на всю ширину при 1440; для поиска пары важнее обороты и итог Низкая: добавить колонки
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 R-1 (design #9): needsUnmappedWarning удаляется в Task 4 вместе с его использованием в OsvTabs, а не в Task 3 Иначе между коммитами Task 3 и 4 сборка (tsc) красная Нулевая
27.10.2026 R-2 (design #10): в тексте модалки «Ещё K строк без ИНН…» слово «строк» не склоняется, M — число разных missing_id (по спеке) Спека дословна; склонение K=1 даёт всё равно неграмматичную фразу («учтены») Низкая
27.10.2026 R-3 (design #11): отдельное ревью Task 6 не проводится — её diff в arm-core (одна фраза в CLAUDE.md) и docs-PR #176 проверяются финальным ревью всей ветки Задача не содержит кода; отдельный ревьюер дублировал бы финальный Низкая
27.10.2026 R-4 (design #12): подпись под парой в списке — «записей: N · без ИНН у <Б>: M» вместо служебных «держатель»/«Б» Аналитик не знает ролей «держатель» и «Б»; название организации понятнее Низкая
27.10.2026 R-5 (design #13): раскладка режима «Встречные записи» — пары одной строкой «таблеток» с переносом, под ними две таблицы на всю ширину одна под другой (вместо трёх колонок) В трёх колонках таблицам не хватало ширины: суммы обрезались, видны были только «Обороты Дт» (находка H-1) Низкая
27.10.2026 R-6 (design #14): уход с «ОСВ» — закрытая версия (FINISHED_STATUSES) переходит без запроса сводки; при summary = null запрашивается сводка, при ошибке запроса переход идёт без модалки и без своего сообщения; на время догрузки переключатель вкладок блокируется; выбранная пара хранится в адресе и в режиме «Свод» (?holder=&missing= без view) Модалка нужна только открытой версии; ошибку сводки показывает общий слой; блокировка исключает двойные клики; адрес — единственное хранилище состояния режима Низкая

Отложено решением владельца 27.10.2026: строки Б с ИНН вне справочника — AURORA-1866 (разбор); подсветка кандидата по сумме — AURORA-1867.