README: достройка GET, адресация PATCH по line_code, строка UNKNOWN_CELL. CLAUDE.md: абзац про справочник строк. design.md: рулинги R-9…R-11. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
152 lines
20 KiB
Markdown
152 lines
20 KiB
Markdown
# Design: полный состав строк ББ/ОФР на экране проверки
|
||
|
||
## Context
|
||
|
||
Форма ББ/ОФР в ядре — `financial_statement_meta` → `financial_statement_column` (колонки-периоды) →
|
||
`financial_statement_line` (только строки с кодом, `(meta_id, line_code)` уникален) → `financial_statement_value`
|
||
(`(line_id, column_id)` уникален). Строки и ячейки пишет только маппинг (`repositories/statements.insert_statement`), ровно
|
||
то, что пришло в `statementDocument` от scan-gateway или tabular-parser. Экран проверки читает форму
|
||
`GET /documents/{id}/statement` (`services/statements.StatementService.build_out`, порядок строк — по коду), правит
|
||
`PATCH …/statement` (`_apply_cells`: `lock_cells` по `(line_id, column_id)` под `FOR UPDATE`; чужая или отсутствующая
|
||
ячейка — 422 `UNKNOWN_CELL`). Проверки §7 (`domain/statement_checks.run_checks`) считают цель присутствующей, если её
|
||
код есть среди строк формы; пустая сумма — 0. Статус D-6 (`statement_status`) учитывает `values_ok` по всем ячейкам и
|
||
`has_lines`. Редакция формы — `edition_for(document.reporting_date)`: `66n` (≤ 2024-12-31) / `fsbu4` / `None`.
|
||
Раскладку «как на бумаге» по разделам делает фронт (`arm-web/src/widgets/DocumentReview/formLayout.ts`).
|
||
|
||
Справочник «код → наименование» по редакциям есть только в scan-gateway (`api/rules/statement_lines.yaml`): `66n`
|
||
заполнен (из старого ядра), `fsbu4` пуст (OQ-150).
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** аналитик видит все строки формы своей редакции и может заполнить любую, включая потерянную источником;
|
||
уже загруженные документы получают это без миграции; распознанные данные не меняются; пустые строки не меняют статус,
|
||
проверки и автоподтверждение.
|
||
|
||
**Non-Goals:** см. proposal.
|
||
|
||
## Decisions
|
||
|
||
### D-1. Достраивать в ответе, а не в БД (подход B, решение владельца 09.10.2026)
|
||
БД хранит только пришедшее из источника; достройка — чистая функция `domain/statement_lines.complete_form` поверх
|
||
прочитанной формы в `build_out`. Альтернатива «дописать строки при маппинге» отклонена: нужна data-миграция для уже
|
||
загруженных документов, в БД появляются строки, которых не было в первичке, и проверки §7 начали бы видеть пустые итоги
|
||
(пустая 1700 при непустых 1300–1500 — жёсткое нарушение), то есть изменился бы статус документов.
|
||
|
||
### D-2. Справочник — данные ядра `api/rules/statement_lines.yaml`
|
||
```yaml
|
||
editions:
|
||
- id: 66n
|
||
valid_until: 2024-12-31
|
||
forms:
|
||
BALANCE: [{code: "1110", name: "Нематериальные активы"}, ...] # порядок формы
|
||
INCOME: [...]
|
||
- id: fsbu4
|
||
valid_from: 2025-01-01
|
||
forms: {BALANCE: [...], INCOME: [...]}
|
||
```
|
||
Список, а не словарь — порядок строк формы сохраняется (пригодится фронту и будущему порядку в ответе; сейчас ответ
|
||
сортируется по коду, как раньше). Загрузка — один раз при импорте модуля (как `statement_checks.yaml`), валидация
|
||
pydantic: коды — четыре цифры, без повторов внутри формы, обе формы в обеих редакциях. Редакция выбирается существующим
|
||
`edition_for` — новой логики выбора нет.
|
||
|
||
Состав (решение владельца 09.10.2026 — из шаблонов `../fixtures/statements-xlsx/templates/`):
|
||
- **66n, ББ** («ББ до 01.01.2025»): 1110, 1120, 1130, 1140, 1150, 1160, 1170, 1180, 1190, 1100, 1210, 1220, 1230, 1240,
|
||
1250, 1260, 1200, 1600, 1310, 1320, 1340, 1350, 1360, 1370, 1300, 1410, 1420, 1430, 1450, 1400, 1510, 1520, 1530,
|
||
1540, 1550, 1500, 1700 (37).
|
||
- **66n, ОФР** («ОФР до 01.01.2025»): 2110, 2120, 2100, 2210, 2220, 2200, 2310, 2320, 2330, 2340, 2350, 2300, 2410,
|
||
2411, 2412, 2460, 2400, 2510, 2520, 2500, 2900, 2910 (22).
|
||
- **fsbu4, ББ** («ББ после 01.01.2025»): 1110, 1150, 1160, 1170, 1180, 1190, 1100, 1210, 1215, 1220, 1230, 1240, 1250,
|
||
1260, 1200, 1600, 1310, 1320, 1340, 1350, 1360, 1370, 1300, 1410, 1420, 1430, 1450, 1400, 1510, 1520, 1540, 1550,
|
||
1500, 1700 (34).
|
||
- **fsbu4, ОФР** («ОФР после 01.01.2025»): 2110, 2120, 2100, 2210, 2220, 2200, 2310, 2320, 2330, 2340, 2350, 2300,
|
||
2410, 2411, 2412, 2420, 2400, 2510, 2520, 2530, 2500, 2900, 2910 (23).
|
||
|
||
Наименования — из тех же шаблонов; склейки с заголовками разделов и сносками исправляются (шаблон склеил объединённые
|
||
ячейки): 1110 «АКТИВ» → «Нематериальные активы», 1210 «II. Оборотные активы» → «Запасы», 1310 «ПАССИВ» → «Уставный
|
||
капитал (складочный капитал, уставный фонд, вклады товарищей)», 1410 «IV. Долгосрочные обязательства» и 1510 «V.
|
||
Краткосрочные обязательства» → «Заемные средства», 2411 «в том числе:» → «текущий налог на прибыль», хвосты сносок
|
||
(«Выручка 5», «Налог на прибыль 7», «( / )») и префикс «Справочно» у 2900 убираются, переносы строк — в пробел.
|
||
|
||
Строки источника, которых нет в справочнике (пятизначные 11101, коды чужой редакции, 1105 «Гудвил» у OCR), остаются как
|
||
есть: справочник только добавляет, ничего не убирает и не переименовывает.
|
||
|
||
### D-3. Достройка ответа
|
||
Достройка (по факту — `_lines_out` в `schemas/statements.py`, R-9; логика та же):
|
||
- для каждой строки из БД — недостающие ячейки по всем `columns` (`id = null`, `amount = null`, `string_value = null`,
|
||
`confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`, `modified_* = null`);
|
||
- при `edition ≠ None` — строки справочника, кода которых нет среди строк формы: `id = null`, `line_code`, `line_name` из
|
||
справочника, `*_confidence = null`, `origin = "template"`, ячейки по всем колонкам как выше;
|
||
- `origin` строки из БД: `template`, если `line_code_original_value IS NULL` (строка создана правкой, D-4), иначе `source`
|
||
(маппинг всегда пишет `line_code_original_value = line_code`);
|
||
- итог — по `line_code` (как сейчас).
|
||
Форма без колонок (OCR без шапки) получает строки без ячеек — заполнить нечего, пока не появятся колонки (их ядро не
|
||
создаёт; Non-goal).
|
||
|
||
### D-4. Правка по коду строки
|
||
`CellPatch`: `line_id: uuid | None`, `line_code: str | None` (ровно одно, иначе 422 валидации тела), `column_id`, `amount`.
|
||
`_apply_cells` (под существующим порядком блокировок «файл `FOR UPDATE` → документ `FOR NO KEY UPDATE`», шапка с
|
||
`refresh`):
|
||
1. Колонка должна принадлежать форме, иначе 422 `UNKNOWN_CELL`.
|
||
2. `line_id`: строка формы (иначе `UNKNOWN_CELL`); если у неё нет ячейки в колонке — ячейка создаётся.
|
||
3. `line_code`: если строка с таким кодом в форме уже есть — 422 `UNKNOWN_CELL` с сообщением «строка уже есть в форме —
|
||
передайте line_id» (фронт после ответа знает `id`); если кода нет в справочнике редакции документа (или редакция не
|
||
определена) — 422 `UNKNOWN_CELL`. Иначе создаётся `financial_statement_line` (`line_code`, `line_name` из
|
||
справочника, `line_code_original_value = NULL`, `line_name_original_value = NULL`, `*_confidence = NULL`,
|
||
`*_modified = false`) и ячейка. Две ячейки одной новой строки в одном запросе создают одну строку.
|
||
4. Создаваемая ячейка: `original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`, `string_value = NULL`;
|
||
затем общая логика правки: `amount`, `is_modified = (amount ≠ original_amount)`, `modified_by/at`.
|
||
5. `amount = null` для несуществующей ячейки — не изменение: ничего не создаётся, в аудит не попадает.
|
||
6. Повтор ячейки в запросе (по `(line_id|line_code, column_id)`) — 422 `DUPLICATE_CELL`, как сейчас.
|
||
|
||
Гонки двух правок одной формы исключены блокировкой документа: второй запрос ждёт и видит строку, созданную первым
|
||
(получит 422 «передайте line_id» и перечитает форму). Уникальный индекс `(meta_id, line_code)` — страховка.
|
||
|
||
Аудит — одно `RECOGNIZED_DATA_EDIT` на запрос, как сейчас; ключ созданной ячейки — тот же `value:<id>` (id новой
|
||
ячейки), `before = null`.
|
||
|
||
### D-5. Пустые шаблонные строки вне проверок и статуса
|
||
Достроенные строки в БД нет — `statement_amounts`, `value_flags`, `has_lines`, `_line_amount` (межформенная) их не
|
||
видят. Созданная правкой строка (`line_code_original_value IS NULL`) участвует в `statement_amounts`, только если у неё
|
||
есть хотя бы одна непустая сумма (фильтр в запросе). Так очищенная до пустоты строка снова «как будто её нет», а
|
||
частично заполненная (1700 есть в текущей колонке, нет в прошлой) проверяется по всем колонкам, как строка источника, —
|
||
расхождение в незаполненной колонке подскажет аналитику заполнить и её. `values_ok` её ячейки не тревожат:
|
||
`is_recognized = NULL` и `confidence = NULL` — «без сомнений», правленные — `is_modified`.
|
||
|
||
### D-6. Фронт (arm-web, PR в `test`)
|
||
`statementGrid`: ключ строки — `line.id ?? "code-" + line_code` (R-10), ключ ячейки — `rowKey:column_id`; `draftToPatch`
|
||
отправляет `line_id` или `line_code` по ключу. Достроенная пустая ячейка (`id = null`) без подсветки «требует проверки»
|
||
(`is_recognized = null`), введённое значение подсвечивается как правка. `formLayout` уже раскладывает строки по разделам —
|
||
меняется только то, что разделы теперь всегда полные. **Строки «БАЛАНС» (1600 по активу, 1700 по пассиву) выделяются
|
||
цветом** (просьба владельца 09.10.2026: сейчас итог баланса сливается с обычными строками и «Итого по разделу»): фон
|
||
всей строки — светло-фиолетовый (`#f9f0ff`, purple-1 палитры antd): серый `#f2f4f7` уже у заголовков разделов,
|
||
светло-синий `#e6f4ff` — «исправлено», оранжевый `#fff7e6` — «требует проверки»; шрифт 700 у наименования, кода и сумм. Подсветка ячейки «требует проверки» / «правлено» перекрывает фон строки —
|
||
сигнал о данных важнее оформления. Признак — `formLayout` (`LayoutRow.grand: true` для 1600/1700 у `BALANCE`), цвет —
|
||
`StatementTable`. В arm-web — свой OpenSpec change `statement-template-lines` (дельта `document-review-ui`). Сверка с макетом [41] (PNG `docs/mockup/screens/`) в Playwright и
|
||
ручной прогон сценария: пустая строка → ввод → «Сохранить» → проверки пересчитаны.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Две копии справочника (ядро и scan-gateway) расходятся**: в ядре 66n — по шаблону банка (без 1105, 1215, 1330 и с
|
||
«Доходные вложения в материальные ценности» у 1160), в scan-gateway — словарь старого ядра. Наименования строк
|
||
источника не трогаются, так что расхождение видно только в наименовании достроенных строк. Follow-up: заполнить
|
||
`fsbu4` в scan-gateway и свести копии.
|
||
- **Шаблоны банка — не нормативный текст.** Если в ФСБУ 4/2023 окажутся строки, которых нет в шаблоне, они не
|
||
достроятся; правится данными (`statement_lines.yaml`), без кода.
|
||
- **Ответ GET растёт** на 30–40 строк × колонки — несущественно.
|
||
|
||
## Decisions made without discussion
|
||
|
||
| # | Дата | Решение | Обоснование | Цена ошибки |
|
||
|---|---|---|---|---|
|
||
| R-1 | 09.10.2026 | Состав **66n** в ядре — из шаблонов банка «до 01.01.2025», а не копия словаря scan-gateway (в чате согласовано «копия») | В словаре scan-gateway у 66n коды и имена эпохи ФСБУ (1105 «Гудвил», 1215, 1160 «Инвестиционная недвижимость»), а шаблон банка — тот же источник, что выбран для fsbu4; одна методика для обеих редакций | Неверное наименование или лишняя/недостающая достроенная строка у отчётности до 2025 года; правится данными |
|
||
| R-2 | 09.10.2026 | Наименования склеенных строк шаблона исправлены по форме (1110, 1210, 1310, 1410, 1510, 2411, сноски) | Склейка — артефакт объединённых ячеек шаблона, а не наименование строки | Неточное наименование достроенной строки; правится данными |
|
||
| R-3 | 09.10.2026 | Созданная правкой строка участвует в проверках §7, только пока у неё есть непустая сумма | Решение владельца «пустая строка не влияет на проверки» должно сохраняться и после очистки | Аналитик очистил итог — нарушение исчезло вместе с ним; так же ведёт себя отсутствующая строка источника |
|
||
| R-4 | 09.10.2026 | Без редакции (нет даты периода) строки не достраиваются, недостающие ячейки — достраиваются | Не из чего выбрать состав; дата периода нужна и для подтверждения (`PERIOD_REQUIRED`) — после её правки строки появятся | Аналитик не видит пустых строк, пока не введёт дату отчёта |
|
||
| R-5 | 09.10.2026 | `line_code` для кода, уже существующего в форме, — 422 `UNKNOWN_CELL`, а не тихая правка существующей строки | Один способ адресации существующей ячейки (`line_id`), второй запрос в гонке не перезапишет чужую правку молча | Лишний круг фронта: перечитать форму и повторить |
|
||
| R-6 | 09.10.2026 | Ячейка, которой нет у существующей строки, создаётся при правке по `line_id` | Та же проблема, что с отсутствующей строкой; отдельного API не требует | — |
|
||
| R-7 | 09.10.2026 | Цвет строк «БАЛАНС» — светло-фиолетовый `#f9f0ff` + жирный 700; подсветка ячейки перекрывает фон строки; ОФР не выделяется | Владелец просил выделить баланс по активу и пассиву, цвет не задал; в макете [41] строки «БАЛАНС» не видно; фиолетовый не занят: серый — разделы, синий — «исправлено», оранжевый — «требует проверки» | Цвет не понравится — правка одной константы |
|
||
| R-8 | 09.10.2026 | `has_lines` (условие D-6 «в форме есть строки») учитывает и строки, созданные правкой | Форму, у которой источник не дал строк (не найдена колонка кодов), аналитик может заполнить вручную — это его данные, как любая правка; статус всё равно требует пройденных проверок и завершённого задания | Документ без строк источника станет `RECOGNIZED` после ручного заполнения — ожидаемо |
|
||
| R-9 | 09.10.2026 | Достройка формы реализована функцией `_lines_out` в `schemas/statements.py`, а не `domain/statement_lines.complete_form` из D-3 | План конкретнее, поведение то же | Рефакторинг в domain позже |
|
||
| R-10 | 09.10.2026 | Ключ достроенной строки на фронте `code-<code>`, а не `code:<code>` из D-6 | ':' разделяет rowKey и columnId в cellKey | Нет |
|
||
| R-11 | 09.10.2026 | Адресация ячеек PATCH вынесена из `_apply_cells` в `_resolve_cells` (отклонение от кода плана ради ruff PLR0912/PLW2901); поведение и порядок проверок сохранены | Линтер | Нет |
|