plan(#116): план 3 — ядро на дополненном API пакета, поля API для фронта, решения Р-34 и Р-35
Пакет: engine.edition (choose_edition) и engine.contract_document_status (status_input_from_contract) вместо сборки StoredForm ради редакции и status_input_from_v2; FailedRelation.expected/actual — Decimal | None; O-8 закрыт. API для плана 6: LineOut.line_code_confidence, StatementHeaderOut.okei, header.source.confidence — строка Decimal, StatementOut.edition_lines, DocumentBrief.issue_counts, line_id/line_code в detail ошибок элемента lines[]; P3-19…P3-24; спека arm-core и гайд §5.6.1 дополнены. Р-34: подтверждение кода не меняет уверенность — буква спеки arm-core, §9.5 п. 9 и гайда приведена; O-2 закрыт. Р-35: очистка отказывает только при верифицированной версии — P3-12, тесты A4, спека «Очистка форм» п. 2 и сценарий; O-5 закрыт. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
c385b7ec42
commit
8a7711da5a
@ -892,9 +892,10 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||||
отметкой; `ok` → `ok` сменой кода. Обратного перехода `detail` → `unreadable` / `missing` нет: ошибочная отметка
|
||||
исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` в PATCH принимает только
|
||||
`"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный или
|
||||
подтверждённый код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`;
|
||||
`line_code_original_value` не меняется (код из источника; у строк без кода — `null`); после правки родитель **всех**
|
||||
`"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный
|
||||
код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`; подтверждённый код —
|
||||
флаг `is_confirmed`, `needs_review` строки = `false`, `line_code_confidence` и `line_code_modified` не меняются,
|
||||
снятие подтверждения возвращает `needs_review` по прежнему правилу (Р-34); `line_code_original_value` не меняется (код из источника; у строк без кода — `null`); после правки родитель **всех**
|
||||
строк `detail` пересчитывается как ближайшая строка `ok` выше по `row_index` (нет такой — `null`). Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
|
||||
совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL`
|
||||
(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она
|
||||
|
||||
@ -426,14 +426,17 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
- `header.needs_review_fields[]` — поля шапки, которые держат документ в «Требует проверки» (сейчас бывает только
|
||||
`report_date`; подсветить поле). Сомнения в ИНН и организации — не здесь, а в `issues` с `field` (`INN_UNCERTAIN`,
|
||||
`ORGANIZATION_UNCERTAIN`, уровень `info`). `header.source` — `{<поле>: {text, confidence}}`: как поле распознано
|
||||
(сырой текст и уверенность) — в подсказку у поля; `header.organization_name_modified` — наименование правилось.
|
||||
(сырой текст и уверенность; `confidence` — строка Decimal `"61.54"` или `null`, как прочие уверенности) — в
|
||||
подсказку у поля; `header.organization_name_modified` — наименование правилось; `header.okei` — код ОКЕИ (только
|
||||
чтение).
|
||||
- `columns[].period_kind` — `absolute` | `relative:current` | `relative:prev_year_end` | `relative:prev_prev_year_end` |
|
||||
`relative:prev_period` | `none`; `columns[].label_confidence`. Колонка с `period_kind = none` и `period_date = null` —
|
||||
нужна дата от аналитика (ввод даты у колонки).
|
||||
- `lines[]` — **все строки источника**, а не только строки с кодом: `row_index` (порядок в файле; `null` — и только
|
||||
тогда — у строк, созданных правкой, и у достроенных шаблонных; `origin = template` ⇔ `row_index = null`), `source_ref`, `line_status` (`ok` | `unreadable` | `missing` | `detail`), `line_code` (`null` у
|
||||
статусов кроме `ok`), `line_code_text` (сырой текст ячейки кода — показывать серым у `unreadable` / `missing`),
|
||||
`line_name` (наименование **из файла**), `reference_name` (справочное имя по коду или `null` — его показывать основным,
|
||||
`line_code_confidence` (строка Decimal или `null`; подтверждение кода её не меняет), `line_name` (наименование **из
|
||||
файла**), `reference_name` (справочное имя по коду или `null` — его показывать основным,
|
||||
файловое рядом, если отличается), `parent_row_index` (родитель расшифровки `detail`), `needs_review` (подсветить строку),
|
||||
`is_confirmed`, `line_code_modified`. **Один код может стоять у нескольких строк** (повтор кода законен): не строить
|
||||
словарь «код → строка», ключ строки — `id` (у достроенных шаблонных — `line_code`, их у кода с реальной строкой `ok` нет).
|
||||
@ -449,6 +452,8 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
Нарушения §7 приходят и здесь (`kind = relation` / `soft`, `code = RELATION_FAILED` / `RELATION_SOFT`) — у любой формы,
|
||||
в том числе загруженной до перехода.
|
||||
- `checks.failed[]` / `soft_failed[]` — у нарушения ещё `codes[]` и `line_ids[]` (все строки кодов соотношения).
|
||||
- `edition_lines[] {code, name}` — справочник строк формы для подсказки кода при назначении: коды редакции формы по
|
||||
одному (имя — первое из словаря), у формы без даты (редакции нет) — коды всех редакций по одному.
|
||||
- Порядок `lines` — по возрастанию кода, как сейчас (одинаковые коды — по `row_index`); `unreadable` / `missing` — после
|
||||
ближайшей предыдущей по `row_index` строки с кодом (без такой — в начале), `detail` — сразу после родителя. Разделы и
|
||||
размещение итогов «как на бумаге» строит экран, как и раньше (`formLayout`).
|
||||
@ -462,12 +467,14 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа, `period_kind = absolute` (правка даты отчёта её больше не пересчитывает); снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN`; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) | 422 `UNKNOWN_COLUMN`; `period_date: null` — 422 массивом |
|
||||
| назначить / сменить код | `lines: [{line_id, line_code}]` | `unreadable` / `missing` / `detail` / `ok` → `ok` с этим кодом, `line_code_confidence = 100` | 422 `INVALID_LINE_CODE` (не 4–5 цифр), 409 `LINE_CODE_TAKEN` (код занят строкой, созданной правкой, с суммами — сначала очистить её) |
|
||||
| отметить расшифровкой | `lines: [{line_id, line_status: "detail"}]` | строка выходит из сумм и не блокирует подтверждение; обратно — только назначением кода | 422 `LINE_CODE_REQUIRED` (`line_status: "ok"` — в `ok` переводит только `line_code`) |
|
||||
| подтвердить код / снять подтверждение | `lines: [{line_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review` строки = `false` (при `false` — пересчитывается) | — |
|
||||
| подтвердить код / снять подтверждение | `lines: [{line_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review` строки = `false` (при `false` — пересчитывается); `line_code_confidence` и `line_code_modified` не меняются — в подсказке остаётся исходная уверенность | — |
|
||||
| наименование организации | `header: {organization_name}` (непустая строка) | `organization_name_modified`, `counterparty_check`; снимает `NO_ORGANIZATION_NO_INN` | 422 массивом |
|
||||
| подтвердить дату отчёта | `header: {report_date}` тем же значением, если `report_date` в `needs_review_fields` | поле уходит из `needs_review_fields`, снимается `REPORT_DATE_UNCERTAIN` | — |
|
||||
|
||||
Общие ошибки `lines`: 422 `UNKNOWN_LINE` (строка не этого документа), 422 `INVALID_LINE_PATCH` (в элементе нет действия или
|
||||
их несколько). Правка суммы по `line_code` (достроенная строка) для кода, у которого уже есть строка `ok`, — 422
|
||||
их несколько). У ошибок элемента `lines[]` (`LINE_CODE_TAKEN`, `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`,
|
||||
`INVALID_LINE_PATCH`, `UNKNOWN_LINE`) в `detail` есть `line_id` и `line_code` первого неверного элемента — показывать
|
||||
ошибку у поля этой строки. Правка суммы по `line_code` (достроенная строка) для кода, у которого уже есть строка `ok`, — 422
|
||||
`UNKNOWN_CELL`: передайте `line_id`. Правка `header.report_date` пересчитывает даты колонок с `period_kind = relative:*`.
|
||||
Удалять строки нельзя — для этого «Сделать расшифровкой». Акцепт проверок сбрасывает правка данных (сумма, код, статус
|
||||
строки, дата колонки, поле шапки), подтверждения (`confirmed`) его не сбрасывают. Ответ — `StatementOut` после пересчёта.
|
||||
@ -487,7 +494,9 @@ line_code_text}` — строки `unreadable` / `missing`, у которых в
|
||||
**Карточка файла и документа (несовместимое изменение).** `FileOut.job.warnings` — список объектов `{code, severity:
|
||||
"error" | "info", message, locator}` вместо строк (у заданий до выкладки ядро отдаёт строки, обёрнутые в `{code:
|
||||
"LEGACY_WARNING", severity: "info", message, locator: null}`); показывать `message` и `locator`, `error` отличать от
|
||||
`info`. `FileOut.job.mapping_warnings` — по-прежнему строки. `DocumentOut.issues[]` — те же записи, что `issues` формы,
|
||||
`info`. `FileOut.job.mapping_warnings` — по-прежнему строки. `FileOut.documents[].issue_counts` и
|
||||
`DocumentOut.issue_counts` — `{error, info}`: сколько записей `issues` у документа (для значка в панели файлов без
|
||||
запроса документа). `DocumentOut.issues[]` — те же записи, что `issues` формы,
|
||||
у любого документа, в том числе ОСВ (предупреждения листа `OSV_*`); `DocumentOut.warnings` (строки маппинга) остаётся на
|
||||
окно перехода.
|
||||
|
||||
|
||||
@ -99,9 +99,10 @@ cd $WS/.wt/normalization-core-a/arm-core && codegraph init && make venv
|
||||
- Порядок блокировок: задание → файл → документы → advisory ГК → версии (по `id`); правка формы — «файл → документ», шапка
|
||||
перечитывается под блокировкой с `refresh=True`.
|
||||
- Ошибки — `DomainError`: 404 `NOT_FOUND`, 409 `ConflictError`, 422 `DomainValidationError`, тело `{"detail": {"code",
|
||||
"message", …}}`; 422 валидации тела pydantic — массив. Коды этого плана: 409 `LINE_CODE_TAKEN`, `LINES_UNRESOLVED`
|
||||
(`lines[] {line_id, row_index, line_code_text}`), `DUPLICATE_FORM` (`document_id`), `CRM_ID_EXISTS`; 422
|
||||
`LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`, `UNKNOWN_CELL`. Каталог
|
||||
"message", …}}`; 422 валидации тела pydantic — массив. Коды этого плана: 409 `LINE_CODE_TAKEN` (`line_id`,
|
||||
`line_code`), `LINES_UNRESOLVED` (`lines[] {line_id, row_index, line_code_text}`), `DUPLICATE_FORM` (`document_id`),
|
||||
`CRM_ID_EXISTS`; 422 `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE` (все четыре — с
|
||||
`line_id`, `line_code` элемента `lines[]`, P3-24), `UNKNOWN_COLUMN`, `UNKNOWN_CELL`. Каталог
|
||||
кодов в `arm-core/README.md` («Ошибки API») дополняется в той задаче, что вводит код.
|
||||
- Один `PATCH …/statement` — один пересчёт и одна запись `audit_event` `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`,
|
||||
`object_type = "document"`); запрос без изменений — 200 без аудита. Аудит — только через `services/audit.py`.
|
||||
@ -196,36 +197,45 @@ cd $WS/.wt/normalization-core-a/arm-core && codegraph init && make venv
|
||||
```python
|
||||
# api/src/services/statements_engine.py — единственная точка вызова функций arm_statements
|
||||
from arm_statements import (DocStatus, DocumentType, Edition, FormVariant, PeriodKind, RecomputeResult, Rules,
|
||||
StatusInput, StoredForm, default_rules, document_status, recompute, resolve_relative,
|
||||
statements_version)
|
||||
StoredForm, choose_edition, default_rules, document_status, recompute, resolve_relative,
|
||||
status_input_from_contract, statements_version)
|
||||
|
||||
class StatementsEngine(Protocol): # тонкая обёртка: подмена в модульных тестах (FakeEngine), кэш правил на процесс
|
||||
@property
|
||||
def version(self) -> str | None: ... # statements_version(); нет файла VERSION — None
|
||||
def recompute(self, form: StoredForm) -> RecomputeResult: ... # recompute(form, rules)
|
||||
def document_status(self, status_input: StatusInput) -> DocStatus: ... # document_status(status_input)
|
||||
def contract_document_status(self, document: Mapping[str, Any]) -> DocStatus: ...
|
||||
# document_status(status_input_from_contract(document)) — statementDocument результата 2.0 как dict
|
||||
def edition(self, document_type: DocumentType, codes: Sequence[tuple[str, bool]],
|
||||
effective_date: datetime.date | None, form_variant: FormVariant | None) -> Edition | None: ...
|
||||
# choose_edition(document_type, codes, effective_date, rules, variant=form_variant)
|
||||
def code_order(self, edition: Edition, document_type: DocumentType, form_variant: FormVariant) -> tuple[str, ...]: ...
|
||||
# rules.code_orders.sequence(edition, document_type, form_variant)
|
||||
def editions(self) -> tuple[Edition, ...]: ... # rules.code_orders.editions — порядок редакций перечня
|
||||
# rules.code_orders.sequence(edition, document_type, form_variant); DISCLOSURE → ()
|
||||
def editions(self) -> tuple[Edition, ...]: ... # rules.code_orders.editions — (RAS_66N, FSBU4)
|
||||
def resolve_relative(self, kind: PeriodKind, effective_date: datetime.date | None) -> datetime.date | None: ...
|
||||
def tolerance(self) -> Decimal: ... # rules.checks.tolerance — допуск ±1, для межформенной проверки ядра
|
||||
# ABSOLUTE / NONE → None
|
||||
def tolerance(self) -> Decimal: ... # rules.checks.tolerance — Decimal("1"), для межформенной проверки ядра
|
||||
|
||||
def default_engine() -> StatementsEngine: ... # ArmStatementsEngine(default_rules()), кэш на процесс
|
||||
```
|
||||
|
||||
| Вызов ядра | Имя плана 1 | Где используется |
|
||||
|---|---|---|
|
||||
| `engine.recompute(form)` | `recompute(form: StoredForm, rules) -> RecomputeResult` | B2 (после `PATCH`), B3 (приём `1.x`), B4 (только `edition`) |
|
||||
| `engine.document_status(...)` | `document_status(StatusInput) -> DocStatus` | B4 (страховка статуса результата `2.0`) |
|
||||
| `engine.code_order(...)` | `default_rules().code_orders.sequence(edition, form, variant)` | B4, B6 |
|
||||
| `engine.editions()` | `default_rules().code_orders.editions` | B6 |
|
||||
| `engine.resolve_relative(...)` | `resolve_relative(kind, effective)` | B2 (правка `header.report_date`) |
|
||||
| `engine.tolerance()` | `default_rules().checks.tolerance` | B6 |
|
||||
| `engine.recompute(form)` | `recompute(form: StoredForm, rules) -> RecomputeResult` | B2 (после `PATCH`), B3 (приём `1.x`) |
|
||||
| `engine.contract_document_status(doc)` | `document_status(status_input_from_contract(doc))` | B4 (страховка статуса результата `2.0`) |
|
||||
| `engine.edition(...)` | `choose_edition(document_type, codes, effective, rules, *, variant)` | B4 (редакция формы `2.0` для `CODE_NOT_IN_EDITION`) |
|
||||
| `engine.code_order(...)` | `default_rules().code_orders.sequence(edition, form, variant) -> tuple[str, ...]` | B4, B6 |
|
||||
| `engine.editions()` | `default_rules().code_orders.editions -> tuple[Edition, ...]` | B6 |
|
||||
| `engine.resolve_relative(...)` | `resolve_relative(kind, effective) -> date \| None` | B2 (правка `header.report_date`) |
|
||||
| `engine.tolerance()` | `default_rules().checks.tolerance -> Decimal` | B6 |
|
||||
| `engine.version` | `statements_version()` | B2 (`STATEMENTS_VERSION_MISMATCH`) |
|
||||
| `StoredForm`, `StoredColumn`, `StoredLine`, `StoredValue`, `StoredWarning` | одноимённые dataclass-ы пакета | B1 (`build_stored_form`) |
|
||||
| `StatusInput`, `StatusLine`, `StatusValue` | одноимённые dataclass-ы пакета | B1 (`status_input_from_v2`) |
|
||||
| `FailedRelation`, `StatementChecks` | из `RecomputeResult.checks` | B2 (`relations_to_issues`) |
|
||||
|
||||
`to_contract` / `warning_to_contract` ядру не нужны: ядро читает контракт, а не пишет его. `StatusInput` ядро не собирает
|
||||
— вход статуса из документа результата строит пакет (`status_input_from_contract`), поэтому страховка статуса считает
|
||||
ровно то, что посчитал сервис (план 1, P1-27).
|
||||
|
||||
Соглашения на границе ядра и пакета (все — в `stored_form.py` / адаптере, остальной код ядра их не видит):
|
||||
|
||||
- Перечисления пакета — `StrEnum` (подкласс `str`, значения — строки контракта и БД): строка БД превращается в
|
||||
@ -234,11 +244,14 @@ def default_engine() -> StatementsEngine: ... # ArmStatementsEngine(default_rul
|
||||
- Уверенности пакета — `float | None` (0–100), в БД — `Numeric(5,2)`: при сборке `float(confidence)`, `None` остаётся `None`.
|
||||
`StoredValue.is_recognized` — `bool`: `NULL` в БД читается как `False`.
|
||||
- `StoredColumn.column_index: int | None` (колонка без номера столбца допустима); ключи строки и колонки — `str(uuid)`.
|
||||
- `RecomputeResult.edition: Edition | None` — `None` при нераспознанной редакции (нет даты и ничья): проверки по редакции
|
||||
(`CODE_NOT_IN_EDITION`) тогда не пишутся.
|
||||
- `RecomputeResult.edition` и `engine.edition(...)` — `Edition | None`; `None` при нераспознанной редакции (нет даты и
|
||||
ничья): проверки по редакции (`CODE_NOT_IN_EDITION`) тогда не пишутся. Пары `codes` для `engine.edition` — строки `ok`
|
||||
результата по `row_index`: `(line_code, line_code_confidence == 100)` (у результата `2.0` кодов, правленых аналитиком,
|
||||
нет); пересчёт после правки берёт ту же функцию внутри `recompute` — редакция приёма и пересчёта совпадает.
|
||||
- `FailedRelation` несёт `column_index: int | None` и `column_key: str`; при пересчёте из БД `column_key` — это
|
||||
`StoredColumn.key`, то есть `str(column_id)`: колонка находится по ключу без обратного сопоставления по индексу.
|
||||
`expected` и `actual` — всегда `Decimal`.
|
||||
`expected` и `actual` — `Decimal | None` (`None` — цель или все члены пусты, план 1 P1-21); `document_issue.expected` /
|
||||
`actual` — `numeric(18,2)` без `NOT NULL`, `IssueDraft.expected` / `actual` — `Decimal | None`.
|
||||
|
||||
Ключи хоста — `str(uuid)` строки и колонки (`key`, `column_key`, `parent_key`).
|
||||
|
||||
@ -259,37 +272,40 @@ def default_engine() -> StatementsEngine: ... # ArmStatementsEngine(default_rul
|
||||
| P3-9 | Результат `2.0`: значение с `column_index` вне колонок формы и повтор `column_index` колонки — первое остаётся, `MAPPING_WARNING` | FK значения на колонку; уникальность `(meta_id, column_index)` | нет |
|
||||
| P3-10 | Части `PATCH` применяются в порядке `lines` → `values` → `columns` → `header`; вся адресация (422) проверяется до первой записи; `UNKNOWN_CELL` по `line_code` — по кодам строк `ok` с учётом назначений `lines` этого запроса; один `line_id` дважды в `lines` — 422 `INVALID_LINE_PATCH` | атомарность и предсказуемость | нет |
|
||||
| P3-11 | `reference_name` — первое имя кода в редакции ответа (`edition` по дате); нет редакции или кода — `null` | спека: «первое имя кода в словаре»; `edition` ответа — по дате | нет (Р-29) |
|
||||
| P3-12 | Очистка отказывает, если документ формы привязан к версии **любого** статуса не «в работе» (как `DELETE /files`), а не только `VERIFIED` и далее | безопаснее; владелец решает по отчёту | O-5 |
|
||||
| P3-12 | Очистка отказывает, только если документ формы упомянут в `ex_snapshot_item` или привязан к версии в статусе «Верифицирована» или дальше: `VERIFIED` или `ARCHIVED` с `ex_snapshot.source_version_id` = версия; привязки к версиям в работе, на согласовании, `REJECTED`, `CANCELLED` (и архивированным отменённым) снимаются, статус версий не меняется | решение владельца 18.10.2026 (Р-35): верифицированная версия read-only и заморожена в ЕХ, прочие — нет; на стенде версий дальше «в работе» нет | архивированная версия без снимка ЕХ (из `CANCELLED`) очистке не мешает — если бы мешала, владелец увидел бы лишний отказ |
|
||||
| P3-13 | Коды выхода `purge_statements.py`: `0` — отчёт или выполнено, `1` — отказ (п. 2 спеки), `2` — ошибка аргументов | как у `check_dbml_vs_orm.py` | нет |
|
||||
| P3-14 | До части Б §7 считает ядро на лету (`statement_checks.yaml`) по агрегату; `checks.failed[].codes` берутся из правила, `line_ids` — по агрегату | часть А должна быть самосогласованной | временный код удаляется в B6 |
|
||||
| P3-15 | Для условия `RECOGNIZED` `2.0` предупреждения файла берутся из `metrics.warnings` файла результата; в `recognition_job.metrics.warnings` — из конверта (inbox) | маппер читает файл результата; по контракту наборы одинаковы | нет |
|
||||
| P3-16 | Подтверждение кода (`lines[] {confirmed: true}`) ставит `is_confirmed = true` и `needs_review = false`, `line_code_confidence` и `line_code_modified` **не меняет**; назначение кода — `confidence = 100`, `line_code_modified = true` | иначе исходная уверенность теряется, а снятие подтверждения не может вернуть `needs_review` (гайд §5.6.1: «при `false` — пересчитывается»); аналогия с ячейкой (`is_modified` не ставится) | O-2: в спеке `arm-core` строка «Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`» |
|
||||
| P3-16 | Подтверждение кода (`lines[] {confirmed: true}`) ставит `is_confirmed = true` и `needs_review = false`, `line_code_confidence` и `line_code_modified` **не меняет**; назначение кода — `confidence = 100`, `line_code_modified = true` | иначе исходная уверенность теряется, а снятие подтверждения не может вернуть `needs_review` (гайд §5.6.1: «при `false` — пересчитывается»); аналогия с ячейкой (`is_modified` не ставится); подтверждено владельцем 18.10.2026 (Р-34), спека `arm-core`, §9.5 п. 9 и гайд §5.6.1 приведены к этому | нет |
|
||||
| P3-17 | Порядок строк ответа: строки `ok` (с шаблонными) — по коду, затем `row_index` (`NULL` последними), затем `id`; за каждой строкой `ok` источника — её «вложения» по `row_index`: строки `detail` с этим родителем и строки `unreadable` / `missing` / `detail` без родителя, у которых она — ближайшая предыдущая по `row_index` строка `ok` источника; вложения без такой строки — в начале | спека «Ответ `GET …/statement`» | нет |
|
||||
| P3-18 | Изменение `crm_id` ГСК/ГСЗ пишет одно событие `GROUP_CHANGE` с `before/after` всех изменённых полей вместо `MEMBERSHIP_CHANGE` | спека: «изменение — аудит `GROUP_CHANGE`» | O-6 |
|
||||
| P3-19 | Ядро зовёт у пакета `choose_edition` (`engine.edition`) и `status_input_from_contract` (`engine.contract_document_status`) вместо сборки `StoredForm` ради редакции и своего `status_input_from_v2`; `to_contract` ядру не нужен | одно правило с сервисом и с `recompute` (план 1, P1-25, P1-27); пары `(код, уверенный)` — из строк `ok` результата по `row_index` | при ошибке сборки пар редакция приёма разойдётся с редакцией после правки — тест B1 сравнивает их |
|
||||
| P3-20 | `header.source.<поле>.confidence` — строка Decimal с двумя знаками (`"61.54"`), как прочие уверенности API; в JSONB `header_source` остаётся число как пришло | фронт читает все уверенности одним способом (план 6, A8) | округление до сотых в подсказке; исходное число — в БД |
|
||||
| P3-21 | `StatementHeaderOut.okei` — из `financial_statement_meta.okei` (колонка есть с v5.18), только чтение; в `PATCH header` не входит | спека `header` контракта несёт `okei`, экран показывает `issues` с `field = "okei"` у поля | нет |
|
||||
| P3-22 | Справочник строк для подсказки кода — поле `StatementOut.edition_lines [{code, name}]`, а не отдельный эндпоинт: данные уже загружены (`LineCatalog`, A6) при сборке ответа, нет нового маршрута, права, кэша и запроса на фронте; около 80 пар на форму (≈ 4 КБ) против строк формы с значениями; у формы без даты — коды всех редакций, по одному | наименьший объём реализации в ядре и во фронте; закрывает пробел «у формы без даты подсказки нет» (план 6, O-3) | ответ `GET` тяжелее на ≈ 4 КБ; если понадобится справочник вне формы — отдельный эндпоинт поверх того же `LineCatalog.edition_lines` |
|
||||
| P3-23 | `DocumentBrief.issue_counts {error, info}` — все записи `document_issue` документа (и `relation` / `soft`, в том числе акцептованные) по `severity`; один групповой запрос на файл | панели файлов нужен признак «есть что смотреть» без запроса на каждый документ (план 6, O-4) | акцептованное нарушение продолжает помечать документ в панели — подробности на экране документа |
|
||||
| P3-24 | Ошибки элемента `lines[]` (409 `LINE_CODE_TAKEN`, 422 `LINE_CODE_REQUIRED`, `INVALID_LINE_CODE`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`) несут в `detail` `line_id` и `line_code` первого неверного элемента; `LINE_CODE_REQUIRED` добавлен к четырём кодам запроса фронта | экран ставит ошибку у поля нужной строки, не угадывая (план 6, O-2); одно правило для всех ошибок элемента | при нескольких неверных элементах фронт видит только первый — как и прежде |
|
||||
|
||||
## Открытые вопросы (`O-n`)
|
||||
|
||||
- **O-1.** Р-10 для результатов `1.x` (P3-4): хранить вторую форму того же типа и при `1.x` или пропускать, как раньше?
|
||||
План хранит.
|
||||
- **O-2.** Подтверждение кода строки (P3-16) расходится с буквой спеки `arm-core` («подтверждённый код: `line_code_confidence
|
||||
= 100`, `line_code_modified = true`») и согласуется с гайдом и DBML («без изменения»). Нужна правка спеки одной строкой
|
||||
или подтверждение буквы (тогда снятие подтверждения не возвращает `needs_review`).
|
||||
- **O-2.** Закрыт решением владельца 18.10.2026 (Р-34): подтверждение кода уверенность не меняет (P3-16); буква спеки
|
||||
`arm-core`, `05-domain-rules.md` §9.5 п. 9 и гайда §5.6.1 приведена к этому правилу.
|
||||
- **O-3.** `CODE_NOT_IN_DICTIONARY` / `CODE_NOT_IN_EDITION` у пятизначных подстрок: каждая подстрока Excel даёт `info`.
|
||||
Спека это требует (сценарий «Нестандартный код Excel»); план пишет по записи на код. Ограничить четырёхзначными?
|
||||
- **O-4.** Пересчёт проверок ядра `info` после правки (P3-8) спека не описывает.
|
||||
- **O-5.** Отказ очистки при привязке к `CANCELLED` / `REJECTED` / маршруту согласования (P3-12) — или только `VERIFIED` и далее?
|
||||
- **O-5.** Закрыт решением владельца 18.10.2026 (Р-35): отказ только при верифицированной версии (P3-12); спека `arm-core`
|
||||
«Очистка форм перед выкладкой» п. 2 приведена к этому правилу, сценарий «Форма в версии на согласовании или
|
||||
отклонённой» добавлен.
|
||||
- **O-6.** Действие аудита при смене `crm_id` ГСК/ГСЗ (P3-18): сейчас правки ГСК/ГСЗ пишут `MEMBERSHIP_CHANGE`.
|
||||
- **O-7.** Конверт с неизвестным MAJOR (`3.0`) inbox, как и сейчас, отбрасывает с предупреждением в лог (задание закроется
|
||||
по сроку); сценарий спеки «файл получает `ERROR` с `mapping_error`» выполняется для файла результата. Оставить?
|
||||
- **O-8.** Сверка с планом 1 выполнена (ядро вызывает имена его раздела «Публичный API пакета»). Пробелы API пакета для
|
||||
ядра, план 1 не правился: (а) типы возврата `Rules.code_orders.sequence` (`tuple[str, ...]`?), `.editions`
|
||||
(`tuple[Edition, ...]`?) и `Rules.checks.tolerance` (`Decimal`?) в плане 1 не заданы, а параметр `variant` у `sequence`
|
||||
— `FormVariant` или `FormVariant | None` (для `DISCLOSURE`) не сказан; ядро предполагает перечисленные в «Что ядро
|
||||
вызывает»; (б) `Edition` формы без строк, то есть в B4 при приёме `2.0`, публично определяется только через
|
||||
`recompute(StoredForm)` (ядро строит `StoredForm` из документа результата ради одного поля `edition`) — нужна
|
||||
функция вроде `choose_edition(effective, codes)`; (в) поведение `resolve_relative` для `ABSOLUTE` / `NONE` не задано
|
||||
(ядро вызывает её только для `relative:*`); (г) `StatusInput` собирает хост — функции «документ результата ->
|
||||
`StatusInput`» в пакете нет, `status_input_from_v2` (B1) её заменяет.
|
||||
- **O-8.** Закрыт 18.10.2026 сведением с планом 1 (его P1-21…P1-31): (а) типы `Rules.code_orders.sequence(...) ->
|
||||
tuple[str, ...]` (параметр `variant: FormVariant`, у `DISCLOSURE` — `()`), `.editions -> tuple[Edition, ...]`,
|
||||
`Rules.checks.tolerance -> Decimal` записаны в API пакета; (б) редакция без пересчёта — `choose_edition` (`engine.edition`,
|
||||
B4); (в) `resolve_relative(ABSOLUTE | NONE, …)` → `None`; (г) вход статуса из документа результата —
|
||||
`status_input_from_contract` пакета (`engine.contract_document_status`), `status_input_from_v2` не пишется.
|
||||
- **O-9.** Выкладка: PR части А в `main` без снимка контура до части Б; встроенный фронт `arm-core/web` получает новые типы
|
||||
только через `sync-web` после плана 6 — выкладка ядра и фронта одним снимком.
|
||||
- **O-10.** Метрики `fields_*` ядро сейчас не считает по строкам формы (копирует из конверта, `recognition_exchange.FIELD_COUNTERS`):
|
||||
@ -488,10 +504,10 @@ assert await count(conn, "financial_statement_line") == lines_before # данн
|
||||
**Interfaces:**
|
||||
- Consumes: `files_repo.delete_file_rows`, `files_repo.expire_s3_objects`, `statements_repo.delete_file_statements`
|
||||
(уже чистит `document_issue`, A1), `consolidation_documents.bound_versions` / `unbind_documents`, `AuditService.record`,
|
||||
`IN_WORK_STATUSES`.
|
||||
`ex_snapshot` (`source_version_id`).
|
||||
- Produces:
|
||||
- `@dataclass PurgePlan`: `form_only_files: list[uuid.UUID]`, `mixed_files: list[uuid.UUID]`, `form_documents: int`,
|
||||
`confirmed_documents: int`, `in_work_bindings: int`, `blocking: list[BlockingVersion]` (`version_id`, `status`,
|
||||
`confirmed_documents: int`, `unverified_bindings: int`, `blocking: list[BlockingVersion]` (`version_id`, `status`,
|
||||
`document_id`), `superseded_with_osv: list[uuid.UUID]` (файлы, заменённые удаляемым файлом, в которых есть ОСВ);
|
||||
- `async def plan_purge(session: AsyncSession, *, include_mixed: bool) -> PurgePlan` — только чтение;
|
||||
- `async def apply_purge(session: AsyncSession, plan: PurgePlan, *, now: datetime) -> None`;
|
||||
@ -502,10 +518,14 @@ assert await count(conn, "financial_statement_line") == lines_before # данн
|
||||
- [ ] **Step 1: Write the failing tests** (БД теста, фабрики `tests/factories.py`):
|
||||
- `test_dry_run_reports_and_changes_nothing`: файл только с ББ+ОФР (ББ `CONFIRMED`, привязан к версии `INITIATED`),
|
||||
смешанный файл (ОСВ + ББ), файл только с ОСВ → `run([])` → код 0; в отчёте строки `файлов только с формами: 1`,
|
||||
`смешанных файлов: 1`, `документов форм: 3`, `подтверждённых документов: 1`, `привязок к версиям «в работе»: 1`; число
|
||||
`смешанных файлов: 1`, `документов форм: 3`, `подтверждённых документов: 1`, `привязок к неверифицированным версиям: 1`; число
|
||||
строк всех таблиц не изменилось.
|
||||
- `test_apply_refuses_verified_binding`: ББ привязан к версии `VERIFIED` → `run(["--apply"])` → код 1, в выводе id версии
|
||||
и `VERIFIED`; база не изменена. То же для документа в `ex_snapshot_item.document_id`.
|
||||
и `VERIFIED`; база не изменена. То же для документа в `ex_snapshot_item.document_id` и для версии `ARCHIVED`, у которой
|
||||
есть `ex_snapshot` с её `source_version_id`.
|
||||
- `test_apply_ignores_unverified_versions` (сценарий «Форма в версии на согласовании или отклонённой», P3-12): ББ
|
||||
привязан к версии `VERIFICATION_RF`, ОФР — к `REJECTED`, ещё одна форма — к `CANCELLED` и к `ARCHIVED` без
|
||||
`ex_snapshot` → `run(["--apply"])` → код 0; привязки сняты, формы удалены, статусы всех версий прежние.
|
||||
- `test_apply_deletes_form_only_file`: файл с ББ, ОФР, `DISCLOSURE`, комментарием к ББ, заданием; файл `old` с
|
||||
`superseded_by_file_id` = удаляемый → после `--apply`: файла, документов, `financial_statement_*`, комментариев, заданий
|
||||
нет; `old.superseded_by_file_id is None`, `old.status == SUPERSEDED`; `s3_object.expires_at` выставлен; привязка снята,
|
||||
@ -701,9 +721,12 @@ def test_template_line_counts_as_ok():
|
||||
|
||||
**Files:**
|
||||
- Create: `api/src/domain/statement_layout.py`
|
||||
- Modify: `api/src/schemas/statements.py`, `api/src/schemas/documents.py`, `api/src/services/statements.py`
|
||||
(`build_out`), `api/src/services/documents.py` (`document_out`), `api/src/repositories/statements.py` (`issues`)
|
||||
- Test: `api/tests/test_domain_statement_layout.py`, `api/tests/test_api_statements.py`, `api/tests/test_api_documents.py`
|
||||
- Modify: `api/src/schemas/statements.py`, `api/src/schemas/documents.py`, `api/src/schemas/files.py` (`DocumentBrief`),
|
||||
`api/src/services/statements.py` (`build_out`), `api/src/services/documents.py` (`document_out`, `file_out` — `issue_counts` рядом с `counts` комментариев),
|
||||
`api/src/services/primary_files.py` (сборка `FileOut.documents`), `api/src/repositories/statements.py` (`issues`,
|
||||
`issue_counts_by_document`), `api/src/domain/statement_lines.py` (`LineCatalog.edition_lines`)
|
||||
- Test: `api/tests/test_domain_statement_layout.py`, `api/tests/test_api_statements.py`, `api/tests/test_api_documents.py`,
|
||||
`api/tests/test_api_files_read.py`, `api/tests/test_domain_statement_lines.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `CodeAggregate` (A7), `LineCatalog.reference_name` (A6).
|
||||
@ -713,12 +736,25 @@ def test_template_line_counts_as_ok():
|
||||
- `repo.issues(session, document_id) -> list[DocumentIssue]` (по `created_at, id`);
|
||||
- `IssueOut {id, kind, code, severity, message, line_ids: list[uuid.UUID], column_id, column_index, field, source,
|
||||
relation, codes: list[str], expected, actual}` — общий для `StatementOut.issues` и `DocumentOut.issues`;
|
||||
- `StatementDocumentOut.form_variant`; `StatementHeaderOut.needs_review_fields: list[str]`, `source: dict[str, dict[str,
|
||||
Any]]` (= `header_source` или `{}`), `organization_name_modified: bool`; `ColumnOut.period_kind`,
|
||||
`label_confidence`; `LineOut` + `row_index`, `source_ref`, `line_status`, `line_code_text`, `reference_name`,
|
||||
`parent_row_index`, `needs_review`, `is_confirmed`, `line_code_modified`; `ValueOut` + `text` (`source_text`),
|
||||
`needs_review`, `is_confirmed`; `FailedCheckOut` + `codes`, `line_ids`; `StatementOut.issues: list[IssueOut]`;
|
||||
- `DocumentOut.issues: list[IssueOut]` (у любого документа); `DocumentOut.warnings` остаётся.
|
||||
- `StatementDocumentOut.form_variant`; `StatementHeaderOut.needs_review_fields: list[str]`, `source: dict[str,
|
||||
HeaderSourceOut]` (= `header_source` или `{}`; `HeaderSourceOut {text: str | None, confidence: Decimal | None}` —
|
||||
число из JSONB приводится `Decimal(str(x)).quantize(Decimal("0.01"))` и отдаётся строкой, как прочие уверенности
|
||||
API, P3-20), `organization_name_modified: bool`, `okei: str | None` (`financial_statement_meta.okei`, P3-21);
|
||||
`ColumnOut.period_kind`, `label_confidence`; `LineOut` + `row_index`, `source_ref`, `line_status`, `line_code_text`,
|
||||
`line_code_confidence: Decimal | None` (строка), `reference_name`, `parent_row_index`, `needs_review`,
|
||||
`is_confirmed`, `line_code_modified`; `ValueOut` + `text` (`source_text`), `needs_review`, `is_confirmed`;
|
||||
`FailedCheckOut` + `codes`, `line_ids`; `StatementOut.issues: list[IssueOut]`;
|
||||
- `StatementOut.edition_lines: list[EditionLineOut]`, `EditionLineOut {code: str, name: str}` — справочник строк для
|
||||
подсказки кода (P3-22): строки словаря имён формы `document.document_type` в редакции ответа (`edition`), по одной
|
||||
на код, имя — первое (как `reference_name`), в порядке словаря; `edition is None` — строки всех редакций словаря по
|
||||
порядку редакций, по одной на код (имя — из первой редакции, где код есть); `LineCatalog.edition_lines(form: str,
|
||||
edition: str | None) -> list[tuple[str, str]]`;
|
||||
- `DocumentOut.issues: list[IssueOut]` (у любого документа); `DocumentOut.warnings` остаётся;
|
||||
- `DocumentBrief.issue_counts: IssueCountsOut {error: int, info: int}` (а значит и `DocumentOut.issue_counts`) — число
|
||||
записей `document_issue` документа по `severity`, всех видов (`warning`, `relation`, `soft`), P3-23;
|
||||
`repo.issue_counts_by_document(session, document_ids: Sequence[uuid.UUID]) -> dict[uuid.UUID, IssueCounts]` — один
|
||||
запрос `GROUP BY document_id, severity` на список документов (как `comments_count_by_document`); документа нет в
|
||||
ответе — `{error: 0, info: 0}`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests:**
|
||||
|
||||
@ -738,7 +774,16 @@ def test_detail_without_parent_follows_previous_ok():
|
||||
`header.organization_name_modified is False`; `columns[].period_kind ∈ {"absolute", "none"}`; `issues == []`);
|
||||
`test_reference_name_first_name` (`fsbu4`, строка `1520` → `reference_name == "Кредиторская задолженность"`);
|
||||
`test_issues_in_statement_and_document` (вставленная запись `document_issue` `warning` `COLUMN_SKIPPED` с `column_index =
|
||||
5`, `column_id = null` → она в `StatementOut.issues` и `DocumentOut.issues` с этими полями).
|
||||
5`, `column_id = null` → она в `StatementOut.issues` и `DocumentOut.issues` с этими полями);
|
||||
`test_line_code_confidence_and_okei` (строка с `line_code_confidence = 67.50` → `"67.50"`; `meta.okei = "384"` →
|
||||
`header.okei == "384"`); `test_header_source_confidence_is_decimal_string` (`header_source = {"inn": {"text": "x",
|
||||
"confidence": 61.538464}, "units": {"text": "тыс. руб.", "confidence": 100}}` → `header.source.inn.confidence ==
|
||||
"61.54"`, `header.source.units.confidence == "100.00"`; `confidence: null` → `null`);
|
||||
`test_edition_lines` (форма `fsbu4` ББ → `edition_lines` — коды редакции `fsbu4` ББ словаря имён по одному, `1520` с
|
||||
именем «Кредиторская задолженность», `1120` нет; форма без даты (`edition is None`) → коды обеих редакций, `1120`
|
||||
есть, повторов кода нет); `test_issue_counts_in_file_and_document` (у ББ записи `warning` `error`, `relation` `error`,
|
||||
`soft` `info`, у ОФР нет → в `GET /files/{id}` `documents[]`: ББ `issue_counts == {"error": 2, "info": 1}`, ОФР `{"error":
|
||||
0, "info": 0}`; `GET /documents/{id}` — те же числа; число SQL-запросов на `GET /files` не растёт с числом документов).
|
||||
- [ ] **Step 2:** запуск трёх файлов — FAIL.
|
||||
- [ ] **Step 3: Implement**; порядок `lines` ответа — `order_lines` (достроенные шаблонные — как строки `ok` без
|
||||
`row_index`).
|
||||
@ -970,7 +1015,10 @@ def test_has_error_warning():
|
||||
dict) -> bool` (возвращает «были правки данных» — для сброса акцепта): проверки до записи — `INVALID_LINE_PATCH` (не
|
||||
ровно одно действие; один `line_id` дважды), `UNKNOWN_LINE`, `LINE_CODE_REQUIRED` (`line_status == "ok"`),
|
||||
`INVALID_LINE_CODE` (не `^\d{4,5}$`), затем `LINE_CODE_TAKEN` (строка `row_index IS NULL`, `line_status = ok`, с тем
|
||||
же кодом и хотя бы одной непустой `amount`; 409, ничего не изменено); затем запись: удаление пустых шаблонных строк
|
||||
же кодом и хотя бы одной непустой `amount`; 409, ничего не изменено); у всех пяти ошибок `detail` несёт `line_id` и
|
||||
`line_code` первого неверного элемента `lines[]` по порядку запроса (`line_id` — как в элементе, у `UNKNOWN_LINE` —
|
||||
чужой id; `line_code` — код элемента или `null`; у `LINE_CODE_TAKEN` — строка, которой назначается код, и этот код;
|
||||
P3-24); затем запись: удаление пустых шаблонных строк
|
||||
назначаемых кодов (значения → строка → `id` из `line_ids` записей документа), назначение (`line_status = ok`,
|
||||
`line_code`, `line_code_confidence = 100`, `line_code_modified = true`, `needs_review = false`), отметка `detail`
|
||||
(`line_code = null`), подтверждение (P3-16), пересчёт `parent_row_index` всех строк `detail`;
|
||||
@ -989,16 +1037,20 @@ def test_has_error_warning():
|
||||
(`row_index 2`) с суммой → обе `ok` с 1250, обе в БД, 409 нет — Review Focus 3;
|
||||
- `test_code_taken_by_template_with_amount` — шаблонная `1250` (`row_index = null`) с суммой → `lines: [{unreadable,
|
||||
line_code: "1250"}, {other, line_status: "detail"}]`, `values: [...]`, `header: {units: "руб."}` → 409
|
||||
`LINE_CODE_TAKEN`; ни одна строка, ячейка и поле шапки не изменились, аудита нет — Review Focus 4;
|
||||
`LINE_CODE_TAKEN` с `detail.line_id` строки `unreadable` и `detail.line_code == "1250"`; ни одна строка, ячейка и поле
|
||||
шапки не изменились, аудита нет — Review Focus 4;
|
||||
- `test_code_taken_by_empty_template` — шаблонная `1250` без сумм и запись `document_issue` с `line_ids = [template,
|
||||
other]` → шаблонной строки и её значений нет, `line_ids == [other]`, строка источника `ok` 1250 — Review Focus 4;
|
||||
- `test_mark_detail` — `missing` с суммой → `line_status == "detail"`, в агрегате её нет, подтверждение не даёт
|
||||
`LINES_UNRESOLVED`;
|
||||
- `test_status_ok_without_code` — `line_status: "ok"` → 422 `LINE_CODE_REQUIRED`;
|
||||
- `test_status_ok_without_code` — `line_status: "ok"` → 422 `LINE_CODE_REQUIRED`, `detail.line_id == id`,
|
||||
`detail.line_code is None`;
|
||||
- `test_invalid_line_patch` — `{line_id}`; `{line_id, line_code, confirmed}`; один `line_id` дважды → 422
|
||||
`INVALID_LINE_PATCH`;
|
||||
- `test_unknown_line` — строка другой формы → 422 `UNKNOWN_LINE`;
|
||||
- `test_invalid_line_code` — `"123"`, `"123456"`, `"12a4"` → 422 `INVALID_LINE_CODE`; `"11101"` → 200;
|
||||
`INVALID_LINE_PATCH`, `detail.line_id` — id элемента (у повтора — повторённый id), `detail.line_code` — код
|
||||
элемента или `None`;
|
||||
- `test_unknown_line` — строка другой формы → 422 `UNKNOWN_LINE`, `detail.line_id` — этот id;
|
||||
- `test_invalid_line_code` — `"123"`, `"123456"`, `"12a4"` → 422 `INVALID_LINE_CODE`, `detail == {code, message,
|
||||
line_id, line_code: "123"…}`; `"11101"` → 200;
|
||||
- `test_confirm_code` — строка `ok` 1120, уверенность 50, `needs_review = true`, акцепт стоит → `confirmed: true` →
|
||||
`is_confirmed is True`, `needs_review is False`, акцепт сохранён; `confirmed: false` → `is_confirmed is False`
|
||||
(P3-16);
|
||||
@ -1049,7 +1101,8 @@ def test_has_error_warning():
|
||||
- Produces:
|
||||
- `StatementsEngine` (протокол) и `default_engine()` — как в разделе «Что ядро вызывает»; `ArmStatementsEngine.version`
|
||||
возвращает `statements_version()` (содержимое `api/arm_statements/VERSION`; нет файла — `None`), `recompute` /
|
||||
`code_order` / `editions` / `tolerance` берут `default_rules()` пакета;
|
||||
`edition` / `code_order` / `editions` / `tolerance` берут `default_rules()` пакета, `contract_document_status` —
|
||||
`document_status(status_input_from_contract(document))`;
|
||||
- `build_stored_form(document: Document, meta: FinancialStatementMeta, columns: Sequence[FinancialStatementColumn],
|
||||
lines: Sequence[tuple[FinancialStatementLine, Sequence[FinancialStatementValue]]], warnings: Sequence[DocumentIssue])
|
||||
-> StoredForm` (типы пакета, перечисления — `StrEnum`: `DocumentType`, `LineStatus`, `PeriodKind`, `Severity`;
|
||||
@ -1061,21 +1114,21 @@ def test_has_error_warning():
|
||||
line.is_confirmed`; `value_fixed = value.is_modified or value.is_confirmed`; `text = value.source_text`;
|
||||
`parent_key` — ключ строки с `row_index == line.parent_row_index` (у `detail`), иначе `None`; `key` строки и колонки —
|
||||
`str(uuid)`;
|
||||
- `status_input_from_v2(doc: StatementDocumentV2) -> StatusInput` — вход страховки статуса из результата (поля
|
||||
`StatusInput` плана 1): `document_type = DocumentType(doc.document_type)`, `layout_found = bool(doc.lines)`,
|
||||
`has_organization_or_inn` — по `header.organization_name`/`header.inn`, `period_known` — `header.report_date` или
|
||||
хотя бы одна колонка с `period_date`, `needs_review_fields = tuple(header.needs_review_fields)`,
|
||||
`lines = tuple(StatusLine(LineStatus(l.line_status), l.needs_review))`, `values = tuple(StatusValue(LineStatus(l.line_status),
|
||||
v.needs_review))` по значениям каждой строки (`line_status` — статус строки значения), `hard_failures =
|
||||
len(checks.relations_failed)` (нет `checks` — 0), `accepted = False`, `warning_severities = tuple(Severity(w.severity)
|
||||
for w in doc.warnings)`;
|
||||
- `FakeEngine` для модульных тестов: возвращает заданный `RecomputeResult`, запоминает вход `StoredForm`.
|
||||
- `edition_codes(doc: StatementDocumentV2) -> list[tuple[str, bool]]` — пары `(line_code, line_code_confidence == 100)`
|
||||
строк `ok` результата по `row_index` (вход `engine.edition`); своего `StatusInput` ядро не собирает — вход статуса
|
||||
из документа результата строит пакет (`status_input_from_contract`, план 1 P1-27);
|
||||
- `FakeEngine` для модульных тестов: возвращает заданный `RecomputeResult`, заданные статус документа и редакцию,
|
||||
запоминает вход `StoredForm`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests:** `test_build_stored_form_maps_flags` (шаблонная строка → `row_index is None`;
|
||||
правленая ячейка → `value_fixed`; подтверждённая ячейка → `value_fixed`; строка с назначенным кодом → `code_fixed`;
|
||||
`detail` с `parent_row_index = 3` → `parent_key` строки `row_index 3`; `warnings` — только `kind = warning`);
|
||||
`test_status_input_from_v2` (на `contracts/examples/result-statement-minimal.json`: `layout_found is True`,
|
||||
`hard_failures == 0`, `needs_review_fields` из шапки, строка `unreadable` во входе в `lines` и `values`); `test_engine_scenario_recompute_after_edit` (реальный пакет,
|
||||
`test_contract_document_status_matches_examples` (реальный пакет: у каждого `statementDocument` из
|
||||
`contracts/examples/result-statement-minimal.json` и `result-statement-excel.json` —
|
||||
`engine.contract_document_status(doc) == DocStatus(doc["status"])`; у минимального — `NEEDS_CHECK` из-за строки
|
||||
`unreadable`); `test_edition_codes_and_engine_edition` (минимальный пример: пары в порядке `row_index`, строка
|
||||
`unreadable` не входит; `engine.edition(DocumentType.BALANCE, pairs, date(2025, 9, 30), FormVariant.FULL)` — `Edition`;
|
||||
тот же ответ, что `recompute` формы, собранной из этого результата); `test_engine_scenario_recompute_after_edit` (реальный пакет,
|
||||
сценарий спеки «Пересчёт после правки по строкам БД»: ячейка 1230 `value_fixed`, код 1250 `code_fixed` у бывшей
|
||||
`unreadable`, шаблонная 1260 с суммой, `detail` с уверенностью 60 → у 1230 и у значения `detail` `needs_review` `False`,
|
||||
у строки 1250 `False`, соотношение `1200 = …` посчитано с 1250 и 1260; ожидаемый `RecomputeResult.checks` и
|
||||
@ -1113,8 +1166,9 @@ def test_has_error_warning():
|
||||
`relations_failed` → `RELATION_FAILED`/`error`, `soft_relations_failed` → `RELATION_SOFT`/`info` (`FailedRelation`
|
||||
плана 1); `column_id` — по `FailedRelation.column_key` (при пересчёте из БД это `str(column_id)`; при приёме `1.x`
|
||||
колонки уже вставлены и ключ строится так же) — по `column_index` только для проверок сервиса `2.0`
|
||||
(`column_key = str(column_index)`, B4); `line_ids` — все строки `ok` кодов `codes`; `expected`/`actual` — `Decimal`;
|
||||
`message` — `f"не сошлось {relation}: {expected} ≠ {actual}"`;
|
||||
(`column_key = str(column_index)`, B4); `line_ids` — все строки `ok` кодов `codes`; `expected`/`actual` — `Decimal |
|
||||
None` (`None` — цель или все члены пусты, план 1 P1-21; в запись — `NULL`); `message` — `f"не сошлось {relation}:
|
||||
{expected} ≠ {actual}"`, `None` пишется словом «пусто»;
|
||||
- `StatementService.recompute` вызывает `recompute_form` (тело A12 удаляется, `nearest_ok_parents` A13 удаляется);
|
||||
- `statement_hard_failures` и `checks.failed`/`soft_failed` ответа — из записей `relation`/`soft` (с `codes`,
|
||||
`line_ids`); `NOTHING_TO_ACCEPT` — нет записей `relation`; правка `header.report_date` (в т. ч. `null`) пересчитывает
|
||||
@ -1174,8 +1228,8 @@ def test_has_error_warning():
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: модели `result_v2` (A5), `IssueDraft`/`add_issues` (A9), `form_status_v2`, `duplicate_period_forms`,
|
||||
`name_mismatches`, `has_error_warning` (A10), `engine.document_status`, `engine.recompute` (только `RecomputeResult.edition`, O-8б),
|
||||
`engine.code_order`, `status_input_from_v2` (B1).
|
||||
`name_mismatches`, `has_error_warning` (A10), `engine.contract_document_status`, `engine.edition`, `engine.code_order`,
|
||||
`edition_codes` (B1).
|
||||
- Produces:
|
||||
- `build_statement_v2(doc: StatementDocumentV2) -> StatementDraftV2` — строки как пришли (`row_index`, `source_ref =
|
||||
ref`, `line_status`, `line_code`, `line_code_original_value = line_code`, `line_code_text`, `line_code_confidence`,
|
||||
@ -1191,7 +1245,10 @@ def test_has_error_warning():
|
||||
`column_index`) и `doc.checks` (`relation`/`soft`, `service`; `column_id` — по `column_index` колонки: у сервиса `FailedRelation.column_key = str(column_index)`);
|
||||
- `core_info_issues(*, lines, edition: Edition | None, form_variant: FormVariant | None, catalog, engine) -> list[IssueDraft]` — `CODE_NOT_IN_DICTIONARY`,
|
||||
`CODE_NOT_IN_EDITION` (P3-7), `NAME_MISMATCH` (строка); `edition is None` или `form_variant is None` — проверки по редакции не пишутся;
|
||||
- статус формы: `service_done = doc.status == DocStatus.DONE and engine.document_status(status_input_from_v2(doc)) == DocStatus.DONE`;
|
||||
`edition = engine.edition(DocumentType(doc.document_type), edition_codes(doc), document.reporting_date,
|
||||
FormVariant(doc.form_variant) if doc.form_variant else None)` — без сборки `StoredForm` и пересчёта;
|
||||
- статус формы: `service_done = doc.status == DocStatus.DONE and engine.contract_document_status(doc.model_dump(mode="json")) == DocStatus.DONE`;
|
||||
`expected`/`actual` нарушений результата — `Decimal | None` (контракт: `null` — цели нет);
|
||||
`doc.status == "done"` при несогласии — запись `RESULT_STATUS_INCONSISTENT` (`info`, `core`, у документа);
|
||||
`file_error_warning = has_error_warning(result.metrics.warnings)` (P3-15); `RECOGNIZED` → автоподтверждение (как сейчас);
|
||||
- `job.statements_version = result.service.statements_version`; `DISCLOSURE` — один документ на файл (P3-6),
|
||||
@ -1299,7 +1356,7 @@ def test_has_error_warning():
|
||||
| Признак строки, созданной правкой | A7, A13 | — |
|
||||
| Шапка формы — сырой текст и уверенность | схема A1; `1.x` A9; `header.source` A8 | `2.0` B4 |
|
||||
| Агрегат формы по коду | A7 | — |
|
||||
| Условие `RECOGNIZED` при приёме 2.0 | чистые функции A10; словарь A6 | B4 (страховка — `engine.document_status`) |
|
||||
| Условие `RECOGNIZED` при приёме 2.0 | чистые функции A10; словарь A6 | B4 (страховка — `engine.contract_document_status`) |
|
||||
| Предупреждения и нарушения документа — `document_issue` | таблица A1; снятие правкой A12; `line_ids` A13; `issues` A8 | `relation`/`soft`, `core` B2–B4 |
|
||||
| Формы результата 1.x — §7 при приёме | поля и `MAPPING_WARNING` A9; `LEGACY_WARNING` A5 | B3 |
|
||||
| Пересчёт после правки | временный пересчёт A12 | B2 |
|
||||
|
||||
@ -269,15 +269,19 @@ SHALL **один раз при приёме** посчитать §7 точко
|
||||
отметкой; `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`; `line_code_original_value` не меняется; после любого изменения статусов или кодов
|
||||
`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`. Правка суммы по `line_code` (для достроенной строки)
|
||||
`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` по коду, статусу, родителю, подтверждениям и наименованию организации.
|
||||
@ -369,13 +373,16 @@ SHALL **один раз при приёме** посчитать §7 точко
|
||||
- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет
|
||||
|
||||
### Requirement: Ответ `GET …/statement` по v2.0
|
||||
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`) и
|
||||
`organization_name_modified`; у колонок `period_kind`, `label_confidence`; у строк `id`, `origin` (`template` ⇔
|
||||
`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`. Порядок
|
||||
`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) строит экран.
|
||||
@ -441,11 +448,14 @@ organization_name`, `document.file_ordinal` — по `(created_at, id)` внут
|
||||
режимами `--dry-run` (по умолчанию: только отчёт) и `--apply`, одной транзакцией:
|
||||
1. **Отбор:** документы `BALANCE` / `INCOME` (у файлов любого статуса, включая `SUPERSEDED`).
|
||||
2. **Отказ без изменений**, если хотя бы один отобранный документ упомянут в `ex_snapshot_item` или привязан
|
||||
(`consolidation_document`) к версии консолидации не «в работе» (`VERIFIED` и далее): отчёт называет версии, решение —
|
||||
за владельцем.
|
||||
(`consolidation_document`) к версии консолидации в статусе «Верифицирована» или дальше — `VERIFIED` или `ARCHIVED`
|
||||
верифицированной версии (есть `ex_snapshot` с `source_version_id` версии): такая версия read-only и заморожена в ЕХ.
|
||||
Отчёт называет версии, решение — за владельцем. Версии в работе, на согласовании, отклонённые (`REJECTED`) и
|
||||
отменённые (`CANCELLED`, в том числе архивированные) очистке не мешают: привязки к ним снимаются, как к версиям
|
||||
«в работе» (п. 3, 4), статус версий не меняется (Р-35).
|
||||
3. **Файлы только с формами** — у файла нет документов `OSV`: удаляется файл целиком, как `DELETE /files/{id}` (§9.5 п. 5),
|
||||
но без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`: привязки документов к версиям «в работе» снимаются (статус версий не
|
||||
меняется), удаляются документы всех типов файла (`BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`) с
|
||||
но без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`: привязки документов к неверифицированным версиям (п. 2) снимаются
|
||||
(статус версий не меняется), удаляются документы всех типов файла (`BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`) с
|
||||
`financial_statement_*` и комментариями документов, задания `recognition_job` файла и `stored_file`; объектам S3
|
||||
ставится `expires_at = now()`; ссылка `superseded_by_file_id` других файлов на удаляемый обнуляется, а заменённые им
|
||||
файлы остаются `SUPERSEDED` (повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ).
|
||||
@ -457,18 +467,22 @@ organization_name`, `document.file_ordinal` — по `(created_at, id)` внут
|
||||
карточка файла в `before_value`); на документ смешанного файла — то же событие по файлу с перечнем удалённых
|
||||
документов.
|
||||
6. **Не трогаются:** ОСВ (`osv_entry`), консолидации и их версии, корректировки, гудвилл, справочники, пользователи,
|
||||
журнал аудита. Результаты КФО версий «в работе», посчитанные с удалёнными формами, пересчитываются, как после удаления
|
||||
файла.
|
||||
журнал аудита. Результаты КФО неверифицированных версий, посчитанные с удалёнными формами, пересчитываются, как после
|
||||
удаления файла.
|
||||
|
||||
#### Scenario: Отчёт перед очисткой
|
||||
- **WHEN** владелец запускает скрипт без ключей
|
||||
- **THEN** скрипт ничего не меняет и печатает число файлов только с формами, смешанных файлов, документов форм,
|
||||
подтверждённых документов и привязок к версиям «в работе»
|
||||
подтверждённых документов и привязок к неверифицированным версиям
|
||||
|
||||
#### Scenario: Форма в верифицированной версии
|
||||
- **WHEN** документ ББ привязан к версии `VERIFIED`
|
||||
- **THEN** `--apply` отказывает с перечнем версий, база не изменена
|
||||
|
||||
#### Scenario: Форма в версии на согласовании или отклонённой
|
||||
- **WHEN** документ ББ привязан к версии `VERIFICATION_RF`, а ОФР — к версии `REJECTED`; верифицированных версий нет
|
||||
- **THEN** `--apply` выполняется: привязки сняты, статусы версий не изменились
|
||||
|
||||
#### Scenario: Очистка и повторная загрузка
|
||||
- **WHEN** очистка выполнена, ядро v5.19 выложено, аналитик загружает тот же PDF с ББ и ОФР
|
||||
- **THEN** дедуп не мешает (файла больше нет), формы создаются по мапперу 1.x или 2.0 — смотря что шлёт сервис
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user