openspec: change statement-template-lines — полный состав строк ББ/ОФР (#80)
Спека: достройка формы по справочнику строк редакции в GET …/statement, создание строки и ячейки при первой правке по line_code, пустые шаблонные строки вне проверок §7 и статуса. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qoydUVv8g1qHxc123u2RR
This commit is contained in:
parent
8be43512af
commit
5c7d5393c5
2
openspec/changes/statement-template-lines/.openspec.yaml
Normal file
2
openspec/changes/statement-template-lines/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-09
|
||||
141
openspec/changes/statement-template-lines/design.md
Normal file
141
openspec/changes/statement-template-lines/design.md
Normal file
@ -0,0 +1,141 @@
|
||||
# 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. Достройка ответа
|
||||
`complete_form(form_type, edition, columns, lines) -> lines'`:
|
||||
- для каждой строки из БД — недостающие ячейки по всем `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` на запрос, как сейчас; в `before/after` ячейки созданной строки ключ —
|
||||
`line_code` + `column_index`, `before.amount = 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`, ключ ячейки — `rowKey:column_id`; `draftToPatch`
|
||||
отправляет `line_id` или `line_code` по ключу. Достроенная пустая ячейка (`id = null`) без подсветки «требует проверки»
|
||||
(`is_recognized = null`), введённое значение подсвечивается как правка. `formLayout` уже раскладывает строки по разделам —
|
||||
меняется только то, что разделы теперь всегда полные. Сверка с макетом [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 не требует | — |
|
||||
67
openspec/changes/statement-template-lines/proposal.md
Normal file
67
openspec/changes/statement-template-lines/proposal.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Proposal: полный состав строк ББ/ОФР на экране проверки
|
||||
|
||||
**Срез:** доработка Среза 3b (`../docs/context/09-delivery-plan.md`) — экран проверки форм ББ/ОФР.
|
||||
**Репозитории:** arm-core (этот change, issue RSHB/arm-core#80); arm-web — PR в `test` (раскладка, правка по коду строки);
|
||||
`../docs` — bounded-PR: правило в `05-domain-rules.md` §7 «Ядро» и §11/§12, OQ-150 в `07-open-questions.md`, гайды
|
||||
`dev/frontend-api-guide.md`, `dev/qa-guide.md`. Follow-up: словарь `fsbu4` в scan-gateway (OQ-150 для нормализатора).
|
||||
DBML и контракты обмена не меняются, миграций нет.
|
||||
|
||||
## Why
|
||||
|
||||
Ядро хранит только строки формы, пришедшие от OCR или разбора Excel (`financial_statement_line`), и правит только
|
||||
существующую ячейку (`PATCH …/statement` по `line_id` + `column_id`, иначе 422 `UNKNOWN_CELL`). Если источник строку
|
||||
потерял — пример: «Русский бисквит», 1 кв. 2025, ИС склеила «1500\n1700» и строка 1700 пропала (до OQ-156), — аналитик
|
||||
не может её заполнить, хотя форма бухгалтерской отчётности обязана её содержать. То же с ячейкой, которой нет у
|
||||
существующей строки в какой-то колонке. Решение владельца 09.10.2026: на экране проверки всегда показывать все строки
|
||||
формы своей редакции и давать их заполнять.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Справочник строк форм по редакциям** — `api/rules/statement_lines.yaml` (данные, не код): редакции `66n`
|
||||
(≤ 2024-12-31) и `fsbu4` (с 2025-01-01), для каждой — ББ и ОФР: коды в порядке формы и наименования. Источник обеих
|
||||
редакций — шаблоны банка `../fixtures/statements-xlsx/templates/` («ББ/ОФР до 01.01.2025», «ББ/ОФР после 01.01.2025»)
|
||||
с исправленными склейками наименований и заголовков разделов (решение владельца 09.10.2026: данные OQ-150 — из
|
||||
шаблонов).
|
||||
- **`GET /documents/{id}/statement` достраивает форму.** К строкам из БД добавляются строки справочника редакции
|
||||
`edition`, которых в форме нет, и недостающие ячейки у существующих строк по всем колонкам формы. У достроенной строки
|
||||
`id = null`, `origin = "template"`, наименование — из справочника; у достроенной ячейки `id = null`, `amount = null`,
|
||||
`is_recognized = null`, `is_modified = false`. Новое поле `LineOut.origin`: `source` — строка пришла из источника,
|
||||
`template` — строка из справочника (достроенная или созданная правкой аналитика). Без `edition` (нет даты периода)
|
||||
строки не достраиваются, недостающие ячейки — достраиваются.
|
||||
- **`PATCH …/statement` создаёт строку и ячейку при первой правке.** Элемент `values[]` адресует ячейку либо
|
||||
`line_id` + `column_id` (как сейчас; если у строки нет ячейки в этой колонке — ячейка создаётся), либо `line_code` +
|
||||
`column_id` — для строки справочника редакции, которой нет в форме: ядро создаёт `financial_statement_line`
|
||||
(`line_code_original_value = NULL` — признак «не из источника», наименование из справочника) и
|
||||
`financial_statement_value` (`original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`). Дальше — как любая
|
||||
правка: `is_modified`, `modified_by/at`, один `audit_event(RECOGNIZED_DATA_EDIT)` с `before = null`, сброс акцепта,
|
||||
пересчёт статуса.
|
||||
- **Пустая шаблонная строка не влияет на статус и проверки** (решение владельца 09.10.2026). Достроенные строки в БД
|
||||
не пишутся и в проверки §7, `values_ok` (D-6) и автоподтверждение не попадают. Созданная правкой строка (`origin =
|
||||
template`) участвует в проверках §7, только пока у неё есть хотя бы одна непустая сумма; очищенная до пустоты —
|
||||
снова как будто её нет.
|
||||
|
||||
Таблицы DBML v5.17: `financial_statement_line` (`line_code_original_value` NULL — признак строки не из источника),
|
||||
`financial_statement_value`, `financial_statement_meta` (сброс акцепта), `document` (статус), `audit_event`. Новых полей
|
||||
нет.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Запись достроенных строк в БД при маппинге и миграция данных — нет: БД хранит то, что пришло (первичка).
|
||||
- Обязательность заполнения: пустая строка не блокирует подтверждение и автоподтверждение; полноту ловят проверки §7.
|
||||
- Заполнение `fsbu4` в словаре scan-gateway (нормализатор, golden-эталоны) — follow-up issue.
|
||||
- Строки без кода (OQ-36, OQ-141, OQ-149), пятизначные подстроки в справочнике, отдельные формы для упрощённой
|
||||
отчётности малых предприятий — нет.
|
||||
- Порядок строк в ответе — по коду, как сейчас; раскладка по разделам — на фронте (`formLayout.ts`).
|
||||
- Удаление созданной строки аналитиком — нет (очистка суммы до `null` возвращает её в состояние «пустая шаблонная»).
|
||||
- Правка кода и наименования строки — нет (решение Среза 3b).
|
||||
|
||||
## Impact
|
||||
|
||||
- Код: `domain/statement_lines.py` (новый: загрузка справочника, достройка формы — чистые функции),
|
||||
`api/rules/statement_lines.yaml` (новый), `schemas/statements.py` (`LineOut.id`/`ValueOut.id` nullable,
|
||||
`LineOut.origin`, `CellPatch` с `line_id | line_code`), `services/statements.py` (`build_out`, `_apply_cells`),
|
||||
`repositories/statements.py` (создание строки/ячейки, исключение пустых шаблонных строк в `statement_amounts`).
|
||||
- Спеки (дельты): `documents-api` («Форма ББ/ОФР для экрана проверки», «Правка формы»), `statement-checks`
|
||||
(«Соотношения §7 по колонкам»).
|
||||
- Фронт arm-web: ключ шаблонной строки, отправка `line_code`, отсутствие подсветки «требует проверки» у пустых
|
||||
достроенных ячеек.
|
||||
@ -0,0 +1,103 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Форма ББ/ОФР для экрана проверки
|
||||
`GET /api/v1/documents/{document_id}/statement` (`DOCUMENT_VIEW`, policy по ЮЛ) SHALL отдавать для документа
|
||||
`BALANCE`/`INCOME`: `document` (`id`, `document_type`, `status`, `period_year`, `period_quarter`, `reporting_date`,
|
||||
`counterparty_check`, `locator`, `file_id`); `edition` (`66n` при дате периода ≤ 2024-12-31, `fsbu4` — позже, `null` без
|
||||
даты); `header` — для `inn`, `report_date`, `units`: `value`, `original_value`, `confidence` (у ИНН), `verified`,
|
||||
`modified`; плюс `organization_name`, `okud`, `okpo`, `okopf`, `okfs`, `address`; `columns[]` (`id`, `column_index`,
|
||||
`label`, `period_date`, `is_current`); `lines[]` по `line_code` (`id`, `origin`, `line_code`, `line_code_confidence`,
|
||||
`line_name`, `line_name_confidence`, `values[]` по `column_index`: `id`, `column_id`, `amount`, `string_value`,
|
||||
`confidence`, `is_recognized`, `is_modified`, `original_amount`, `modified_by`, `modified_at`); `checks` (`failed`,
|
||||
`soft_failed`, `cross_form`, `accepted`: `by`, `at`, `comment` или `null`). Документ другого типа — 409
|
||||
`DOCUMENT_NOT_STATEMENT`; чужой или несуществующий — 404.
|
||||
|
||||
Форма SHALL достраиваться в ответе по справочнику строк редакции `edition` (`api/rules/statement_lines.yaml`, свой
|
||||
список кодов и наименований для `BALANCE` и `INCOME` каждой редакции): строки справочника, кода которых нет в форме,
|
||||
добавляются с `id = null`, `origin = "template"`, наименованием из справочника и `*_confidence = null`; у каждой строки
|
||||
SHALL быть значение по каждой колонке формы — недостающее добавляется с `id = null`, `amount = null`,
|
||||
`string_value = null`, `confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`,
|
||||
`modified_by = null`, `modified_at = null`. При `edition = null` строки не достраиваются, недостающие значения —
|
||||
достраиваются. Строка из БД имеет `origin = "source"`, если пришла из источника, и `"template"`, если создана правкой
|
||||
аналитика (`line_code_original_value IS NULL`). Строки источника, которых нет в справочнике, отдаются как есть.
|
||||
Достроенные строки и значения MUST NOT записываться в БД при чтении.
|
||||
|
||||
#### Scenario: Форма для экрана
|
||||
- **WHEN** запрошена форма `BALANCE` из `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
|
||||
- **THEN** `edition = fsbu4`, все 37 строк файла — `origin = source` с `id`, каждый код справочника `fsbu4` / `BALANCE`
|
||||
есть среди `lines`, у каждой строки значения по всем колонкам, `checks.failed` пуст
|
||||
|
||||
#### Scenario: Строка, потерянная источником
|
||||
- **WHEN** у формы `BALANCE` за 2025 Q1 в БД нет строки 1700 (OCR склеил «1500\n1700» до OQ-156)
|
||||
- **THEN** в `lines` есть 1700: `id = null`, `origin = template`, `line_name = "БАЛАНС"`, значения по всем колонкам
|
||||
`id = null`, `amount = null`, `is_recognized = null`; статус документа и `checks` — те же, что без достройки
|
||||
|
||||
#### Scenario: Недостающая ячейка
|
||||
- **WHEN** у строки 1150 в БД есть значение только для колонки 0, а у формы две колонки
|
||||
- **THEN** у 1150 два значения; значение колонки 1 — `id = null`, `amount = null`
|
||||
|
||||
#### Scenario: Нет даты периода
|
||||
- **WHEN** у формы OCR не определена дата периода (`edition = null`)
|
||||
- **THEN** `lines` — только строки из БД, у каждой значения по всем колонкам
|
||||
|
||||
#### Scenario: Не форма
|
||||
- **WHEN** запрошена форма документа `OSV`
|
||||
- **THEN** 409 `DOCUMENT_NOT_STATEMENT`
|
||||
|
||||
### Requirement: Правка формы
|
||||
`PATCH /api/v1/documents/{document_id}/statement` (`DOCUMENT_EDIT`, policy) SHALL принимать частичное тело `header`
|
||||
(`inn` — 10 или 12 цифр, иначе 422 `INVALID_INN`; `report_date` — дата; `units` — непустая строка ≤ 50) и `values[]`
|
||||
(ровно одно из `line_id` | `line_code`, плюс `column_id`, `amount` — число с ≤ 2 знаками или `null`) и в одной
|
||||
транзакции: при первой правке значения записывать `original_amount` = прежняя сумма; ставить `amount`,
|
||||
`is_modified = (amount ≠ original_amount)`, `modified_by/at`; для поля шапки — `*_modified = true`, `*_original_value`
|
||||
не менять; после `inn` — пересчитать `counterparty_check`; после `report_date` — пересчитать `period_year`,
|
||||
`period_quarter`, `reporting_date`; сбросить акцепт проверок; пересчитать статус; записать одно
|
||||
`audit_event(RECOGNIZED_DATA_EDIT, RECOGNITION)` с `before/after` изменённых полей и ячеек; вернуть форму, как `GET`.
|
||||
Документ `CONFIRMED` — 409 `DOCUMENT_READONLY`; файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`; не форма — 409
|
||||
`DOCUMENT_NOT_STATEMENT`; ячейка не этого документа — 422 `UNKNOWN_CELL`. Запрос без изменений — 200 без аудита.
|
||||
Блокировки — «файл `FOR UPDATE` → документ `FOR NO KEY UPDATE`».
|
||||
|
||||
Ячейка, которой нет в БД, SHALL создаваться при правке:
|
||||
- по `line_id` строки формы и `column_id` колонки формы, если у строки нет значения в этой колонке;
|
||||
- по `line_code`, если строки с этим кодом в форме нет, а код есть в справочнике редакции документа для типа формы:
|
||||
создаётся `financial_statement_line` (`line_code`, `line_name` из справочника, `line_code_original_value = NULL`,
|
||||
`line_name_original_value = NULL`, `*_confidence = NULL`, `*_modified = false`); несколько ячеек одной новой строки в
|
||||
запросе создают одну строку.
|
||||
Созданная ячейка — `original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`, `string_value = NULL`, далее
|
||||
общая логика правки. `line_code`, строка с которым в форме уже есть, код вне справочника редакции или редакция не
|
||||
определена — 422 `UNKNOWN_CELL`; колонка не этой формы — 422 `UNKNOWN_CELL`; одна ячейка дважды в запросе — 422
|
||||
`DUPLICATE_CELL`. `amount = null` для ячейки, которой нет в БД, — не изменение: ничего не создаётся и не аудируется.
|
||||
|
||||
#### Scenario: Правка значения
|
||||
- **WHEN** аналитик меняет сумму строки 1150 колонки 0 с 100 на 120
|
||||
- **THEN** `amount = 120`, `original_amount = 100`, `is_modified = true`, аудит `RECOGNIZED_DATA_EDIT` с `before 100`, `after 120`
|
||||
|
||||
#### Scenario: Возврат к исходному
|
||||
- **WHEN** затем та же ячейка меняется обратно на 100
|
||||
- **THEN** `is_modified = false`, `original_amount = 100`
|
||||
|
||||
#### Scenario: Заполнение строки, которой нет в форме
|
||||
- **WHEN** у формы `BALANCE` редакции `fsbu4` нет строки 1700, аналитик передаёт `{line_code: "1700", column_id: <колонка 0>, amount: 2371333}`
|
||||
- **THEN** в БД создана строка 1700 (`line_code_original_value = NULL`, `line_name = "БАЛАНС"`) и значение
|
||||
`amount = 2371333`, `original_amount = NULL`, `is_modified = true`; в ответе у 1700 `id` заполнен, `origin = template`;
|
||||
аудит `RECOGNIZED_DATA_EDIT` с `before.amount = null`; акцепт сброшен, статус пересчитан
|
||||
|
||||
#### Scenario: Код, который уже есть в форме
|
||||
- **WHEN** передан `line_code: "1150"`, а строка 1150 в форме есть
|
||||
- **THEN** 422 `UNKNOWN_CELL`, данные не изменились
|
||||
|
||||
#### Scenario: Код вне справочника
|
||||
- **WHEN** передан `line_code: "1105"` для формы редакции `66n`
|
||||
- **THEN** 422 `UNKNOWN_CELL`
|
||||
|
||||
#### Scenario: Пустая сумма в несуществующую ячейку
|
||||
- **WHEN** передан `{line_code: "1530", column_id: <колонка 0>, amount: null}` и строки 1530 нет
|
||||
- **THEN** 200, строка не создана, аудита нет
|
||||
|
||||
#### Scenario: Правка даты отчёта
|
||||
- **WHEN** у формы OCR без периода дата отчёта правится на 2025-06-30
|
||||
- **THEN** `period_year = 2025`, `period_quarter = Q2`, `report_date_modified = true`, `report_date_original_value` не изменился
|
||||
|
||||
#### Scenario: Подтверждённая форма
|
||||
- **WHEN** правится форма `CONFIRMED`
|
||||
- **THEN** 409 `DOCUMENT_READONLY`, данные не изменились
|
||||
@ -0,0 +1,45 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Соотношения §7 по колонкам
|
||||
Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки
|
||||
отдельно по словарю `api/rules/statement_checks.yaml` (копия словаря tabular-parser): соотношение проверяется, только
|
||||
если присутствует его левая часть; отсутствующий код и пустая сумма — 0; выполнено, если `|левая − правая| ≤ tolerance`
|
||||
(1, в единицах формы) хотя бы в одном варианте правой части — как записано или с кодами `abs_terms` по модулю. Нарушение
|
||||
жёсткого (`strict`) соотношения, не превышающее `rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа
|
||||
соотношения, SHALL считаться мягким («в пределах округления»). Мягкие: соотношения `strict: false` и «сумма пятизначных
|
||||
подстрок = четырёхзначный родитель». Результат — `{failed: [...], soft_failed: [...]}`, элемент — `{column_index,
|
||||
relation, expected, actual}`; MUST NOT храниться в БД.
|
||||
|
||||
Присутствующими SHALL считаться строки формы из БД, кроме строк, созданных правкой аналитика
|
||||
(`line_code_original_value IS NULL`), у которых все суммы пустые; строки, достроенные в ответе по справочнику
|
||||
(`documents-api`, `id = null`), в проверках MUST NOT участвовать. Созданная правкой строка с хотя бы одной непустой
|
||||
суммой проверяется по всем колонкам, пустые суммы — 0.
|
||||
|
||||
#### Scenario: Сходящийся баланс
|
||||
- **WHEN** проверяется форма `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
|
||||
- **THEN** `failed` пуст
|
||||
|
||||
#### Scenario: Нарушение жёсткого соотношения
|
||||
- **WHEN** у баланса 1600 = 1000, 1100 = 300, 1200 = 600 в колонке 0
|
||||
- **THEN** `failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected: 1000, actual: 900}`
|
||||
|
||||
#### Scenario: Расходы со знаком минус
|
||||
- **WHEN** ОФР 2110 = 100, 2120 = −60, 2100 = 40
|
||||
- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms`
|
||||
|
||||
#### Scenario: Правка исправляет нарушение
|
||||
- **WHEN** аналитик правит 1200 с 600 на 700 в форме из предыдущего сценария
|
||||
- **THEN** проверка по текущим суммам: `failed` пуст
|
||||
|
||||
#### Scenario: Пустая шаблонная строка не проверяется
|
||||
- **WHEN** в форме нет строки 1700 (в ответе она достроена пустой), 1300 + 1400 + 1500 = 2 371 333 в колонке 0
|
||||
- **THEN** `1700 = 1300 + 1400 + 1500` и `1600 = 1700` не проверяются, `failed` — как без достройки
|
||||
|
||||
#### Scenario: Заполненная шаблонная строка проверяется
|
||||
- **WHEN** аналитик заполнил 1700 = 2 371 333 только в колонке 0, а в колонке 1 1300 + 1400 + 1500 = 2 000 000
|
||||
- **THEN** в колонке 0 соотношение выполнено; в колонке 1 `failed` содержит `1700 = 1300 + 1400 + 1500` с `expected 0`,
|
||||
`actual 2000000`
|
||||
|
||||
#### Scenario: Очищенная шаблонная строка
|
||||
- **WHEN** затем аналитик очищает 1700 в колонке 0 (`amount = null`)
|
||||
- **THEN** строка 1700 снова не участвует в проверках
|
||||
Loading…
x
Reference in New Issue
Block a user