680 lines
70 KiB
Markdown
680 lines
70 KiB
Markdown
# documents-api Specification
|
||
|
||
## Purpose
|
||
Чтение файлов, документов и строк ОСВ для панели «Первичные документы» (подсрез 2d) и подтверждение документа.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Файлы организации и карточка файла
|
||
`GET /api/v1/legal-entities/{id}/files` (`DOCUMENT_VIEW`, policy; `include_superseded=false` по умолчанию) SHALL
|
||
возвращать `Page` файлов ЮЛ по убыванию `uploaded_at`: поля `stored_file` (в том числе `consolidation_id: uuid | null`,
|
||
v5.18 — консолидация, в контексте которой файл загружен), текущее задание (`status`, `error_code`,
|
||
`error_message`, `mapping_error` (`metrics.mapping_error`: причина `ERROR` файла после неудачного маппинга, когда у задания
|
||
`error_code` пуст), `metrics.warnings`, `metrics.mapping_warnings`, `finished_at`), документы файла (`id`, `document_type`,
|
||
`osv_account_code`, `period_year`, `period_quarter`, `is_cumulative`, `status`, `period_check`, `counterparty_check`,
|
||
`comments_count`). `GET /api/v1/files/{file_id}` SHALL отдавать ту же карточку; чужой или несуществующий файл — 404.
|
||
|
||
#### Scenario: Список после обработки
|
||
- **WHEN** у ЮЛ два файла: `PROCESSED` с документом и `ERROR`
|
||
- **THEN** оба в списке; у первого `documents` непустой, у второго `job.error_code` заполнен
|
||
|
||
#### Scenario: Счётчик комментариев
|
||
- **WHEN** к документу файла добавлены два комментария
|
||
- **THEN** в карточке файла у документа `comments_count = 2`
|
||
|
||
#### Scenario: Консолидация в карточке
|
||
- **WHEN** один файл ЮЛ загружен в консолидацию `C`, другой — без контекста
|
||
- **THEN** в списке ЮЛ и в `GET /files/{id}` у первого `consolidation_id = C`, у второго `consolidation_id = null`
|
||
|
||
### Requirement: Документ и строки
|
||
`GET /api/v1/documents/{document_id}` (`DOCUMENT_VIEW`, policy по ЮЛ владельца) SHALL отдавать документ, статус,
|
||
проверки, `locator`, схему группировок листа из `recognition_job.group_schema` и `file_id`.
|
||
`GET /api/v1/documents/{document_id}/entries` SHALL отдавать `Page` строк по возрастанию `row_number` (`limit` ≤ 2000,
|
||
по умолчанию 500) с полями `osv_entry`, включая `parent_entry_id`, `account_id`, `counterparty_id`.
|
||
|
||
#### Scenario: Строки деревом
|
||
- **WHEN** запрошены строки документа ОСВ 66
|
||
- **THEN** первая строка — счёт `66` уровня 0 без родителя, у строк уровня 2 `parent_entry_id` указывает на субсчёт
|
||
|
||
### Requirement: Подтверждение документа
|
||
`POST /api/v1/documents/{document_id}/confirm` (`DOCUMENT_EDIT`, policy) SHALL переводить документ из `RECOGNIZED`
|
||
или `NEEDS_CHECK` в `CONFIRMED` (`confirmed_by`, `confirmed_at`) с `audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS)`;
|
||
из `CONFIRMED` повторно — 409 `ALREADY_CONFIRMED`; документ файла `SUPERSEDED` — 409 `FILE_SUPERSEDED`; документ в любом
|
||
другом статусе (например, `ERROR`) — 409 `DOCUMENT_NOT_CONFIRMABLE`. Для `BALANCE`/`INCOME` дополнительно: пустые
|
||
`period_year` или `period_quarter` — 409 `PERIOD_REQUIRED`; строки `unreadable` / `missing` с непустой суммой или текстом значения — 409 `LINES_UNRESOLVED` (`detail.lines[] {line_id, row_index, line_code_text}`); вторая подтверждённая форма того же типа и периода в файле — 409 `DUPLICATE_FORM` (`detail.document_id`); записи `document_issue` вида `relation` без акцепта — 409
|
||
`CHECKS_NOT_PASSED` со списком нарушений в `detail.failures`; при подтверждении SHALL записываться
|
||
`financial_statement_meta.reviewed_by/at`. Несовпадение контрагента и уверенность значений подтверждение MUST NOT
|
||
блокировать. Если совпали несколько условий, порядок: `FILE_SUPERSEDED`, `ALREADY_CONFIRMED`,
|
||
`DOCUMENT_NOT_CONFIRMABLE`, `PERIOD_REQUIRED`, `LINES_UNRESOLVED`, `DUPLICATE_FORM`, `CHECKS_NOT_PASSED`. Подтверждение берёт блокировки в порядке «файл →
|
||
документ → (advisory-блокировка привязок ГК) → версии консолидации (по возрастанию `id`)»: файл — `FOR UPDATE`,
|
||
документ — `FOR NO KEY UPDATE` (R-3; совместима со вставкой привязки `FOR KEY SHARE`), версии — `FOR UPDATE`; состояние
|
||
перечитывается под этими блокировками; документ или файл, исчезнувшие за время ожидания блокировки, — 404 `NOT_FOUND`.
|
||
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
|
||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
|
||
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
|
||
версии; false → `period_quarter` ≤ квартала версии). ОСВ за `Q1` равнозначна нарастающей и квартальной (R-14): она
|
||
подходит любой версии года при любом `is_cumulative`; ББ/ОФР за `Q1` — отчёт на конец квартала и подходят только версии
|
||
за `Q1` (R-14a). Привязанный ранее документ того же ЮЛ с тем же ключом (`document_type`, `osv_account_code`,
|
||
`period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же
|
||
версии SHALL отвязываться (R-1). Каждая версия, получившая
|
||
привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` =
|
||
подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться.
|
||
|
||
#### Scenario: Причина ошибки маппинга в карточке файла
|
||
- **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст)
|
||
- **THEN** `job.mapping_error` содержит причину, `job.error_code` пуст
|
||
|
||
#### Scenario: Документ не подтверждается
|
||
- **WHEN** подтверждается документ в статусе `ERROR`
|
||
- **THEN** 409 `DOCUMENT_NOT_CONFIRMABLE`, статус не изменился
|
||
|
||
#### Scenario: Подтверждение
|
||
- **WHEN** `ka_rf` подтверждает документ `NEEDS_CHECK` своего филиала
|
||
- **THEN** `status = CONFIRMED`, `confirmed_by` = пользователь, `audit_event(DOCUMENT_CONFIRM)` с `before_value.status = NEEDS_CHECK`
|
||
|
||
#### Scenario: Привязка к версии и переход статуса
|
||
- **WHEN** версия ГК 2024 Q3 в `INITIATED` с ЮЛ в периметре; подтверждается квартальная ОСВ 60 этого ЮЛ за 2024 Q2
|
||
- **THEN** `consolidation_document(version, document)` создан; версия в `DATA_CORRECTION`; запись истории
|
||
`INITIATED → DATA_CORRECTION` с `changed_by` = пользователь
|
||
|
||
#### Scenario: Не подходит по периоду
|
||
- **WHEN** подтверждается нарастающая ОСВ 60 за 2024 Q2 при версии за 2024 Q3
|
||
- **THEN** привязки нет, статус версии не изменился
|
||
|
||
#### Scenario: Версия в CANCELLED
|
||
- **WHEN** подходящий документ подтверждается при версии `CANCELLED`
|
||
- **THEN** привязки нет
|
||
|
||
#### Scenario: Форма с нарушением проверок
|
||
- **WHEN** подтверждается `BALANCE` с непройденным жёстким соотношением без акцепта
|
||
- **THEN** 409 `CHECKS_NOT_PASSED`, в `detail.failures` — нарушение; после акцепта подтверждение проходит
|
||
|
||
#### Scenario: Форма без периода
|
||
- **WHEN** подтверждается форма из OCR, у которой нет ни даты отчёта, ни дат колонок
|
||
- **THEN** 409 `PERIOD_REQUIRED`; после правки даты отчёта подтверждение проходит
|
||
|
||
#### Scenario: Форма в чек-листе
|
||
- **WHEN** версия ГК 2025 Q3 `INITIATED`, ЮЛ в периметре; подтверждается его `BALANCE` за 2025 Q3
|
||
- **THEN** привязка создана, ячейка «Бух. баланс» чек-листа ЮЛ — `CONFIRMED`, `reviewed_by` шапки = пользователь
|
||
|
||
#### Scenario: Неразобранная строка с суммой
|
||
- **WHEN** у формы строка `missing` с суммой и одновременно жёсткое нарушение
|
||
- **THEN** 409 `LINES_UNRESOLVED` с этой строкой в `detail.lines`
|
||
|
||
#### Scenario: Неразобранная строка только с текстом
|
||
- **WHEN** у формы строка `unreadable` без сумм, но с мусором `211(1 |` в ячейке значения (`string_value`)
|
||
- **THEN** 409 `LINES_UNRESOLVED` — потерянная сумма тоже требует разбора
|
||
|
||
#### Scenario: Вторая форма того же периода
|
||
- **WHEN** в файле два ББ на 30.09.2025, первый подтверждён, аналитик подтверждает второй
|
||
- **THEN** 409 `DUPLICATE_FORM` со ссылкой на первый
|
||
|
||
### Requirement: Подхват документов при создании версии и расширении периметра
|
||
При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из
|
||
незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`,
|
||
`osv_account_code`, `period_quarter`, признак «нарастающий»; NULL признака — нарастающий; для ОСВ за Q1 признак не
|
||
различается, R-1, R-14) — документ с самым поздним `confirmed_at`. Удаление ЮЛ из периметра SHALL
|
||
снимать привязку всех его документов в этой версии. Подхват и отвязка статус версии MUST NOT менять.
|
||
|
||
#### Scenario: Фикстуры, подтверждённые до консолидации
|
||
- **WHEN** ОСВ 60 РБ за Q1, Q2, Q3 2024 подтверждены, затем создана консолидация ГК за 2024 Q3
|
||
- **THEN** три `consolidation_document` на эти документы; в чек-листе ячейка ОСВ 60 РБ — `CONFIRMED`; версия в `INITIATED`
|
||
|
||
#### Scenario: Две подтверждённые версии одного документа
|
||
- **WHEN** у ЮЛ два незаменённых файла с `CONFIRMED` ОСВ 62 за 2024 Q3 (разные `locator`), создана версия 2024 Q3
|
||
- **THEN** привязан документ с более поздним `confirmed_at`
|
||
|
||
### 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[]` в порядке, описанном ниже (`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`). Элемент нарушения в `checks` — `{column_index,
|
||
relation, expected, actual, codes, line_ids}`; `expected` и `actual` SHALL быть `null`, когда сторона соотношения пуста
|
||
(пустая цель при непустых членах — `expected = null`, пакет `arm_statements`, P1-21), см. требование «Блок `checks`
|
||
ответа формы из `document_issue`». Документ другого типа — 409 `DOCUMENT_NOT_STATEMENT`; чужой или несуществующий — 404.
|
||
|
||
**Состав ответа v2.0.** `StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`;
|
||
`confidence` — строкой Decimal, как прочие уверенности API), `organization_name_modified` и `okei`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔
|
||
`row_index = null`), `row_index`, `source_ref`, `line_status`, `line_code`, `line_code_text`, `line_code_confidence`,
|
||
`line_name` (из файла), `reference_name` (справочное имя по коду — первое имя кода в словаре — или `null`),
|
||
`parent_row_index`, `parent_code`, `needs_review`, `is_confirmed`, `line_code_modified`; у значений `text`
|
||
(`source_text`), `needs_review`, `is_confirmed` и прежние поля; `issues[]` — записи `document_issue` с `line_ids`,
|
||
`column_id`, `column_index`, `field`, `codes`; `checks` — нарушения из `document_issue` с `codes` и `line_ids`;
|
||
`edition_lines[] {code, name}` — справочник строк для подсказки кода: коды словаря имён формы в редакции ответа по одному
|
||
(имя — первое), у формы без редакции — коды всех редакций по одному. `DocumentBrief` (`FileOut.documents[]`) и
|
||
`DocumentOut` SHALL отдавать `issue_counts {error, info}` — число записей `document_issue` документа по уровню. Порядок
|
||
`lines` — по возрастанию кода, как сейчас (`schemas/statements.py`), одинаковые коды — по `row_index`; строки
|
||
`unreadable` / `missing` — после ближайшей предыдущей по `row_index` строки, размещённой по коду, без такой — в начале;
|
||
`detail` — после родителя. Разделы и размещение итогов на экране («как на бумаге», Р-14) строит экран.
|
||
|
||
Форма 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 = "template"` ⇔ `row_index IS NULL` (создана правкой аналитика), иначе `"source"`. Строки источника, которых нет в справочнике, отдаются как есть.
|
||
Достроенные строки и значения MUST NOT записываться в БД при чтении и MUST NOT участвовать в проверках §7.
|
||
|
||
#### 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.failed` содержит соотношения с пустой целью 1700 (`1700 = 1300 + 1400 + 1500`, `expected = null`, `actual` —
|
||
сумма членов) и `1600 = 1700` (цель 1600 заполнена, 1700 пуста) — такие же, как без достройки; подтверждение формы
|
||
требует правки строки 1700 или акцепта проверок
|
||
|
||
#### 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`
|
||
|
||
#### Scenario: Документ, принятый по 1.x
|
||
- **WHEN** форма загружена до перехода сервиса (результат 1.2)
|
||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, `origin = source`, у значений `text =
|
||
string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает
|
||
|
||
### 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`».
|
||
|
||
**Расширения v2.0.** `PATCH /documents/{id}/statement` SHALL принимать помимо `header {inn, report_date, units}` и `values` (сумма):
|
||
- `header.organization_name` — непустая строка до 1000 символов: `organization_name_modified = true`,
|
||
`organization_name_original_value` не меняется, пересчитывается `counterparty_check`, аудит `before/after`;
|
||
- `header.report_date`, равный текущему, у поля из `needs_review_fields` — подтверждение даты: поле уходит из
|
||
`needs_review_fields`, снимается `REPORT_DATE_UNCERTAIN`, аудит; иначе запрос без изменений — 200 без эффектов, как
|
||
прежде;
|
||
- `values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения значения или снять
|
||
подтверждение;
|
||
- `columns[] {column_id, period_date}` — дата колонке (обязательная дата; `null` — 422 ошибка валидации; пересчитывает
|
||
`is_current` и период документа по правилу §9.5 п. 2); ручная дата ставит колонке `period_kind = absolute`, чтобы
|
||
следующая правка даты отчёта её не перезаписала; дата, равная текущей, у колонки с `COLUMN_DATE_UNCERTAIN` или
|
||
`COLUMN_WITHOUT_DATE` — подтверждение: предупреждение снимается, `period_kind = absolute`, аудит (иначе запрос без
|
||
изменений — 200 без эффектов); правка `header.report_date` (в том числе `null`) пересчитывает `period_date` колонок с
|
||
`period_kind = relative:*` общим кодом (`null` даёт `null`);
|
||
- `lines[] {line_id, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
|
||
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить код или снять подтверждение.
|
||
|
||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||
отметкой; `ok` → `ok` сменой кода; `ok` с неуверенным кодом — подтверждением. Обратного перехода `detail` →
|
||
`unreadable` / `missing` нет: ошибочная отметка исправляется назначением кода, исходное состояние — перезагрузкой файла.
|
||
`line_status` принимает только `"detail"`; `"ok"` — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только
|
||
`line_code`). Назначенный код: `line_code_confidence = 100`, `line_code_modified = true`, `needs_review` строки =
|
||
`false`. Подтверждённый код (`confirmed: true`): `is_confirmed = true`, `needs_review` строки = `false`,
|
||
`line_code_confidence` остаётся как пришло, `line_code_modified` не ставится; снятие подтверждения (`confirmed: false`)
|
||
возвращает `needs_review` по прежнему правилу (Р-34). `line_code_original_value` не меняется; после любого изменения статусов или кодов
|
||
`parent_row_index` **всех** строк `detail` документа пересчитывается как ближайшая строка `ok` выше по `row_index`
|
||
(требование «Пересчёт после правки»). Код — 4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с
|
||
существующим; незнакомый словарю — `info`. Строка с тем же кодом, **созданная правкой** (`row_index IS NULL`), с суммами —
|
||
409 `LINE_CODE_TAKEN` (сначала очистить её суммы или выбрать другой код); без сумм — удаляется (её `id` убирается из
|
||
`document_issue.line_ids`), её место занимает назначенная строка. Строки источника с тем же кодом не трогаются. В одном
|
||
элементе больше одного действия или ни одного — 422 `INVALID_LINE_PATCH`; строка не этого документа — 422
|
||
`UNKNOWN_LINE`; колонка не этого документа — 422 `UNKNOWN_COLUMN`. Ошибки элемента `lines[]` (`LINE_CODE_TAKEN`,
|
||
`LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`) SHALL нести в `detail` `line_id` и
|
||
`line_code` первого неверного элемента (`line_code` — код элемента или `null`). Правка суммы по `line_code` (для достроенной строки)
|
||
при наличии строки `ok` с этим кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления строк источника нет. Строки,
|
||
созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`, `line_code_original_value = null`. Аудит
|
||
`RECOGNIZED_DATA_EDIT` с `before/after` по коду, статусу, родителю, подтверждениям и наименованию организации.
|
||
Ограничения состояния — как прежде (`DOCUMENT_READONLY`, `FILE_SUPERSEDED`, `DOCUMENT_NOT_STATEMENT`).
|
||
|
||
Части запроса применяются в порядке `lines` → `values` → `columns` → `header`; вся адресация (422) проверяется до первой записи. Один `line_id` дважды в `lines` — 422 `INVALID_LINE_PATCH`; `UNKNOWN_CELL` по `line_code` — по кодам строк `ok` с учётом назначений `lines` этого запроса.
|
||
|
||
Ячейка, которой нет в БД, SHALL создаваться при правке:
|
||
- по `line_id` строки формы и `column_id` колонки формы, если у строки нет значения в этой колонке;
|
||
- по `line_code`, если строки с этим кодом в форме нет, а код есть в справочнике редакции документа для типа формы:
|
||
создаётся `financial_statement_line` (`line_code`, `line_name` из справочника, `line_code_original_value = NULL`, `row_index = NULL`, `source_ref = NULL`, `line_status = ok`,
|
||
`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`, данные не изменились
|
||
|
||
#### Scenario: Назначение кода строке-мусору (AURORA-1462)
|
||
- **WHEN** аналитик назначает строке `unreadable` «ИЧ,250» (`row_index = 2`) код 1250
|
||
- **THEN** строка `ok`, `line_code = 1250`, `line_code_confidence = 100`, `line_code_modified = true`,
|
||
`line_code_original_value` остаётся `null`, `row_index` остаётся 2 (`origin = source`); §7 и статус пересчитаны
|
||
|
||
#### Scenario: Отметка расшифровкой
|
||
- **WHEN** аналитик отмечает строку `missing` с суммой как расшифровку
|
||
- **THEN** `line_status = detail`, строка выходит из сумм и не блокирует подтверждение
|
||
|
||
#### Scenario: Код занят шаблонной строкой с суммами
|
||
- **WHEN** аналитик назначает строке `unreadable` код 1250, а строка 1250 создана правкой аналитика (`row_index = null`) и
|
||
имеет сумму
|
||
- **THEN** 409 `LINE_CODE_TAKEN`, ничего не изменено
|
||
|
||
#### Scenario: Код занят пустой шаблонной строкой
|
||
- **WHEN** аналитик назначает строке `unreadable` код 1250, а строка 1250 создана правкой и сумм не имеет
|
||
- **THEN** шаблонная строка удалена, её `id` убран из `document_issue.line_ids`; строка источника стала `ok` с кодом 1250
|
||
|
||
#### Scenario: Правка наименования организации
|
||
- **WHEN** у формы `NO_ORGANIZATION_NO_INN`, аналитик вводит `header.organization_name`
|
||
- **THEN** наименование сохранено, `organization_name_modified = true`, исходное — в `organization_name_original_value`,
|
||
`counterparty_check` пересчитан, предупреждение снято, аудит
|
||
|
||
#### Scenario: Подтверждение неуверенной даты отчёта
|
||
- **WHEN** `report_date` в `needs_review_fields`, аналитик отправляет PATCH с тем же `header.report_date`
|
||
- **THEN** `needs_review_fields` пуст, `REPORT_DATE_UNCERTAIN` снят, статус пересчитан, аудит
|
||
|
||
#### Scenario: Ручная дата относительной колонки
|
||
- **WHEN** колонке `relative:prev_year_end` аналитик ставит `period_date` 31.12.2024, затем правит дату отчёта
|
||
- **THEN** после первой правки `period_kind = absolute`; правка даты отчёта её дату не меняет
|
||
|
||
#### Scenario: Подтверждение неуверенной даты колонки
|
||
- **WHEN** у колонки `COLUMN_DATE_UNCERTAIN`, аналитик отправляет `columns: [{column_id, period_date}]` с той же датой
|
||
- **THEN** предупреждение снято, `period_kind = absolute`, статус пересчитан, аудит
|
||
|
||
#### Scenario: Статус ok без кода
|
||
- **WHEN** приходит `lines: [{line_id, line_status: "ok"}]`
|
||
- **THEN** 422 `LINE_CODE_REQUIRED`
|
||
|
||
### Requirement: Акцепт проверок формы
|
||
`POST /api/v1/documents/{document_id}/statement/checks/accept` (`DOCUMENT_EDIT`, policy) с `{comment}` (1–2000 символов)
|
||
SHALL выполнять акцепт (`statement-checks`), пересчитывать статус и писать `audit_event(RECOGNIZED_DATA_EDIT,
|
||
RECOGNITION)` с `before/after` полей акцепта; без непройденных жёстких проверок — 409 `NOTHING_TO_ACCEPT`; те же 409,
|
||
что у правки формы.
|
||
|
||
#### Scenario: Акцепт
|
||
- **WHEN** у формы нарушено 1600 = 1100 + 1200, аналитик акцептует с комментарием «округление в форме клиента»
|
||
- **THEN** `checks.accepted` заполнен, документ `RECOGNIZED`, если других причин `NEEDS_CHECK` нет
|
||
|
||
### Requirement: Комментарии к документу
|
||
`POST /api/v1/documents/{document_id}/comments` (`DOCUMENT_EDIT`, policy) с `{text}` (1–4000 символов после обрезки
|
||
пробелов) SHALL создавать `comment(object_type = 'document')` с автором и временем; `GET …/comments` (`DOCUMENT_VIEW`)
|
||
SHALL отдавать комментарии по `created_at`: `{id, text, author: {id, full_name}, created_at}`. Правки и удаления нет;
|
||
комментировать можно документ в любом статусе, в том числе файла `SUPERSEDED`.
|
||
|
||
#### Scenario: Комментарий
|
||
- **WHEN** аналитик добавляет комментарий «нет подписи руководителя» к подтверждённому документу
|
||
- **THEN** 201, комментарий в списке с ФИО автора, `comments_count` документа = 1
|
||
|
||
### Requirement: Исходный файл
|
||
`GET /api/v1/files/{file_id}/content` (`DOCUMENT_VIEW`, policy) SHALL отдавать содержимое исходного объекта файла из S3
|
||
потоком, без чтения целиком в память: `Content-Type` по `file_type`, `Content-Disposition` (`attachment` по умолчанию,
|
||
`inline` при `?disposition=inline`) с ASCII-именем и `filename*` RFC 5987 исходного имени; работает и для файла
|
||
`SUPERSEDED`. Объект не найден в S3 — 404 `FILE_CONTENT_NOT_FOUND`; чужой файл — 404.
|
||
|
||
#### Scenario: PDF для просмотра
|
||
- **WHEN** запрошено содержимое загруженного `aep_6.pdf` с `disposition=inline`
|
||
- **THEN** 200, `Content-Type: application/pdf`, `Content-Disposition: inline; filename="file.pdf"; filename*=UTF-8''aep_6.pdf`, тело байт в байт равно загруженному
|
||
|
||
#### Scenario: Кириллица в имени
|
||
- **WHEN** запрошен файл `ОСВ_66 за 2024.xls`
|
||
- **THEN** `filename*` содержит percent-кодированное исходное имя
|
||
|
||
### Requirement: Рабочий стол консолидации для ЮЛ
|
||
`GET /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (`DOCUMENT_VIEW`; `limit`,
|
||
`offset`, `include_superseded=false` по умолчанию; §9.5 п. 11 v5.18) SHALL отвечать 404 `NOT_FOUND` на консолидацию вне
|
||
видимости или несуществующую («консолидация не найдена») и на ЮЛ вне видимости, иначе возвращать `Page[FileOut]` —
|
||
объединение файлов ЮЛ, у которых `stored_file.consolidation_id = consolidation_id`, и файлов ЮЛ, у которых есть документ
|
||
с записью `consolidation_document` к любой версии этой консолидации; порядок и карточка — как у списка файлов ЮЛ;
|
||
`SUPERSEDED` — только при `include_superseded=true` (в обоих множествах). Принадлежность ЮЛ к ГК консолидации не
|
||
проверяется: чужому ЮЛ соответствует пустой список.
|
||
|
||
#### Scenario: Стол показывает только файлы консоли
|
||
- **WHEN** у ЮЛ три файла: загружен в консолидацию 2025 Q1, загружен в консолидацию 2025 Q2, без контекста
|
||
- **THEN** стол 2025 Q1 содержит только первый; стол 2025 Q2 — только второй; `GET /legal-entities/{id}/files` — все три
|
||
|
||
#### Scenario: Подхваченный файл виден через привязку
|
||
- **WHEN** файл с `CONFIRMED` ОСВ за 2025 Q1 загружен без контекста и его документ привязан к версии консолидации 2025 Q2
|
||
(подхват при создании версии)
|
||
- **THEN** файл есть в столе 2025 Q2, хотя `consolidation_id IS NULL`
|
||
|
||
#### Scenario: Заменённый файл скрыт по умолчанию
|
||
- **WHEN** файл консолидации заменён (`SUPERSEDED`) новым файлом той же консолидации
|
||
- **THEN** по умолчанию в столе только новый; с `include_superseded=true` — оба
|
||
|
||
#### Scenario: Чужая консолидация
|
||
- **WHEN** `ka_rf` запрашивает стол консолидации ГК другого филиала и несуществующий id
|
||
- **THEN** оба ответа 404 с одинаковым телом
|
||
|
||
### Requirement: Предупреждения и нарушения документа — `document_issue`
|
||
Ядро SHALL хранить предупреждения и нарушения документа в `document_issue`: вид (`warning` | `relation` | `soft`), код,
|
||
уровень (`error` | `info`), сообщение, список `line_id`, `column_id` (колонка формы) или `column_index` (столбец
|
||
предупреждения, которого нет среди колонок формы), поле шапки, источник (`service` | `core`); для нарушений — `codes`
|
||
(коды цели и членов), текст соотношения, `expected`, `actual`. Нарушение из `checks.relations_failed` — `kind =
|
||
relation`, `code = RELATION_FAILED`, `severity = error`; из `soft_relations_failed` — `kind = soft`, `code =
|
||
RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` кодов `codes` в документе. При загрузке пишутся
|
||
предупреждения и `checks` результата (источник `service`) и проверки ядра уровня `info` (источник `core`):
|
||
`CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён редакции документа) и `CODE_NOT_IN_EDITION` (код не из перечня
|
||
редакции документа и его варианта — по копии перечня `arm_statements`) — только для четырёхзначных кодов, одна запись на код документа (Р-40), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`.
|
||
**Редакция документа** для этих проверок — `edition` точки входа пакета для пересчёта (`statement-normalization`):
|
||
редакция с лучшей подпоследовательностью кодов строк `ok` документа, при равенстве — по эффективной дате; так бланк 66н,
|
||
сданный в 2025 году, проверяется по `66n`, а не по дате. Поле `edition` ответа `GET …/statement` и выбор шаблонных
|
||
строк по-прежнему — по дате периода (§7 «Полный состав формы»). `document_issue` ведётся у **любого** документа файла: у
|
||
ОСВ — предупреждения `osvDocument.warnings` результата `2.0` (вид `warning`, источник `service`, `OSV_*`, `info`). При перезагрузке файла записи документа
|
||
удаляются вместе с ним. При пересчёте после правки записи `relation` и `soft` заменяются целиком независимо от источника;
|
||
записи `warning` источника `service` сохраняются, кроме снимаемых правкой: правка `header.report_date` (в том числе
|
||
подтверждение тем же значением) снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`, `REPORT_DATE_UNCERTAIN`; правка
|
||
`header.inn` или `header.organization_name` — `NO_ORGANIZATION_NO_INN`; правка `columns[].period_date` —
|
||
`COLUMN_WITHOUT_DATE` и `COLUMN_DATE_UNCERTAIN` этой колонки; прочие `error` сервиса снимаются только перезагрузкой
|
||
файла. При удалении шаблонной строки её `id` SHALL убираться из `line_ids` всех записей документа. `GET …/statement` и
|
||
`POST …/confirm` SHALL читать нарушения §7 только из `document_issue`.
|
||
|
||
#### Scenario: Дата колонке
|
||
- **WHEN** у колонки предупреждение `COLUMN_WITHOUT_DATE`, аналитик ставит ей `period_date` через PATCH
|
||
- **THEN** предупреждение удаляется, статус пересчитывается
|
||
|
||
#### Scenario: Неснимаемое предупреждение
|
||
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
|
||
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
|
||
|
||
#### Scenario: Бланк 66н в 2025 году без ложного info
|
||
- **WHEN** форма Межениновской за 3 кв. 2025 на бланке 66н с кодом 1120 (в перечне `fsbu4` его нет)
|
||
- **THEN** редакция документа для проверок ядра — `66n` (подпоследовательность длиннее), `CODE_NOT_IN_EDITION` у 1120 нет
|
||
|
||
#### Scenario: Пропущенная колонка
|
||
- **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет
|
||
- **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа
|
||
|
||
### Requirement: Пересчёт после правки
|
||
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review`, `form_variant`, родителей строк `detail`
|
||
и статус **точкой входа пакета `arm_statements` для пересчёта** по сохранённым данным (`statement-normalization`;
|
||
конвенция знака — по `source_text`); временного пересчёта ядра MUST NOT быть. Правила: `is_modified` или `is_confirmed`
|
||
значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed` строки ⇒ `needs_review` строки = `false`;
|
||
значения строк `detail` — `needs_review = false` (Р-23), у строки, выведенной из `detail` назначением кода,
|
||
`needs_review` её значений считается по общему правилу; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в
|
||
голосование знака не входят. Результат пишется: `needs_review` строк и значений, `form_variant`, `parent_row_index` всех
|
||
строк `detail` (ближайшая строка `ok` выше по `row_index`; нет такой — `null`, строка остаётся `detail`), записи
|
||
`document_issue` видов `relation` / `soft` (заменяются целиком независимо от источника), статус. Относительные даты
|
||
колонок (`period_kind`, `statement-normalization`) пересчитываются от даты отчёта после правки `header.report_date` и
|
||
`columns[].period_date`. Отличие `statements_version` задания от версии копии ядра — `document_issue`
|
||
`STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Записи `core`
|
||
`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION` (только четырёхзначные коды), `NAME_MISMATCH` и
|
||
`STATEMENTS_VERSION_MISMATCH` пересчитываются заменой; `NAME_MISMATCH` не ставится строке с назначенным аналитиком кодом;
|
||
`MAPPING_WARNING`, `RESULT_STATUS_INCONSISTENT`, `LEGACY_WARNING` сохраняются. Один PATCH — один пересчёт и одна запись
|
||
аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
|
||
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки
|
||
тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47).
|
||
|
||
#### Scenario: Подтверждение ячейки без правки
|
||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||
- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит; акцепт
|
||
проверок, если был, сохраняется
|
||
|
||
#### Scenario: Снятие подтверждения
|
||
- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением
|
||
- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит
|
||
|
||
#### Scenario: Родитель уходит в расшифровки
|
||
- **WHEN** у строки 1150 две расшифровки (`detail`, `parent_row_index` = 1150), аналитик отмечает саму 1150 расшифровкой
|
||
- **THEN** после пересчёта родитель всех трёх строк `detail` — ближайшая строка `ok` выше 1150; строк над ними нет — `null`
|
||
(на экране — в начале раздела)
|
||
|
||
#### Scenario: Правка исправляет нарушение
|
||
- **WHEN** форма принята с `RELATION_FAILED` по `1200 = 1210 + … + 1260`, аналитик правит сумму 1230 так, что
|
||
соотношение сходится
|
||
- **THEN** запись `relation` по 1200 заменена (её больше нет), статус пересчитан точкой входа пакета
|
||
|
||
#### Scenario: После PATCH остаётся RELATION_FAILED
|
||
- **WHEN** аналитик правит сумму, но соотношение всё ещё не сходится
|
||
- **THEN** запись `RELATION_FAILED` пересоздана с новыми `expected` / `actual`; `POST …/confirm` без акцепта — 409
|
||
`CHECKS_NOT_PASSED`
|
||
|
||
### Requirement: Словари ядра и достройка шаблонных строк
|
||
Копия словаря §7 ядра (`api/rules/statement_checks.yaml`) SHALL быть удалена: проверки ядро считает копией пакета
|
||
`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименования по
|
||
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
|
||
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
|
||
состав редакций — из копии перечня кодов пакета (`code_orders.editions`); код словаря имён, которого нет в перечне своей
|
||
редакции, — ошибка проверки (тест согласованности, `sync-statements --check`). В редакции `fsbu4` словарь имён SHALL быть
|
||
дополнен вторыми именами законных повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410
|
||
«Налог при упрощенной системе налогообложения» (Р-20) и кодами печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные
|
||
поисковые активы», 1140 «Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по
|
||
`fixtures/ocr` и шаблонам банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают
|
||
`NAME_MISMATCH`. Шаблонные строки достраиваются по словарю имён (одна на код, первое имя); код, у которого есть хотя бы
|
||
одна строка `ok` из файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой
|
||
молча.
|
||
|
||
#### Scenario: Код словаря вне перечня
|
||
- **WHEN** в `statement_lines.yaml` у редакции есть код, которого нет в перечне этой редакции копии пакета
|
||
- **THEN** проверка согласованности словаря с перечнем падает
|
||
|
||
#### Scenario: Код есть только у строки unreadable
|
||
- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250»
|
||
- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом
|
||
|
||
#### Scenario: Два имени у кода
|
||
- **WHEN** в редакции `fsbu4` словаря имён код 1520 записан дважды — «Кредиторская задолженность» и «Обязательство по
|
||
аренде»
|
||
- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
|
||
|
||
#### Scenario: Гудвил с заголовком раздела в имени
|
||
- **WHEN** строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил»
|
||
- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет
|
||
|
||
### Requirement: Предупреждения файла и ОСВ в API, статус ОСВ при приёме 2.0
|
||
`recognition_job.metrics.warnings` SHALL хранить предупреждения уровня файла структурами `{code, severity, message,
|
||
locator?}` (`fileWarning` контракта): у `2.0` — как пришли, у `1.x` — строки, обёрнутые в `LEGACY_WARNING` `info`.
|
||
`JobBrief.warnings` карточки файла (`FileOut.job`) SHALL отдавать список этих структур, а не строк; строки заданий,
|
||
сохранённых до выкладки ядра v5.19, при чтении конвертируются в `{code: "LEGACY_WARNING", severity: "info", message:
|
||
<строка>, locator: null}` (данные в БД не переписываются). `JobBrief.mapping_warnings` (предупреждения маппинга ядра)
|
||
остаётся списком строк. Это несовместимое изменение типа поля для фронта — фиксируется в гайде фронта.
|
||
|
||
Предупреждения листа ОСВ результата `2.0` (`osvDocument.warnings`, коды `OSV_*`, `info`) SHALL храниться в
|
||
`document_issue` документа ОСВ (вид `warning`, источник `service`) и отдаваться в `DocumentOut.issues[]` (те же поля,
|
||
что `issues` формы) вместе с предупреждениями маппинга ядра этого листа; `DocumentOut.warnings` (строки маппинга файла)
|
||
сохраняется на окно перехода. `osvDocument.status` SHALL использоваться: при приёме `2.0` документ ОСВ `RECOGNIZED` ⇔
|
||
`osvDocument.status = done` ∧ `checks` листа без нарушений ∧ в `metrics.warnings` файла нет предупреждений уровня
|
||
`error` ∧ `counterparty_check ≠ MISMATCH` ∧ нет предупреждений маппинга ядра по этому листу (повтор счёта в файле, строки
|
||
без родителя, нарастающая по данным Q1); статус конверта в условие не входит (как Р-19 для форм). При приёме `1.x` — как
|
||
прежде (§9.3).
|
||
|
||
#### Scenario: Карточка файла задания до выкладки
|
||
- **WHEN** у задания, сохранённого до v5.19, `metrics.warnings = ["sheet=ОСВ 60 строки без родителя"]`
|
||
- **THEN** `JobBrief.warnings = [{code: "LEGACY_WARNING", severity: "info", message: "sheet=ОСВ 60 строки без родителя",
|
||
locator: null}]`
|
||
|
||
#### Scenario: Потерянный json в карточке файла
|
||
- **WHEN** результат `2.0` scan-gateway несёт `metrics.warnings` с `JSON_FAILED`
|
||
- **THEN** `JobBrief.warnings` содержит `{code: "JSON_FAILED", severity: "error", message, locator}`; предупреждение не
|
||
теряется
|
||
|
||
#### Scenario: ОСВ в файле с ОФР, требующим проверки
|
||
- **WHEN** книга из ОСВ (`status = done`, контрольные суммы сошлись) и ОФР (`needs_check`), конверт `needs_check`,
|
||
предупреждений файла `error` нет
|
||
- **THEN** ОСВ `RECOGNIZED` и автоподтверждается; её предупреждения `OSV_*` — в `DocumentOut.issues`
|
||
|
||
### Requirement: `CHECKS_NOT_PASSED` из `document_issue`
|
||
Нарушения для 409 `CHECKS_NOT_PASSED` при `POST /documents/{id}/confirm` формы ББ/ОФР (порядок проверок и условия —
|
||
«Подтверждение документа») SHALL читаться **только из записей `document_issue` вида `relation`** без акцепта
|
||
(акцептованная форма нарушенной не считается); пересчёта §7 в запросе подтверждения нет. `detail.failures[]` —
|
||
`{column_index, relation, expected, actual}`: `expected` и `actual` — строки Decimal или `null` (пустая цель при
|
||
непустых членах — `expected = null`); `column_index` — индекс колонки записи, а если колонку определить нельзя
|
||
(у записи нет ни `column_id`, ни `column_index`) — `null`.
|
||
|
||
#### Scenario: Жёсткое нарушение из document_issue
|
||
- **WHEN** у формы есть запись `relation` без акцепта и нет строк `unreadable` / `missing` со значениями
|
||
- **THEN** 409 `CHECKS_NOT_PASSED`, `detail.failures[]` — по записи на нарушение
|
||
|
||
#### Scenario: Нарушение с пустой целью
|
||
- **WHEN** у формы нет строки 1700 (потеряна источником), 1300 + 1400 + 1500 заполнены
|
||
- **THEN** 409 `CHECKS_NOT_PASSED`; в `detail.failures[]` нарушение `1700 = 1300 + 1400 + 1500` с `expected = null`
|
||
|
||
### Requirement: Блок `checks` ответа формы из `document_issue`
|
||
Блок `checks` ответа `GET …/statement` SHALL строиться из записей `document_issue` видов `relation` (`failed`) и `soft`
|
||
(`soft_failed`) с `codes`, `expected`, `actual`, `line_ids`; собственного расчёта §7 при чтении нет. Элемент нарушения —
|
||
`{column_index, relation, expected, actual, codes, line_ids}`: `expected` и `actual` — `Decimal` или `null`
|
||
(`expected = null` — цель соотношения пуста при непустых членах); `column_index` — индекс колонки формы по `column_id`
|
||
записи, иначе `column_index` записи, иначе `-1` (колонки нет: межформенная проверка `cross_form` всегда `-1`).
|
||
Фактическое различие с `detail.failures[]` 409 (там отсутствующий индекс — `null`, а не `-1`) сохраняется как есть
|
||
(P3-60). Межформенная проверка `cross_form` считается по агрегату формы по коду.
|
||
|
||
#### Scenario: checks из document_issue
|
||
- **WHEN** у формы две записи `relation` и одна `soft`
|
||
- **THEN** `checks.failed` содержит две записи, `checks.soft_failed` — одну; расчёт §7 при запросе не выполняется
|
||
|
||
#### Scenario: Нарушение с пустой целью в ответе
|
||
- **WHEN** у формы запись `relation` по `1700 = 1300 + 1400 + 1500` с `expected = NULL`
|
||
- **THEN** элемент `checks.failed` имеет `expected = null`, `actual` — сумма членов
|
||
|
||
### Requirement: Уточнение шапки ОСВ
|
||
`PATCH /api/v1/documents/{id}/osv-header` SHALL требовать `DOCUMENT_EDIT` и видимость ЮЛ, принимать частичный набор
|
||
`period_year`, `period_quarter`, `is_cumulative`, `osv_account_code` и возвращать карточку документа с
|
||
`is_modified`, `original_values`, `modified_by`, `modified_at`. Отсутствующие поля SHALL сохраняться.
|
||
Правила — §9.5 п.12. Чужой и несуществующий документ SHALL возвращать одинаковый 404.
|
||
|
||
#### Scenario: Изменение только счёта
|
||
- **WHEN** аналитик меняет счёт ОСВ РБ с 60 на 62, не передавая период
|
||
- **THEN** меняется только счёт; даже неопределённый квартал остаётся прежним
|
||
|
||
#### Scenario: Нет доступа
|
||
- **WHEN** пользователь без DOCUMENT_EDIT правит шапку либо документ находится вне видимости ЮЛ
|
||
- **THEN** без права возвращается 403, при наличии права и отсутствии видимости — 404; данные не меняются
|
||
|
||
### Requirement: Проверки правки шапки ОСВ
|
||
После проверки типов тела запроса Pydantic PATCH SHALL проверять последовательно: тип OSV (иначе 409 DOCUMENT_NOT_OSV), файл не SUPERSEDED (иначе 409
|
||
FILE_SUPERSEDED), корректность периода (422 INVALID_PERIOD), счёта (422 INVALID_ACCOUNT_CODE), отсутствие
|
||
другой ОСВ с тем же нормализованным счётом в файле (409 DUPLICATE_ACCOUNT с detail.document_id),
|
||
редактируемость привязанных версий (409 VERSION_NOT_EDITABLE с detail.version_ids). Все проверки SHALL
|
||
предшествовать записи.
|
||
|
||
#### Scenario: Частичная правка периода
|
||
- **WHEN** передано хотя бы одно поле периода
|
||
- **THEN** итоговые год, квартал и is_cumulative обязательны: год 1900–2099, квартал Q1–Q4, признак boolean;
|
||
для Q1 признак true; иначе 422 INVALID_PERIOD без изменений
|
||
|
||
#### Scenario: Нормализация и коллизия
|
||
- **WHEN** передано 60.1, а у другой ОСВ этого файла уже счёт 60.01
|
||
- **THEN** нормализация даёт 60.01, возвращается 409 DUPLICATE_ACCOUNT с id другого документа
|
||
|
||
#### Scenario: Пустой счёт
|
||
- **WHEN** передан пустой счёт
|
||
- **THEN** 422 INVALID_ACCOUNT_CODE, прежний счёт сохранён
|
||
|
||
#### Scenario: Порядок конфликтов
|
||
- **WHEN** у заменённого файла документ BALANCE и некорректный период в теле
|
||
- **THEN** возвращается DOCUMENT_NOT_OSV; у OSV того же файла — FILE_SUPERSEDED
|
||
|
||
### Requirement: Исходная шапка и признак исправления
|
||
При первой фактической правке ядро SHALL сохранить прежние значения всех четырёх полей в document.original_values;
|
||
при последующих правках снимок MUST NOT меняться. Новые значения SHALL записываться в ту же строку document,
|
||
is_modified SHALL отражать отличие от снимка, modified_by/at — последнюю правку. document.id, file_ordinal,
|
||
строки osv_entry, файл и JSON результата в S3 MUST NOT изменяться.
|
||
|
||
#### Scenario: Две правки
|
||
- **WHEN** счёт ОСВ РБ меняется 60 → 62 → 76
|
||
- **THEN** текущий счёт 76, original_values.osv_account_code = 60, в original_values также исходный период;
|
||
is_modified = true, id и количество документов и строк ОСВ прежние
|
||
|
||
#### Scenario: Возврат к исходному
|
||
- **WHEN** все четыре поля возвращены к значениям original_values
|
||
- **THEN** is_modified = false, original_values остаётся заполненным, последняя правка атрибутирована
|
||
|
||
#### Scenario: Запрос без изменений
|
||
- **WHEN** нормализованная итоговая шапка совпадает с текущей
|
||
- **THEN** 200 с карточкой без аудита, создания original_values, сброса подтверждения и изменения modified_by/at
|
||
|
||
### Requirement: Снятие подтверждения ОСВ после уточнения
|
||
Фактическая правка CONFIRMED ОСВ SHALL установить NEEDS_CHECK, очистить confirmed_by/at и снять привязки
|
||
consolidation_document из всех версий «в работе», сохранив привязки CANCELLED и статусы версий.
|
||
Привязка к иной версии SHALL запретить правку. Неподтверждённый документ SHALL сохранить статус.
|
||
consolidation_lock MUST NOT требоваться; автоподтверждение после PATCH MUST NOT запускаться.
|
||
|
||
#### Scenario: Исправление автоподтверждённой ОСВ
|
||
- **WHEN** ОСВ РБ привязана к версии DATA_CORRECTION, аналитик исправляет период
|
||
- **THEN** NEEDS_CHECK, confirmed_by/at = null, привязка снята, версия остаётся DATA_CORRECTION;
|
||
повторное ручное подтверждение привязывает документ по новому периоду и счёту
|
||
|
||
#### Scenario: Завершённая версия
|
||
- **WHEN** документ привязан к VERIFIED, даже если одновременно привязан к версии «в работе»
|
||
- **THEN** 409 VERSION_NOT_EDITABLE со списком запрещающих version_ids, ни одна привязка и шапка не меняются
|
||
|
||
#### Scenario: Отменённая версия
|
||
- **WHEN** подтверждённый документ связан только с CANCELLED и версиями «в работе»
|
||
- **THEN** правка разрешена без consolidation_lock, привязки CANCELLED остаются, остальные снимаются
|
||
|
||
### Requirement: Производные данные после уточнения ОСВ
|
||
Подходящесть версиям, чек-лист и колонка «ОСВ» SHALL отражать новую шапку при следующем чтении;
|
||
period_check, counterparty_check и строки osv_entry SHALL сохраняться. Правка MUST NOT запускать автозамену
|
||
другого файла; замещение привязки с тем же ключом происходит при повторном подтверждении по §3 п.1.
|
||
Перезагрузка файла MUST NOT переносить уточнения старых документов. Флаг CONSOLIDATION_PERIOD_FROM_VERSION сохраняется.
|
||
|
||
#### Scenario: Смена счёта шапки
|
||
- **WHEN** счёт ОСВ РБ исправлен с 60 на 62
|
||
- **THEN** колонка «ОСВ» и подходящие слоты чек-листа учитывают 62; account_code/account_id/account_root
|
||
строк ОСВ, period_check и counterparty_check не изменились, другой файл ОСВ 62 не заменён
|
||
|
||
#### Scenario: Перезагрузка
|
||
- **WHEN** новый загруженный файл распознан с исходным счётом 60
|
||
- **THEN** новые документы созданы из нового результата, исправление 62 предыдущего документа не переносится
|
||
|
||
### Requirement: Строгие типы шапки ОСВ
|
||
PATCH SHALL принимать period_year как integer, period_quarter и osv_account_code как string,
|
||
is_cumulative как boolean, без приведения типов. Отсутствующие поля не меняются; null SHALL передаваться
|
||
в доменную проверку явно. Неверные типы SHALL отклоняться стандартным 422 валидации тела до доменных
|
||
проверок. Границы года, допустимые кварталы и совместимость полей SHALL проверяться доменом.
|
||
|
||
#### Scenario: Неверный тип года
|
||
- **WHEN** передан period_year как строка, дробное число, boolean, массив или объект
|
||
- **THEN** стандартный 422 с detail-массивом ошибок валидации, доменная обработка не запускается
|
||
|
||
#### Scenario: Год вне диапазона
|
||
- **WHEN** у доступного незаменённого документа OSV передан period_year = 2100
|
||
- **THEN** тип integer принят, домен возвращает 422 INVALID_PERIOD
|
||
|
||
#### Scenario: Неизвестный квартал
|
||
- **WHEN** у доступного незаменённого документа OSV передан period_quarter = "Q0"
|
||
- **THEN** тип string принят, домен возвращает 422 INVALID_PERIOD
|