Compare commits
5 Commits
dee36c8d68
...
248f8b0c74
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248f8b0c74 | ||
|
|
2a37a70189 | ||
|
|
2b094ffe0b | ||
|
|
a8a4d90c83 | ||
|
|
e33a4a45f9 |
8
Makefile
8
Makefile
@ -1,7 +1,7 @@
|
||||
# Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория.
|
||||
# Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md.
|
||||
VENV := .venv
|
||||
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema)
|
||||
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML)
|
||||
PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
|
||||
CHANGE ?= slice-0-skeleton
|
||||
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
|
||||
@ -9,12 +9,12 @@ DBML ?= model/data-model-v5.19.dbml
|
||||
|
||||
.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams
|
||||
|
||||
# Единственная зависимость скриптов проверки контракта — jsonschema
|
||||
# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml)
|
||||
venv:
|
||||
uv venv --python 3.11 --allow-existing $(VENV)
|
||||
uv pip install --python $(VENV)/bin/python jsonschema
|
||||
uv pip install --python $(VENV)/bin/python jsonschema pyyaml
|
||||
|
||||
# Примеры contracts/examples/ против трёх схем JSON Schema 2020-12
|
||||
# Примеры contracts/examples/ (и examples/v1/ окна перехода) против схем JSON Schema 2020-12 + перечни кодов форм
|
||||
validate-contracts:
|
||||
$(PY) scripts/validate_contracts.py
|
||||
|
||||
|
||||
@ -169,8 +169,10 @@ advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскак
|
||||
Шаги 2–8 нормализации форм (раскладка, статусы строк, значения, код и наименование, сборка документа, проверки §7,
|
||||
`needs_review`, статус и предупреждения) и их словари живут в отдельном репозитории `arm-statements` пакетом
|
||||
`arm_statements/` (Р-1: платформа не собирает Python-пакеты). В tabular-parser, scan-gateway и arm-core пакет лежит
|
||||
**копией** с тестами, импортируется по имени `arm_statements`; копию раскладывает и проверяет `sync-statements` в docs
|
||||
(`--check`, код 20 при прямой правке; вызывается из `make check-full`, не из `make check`). Сервисы держат только адаптер
|
||||
**копией** (`api/arm_statements/`, маркер версии — `api/arm_statements/VERSION`) с тестами, импортируется по имени
|
||||
`arm_statements`; копию раскладывает и проверяет `sync-statements` в docs (`--check`: 0 — синхронно, 10 — отстала, 20 —
|
||||
прямая правка, 2 — нет checkout; вызывается из `make check-full`, не из `make check`; механика — `dev/checks.md`).
|
||||
Упорядоченные перечни кодов форм — данные `contracts/statement-code-orders.yaml`, в пакете — их байтовая копия. Сервисы держат только адаптер
|
||||
шага 1 (json ИС / лист Excel → `FormCandidate`); ядро считает тем же кодом после правок аналитика и не держит своих
|
||||
копий словаря §7. Словарь «код → наименование» — только в ядре. Правила — `05-domain-rules.md` §11; маркер версии
|
||||
копии — `service.statements_version` результата → `recognition_job.statements_version`. Порядок выкладки — «сначала
|
||||
|
||||
@ -521,7 +521,9 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
`1700 = 1300 + 1400 + 1500`, `1600 = 1700`; ОФР — `2100`, `2200`, `2300`. **Мягкие** (`info`, `ASSUMPTION(OQ-12)`): `2400 =
|
||||
2300 − 2410 + 2420` и варианты ФСБУ 4/2023 (`2400 = 2300 + 2410 + 2460`, `2410 = 2411 + 2412`), `2500 = 2400 + 2510 +
|
||||
2520 + 2530`, «сумма пятизначных подстрок = родитель» (по колонке — только если хотя бы одна подстрока родителя в ней
|
||||
непуста: `amount` не `null`, ноль — значение; AURORA-1527).
|
||||
непуста: `amount` не `null`, ноль — значение; AURORA-1527). Мягкое соотношение на статус и подсветку не влияет, но
|
||||
**сошедшееся точно** (расхождение в пределах допуска ±1, без «округления») подтверждает неуверенные значения своих
|
||||
кодов (Р-21, §11.9): так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки.
|
||||
- **Значение кода** в колонке — сумма по всем строкам `ok` с этим кодом (повтор кода — законный случай, Р-4); строки
|
||||
`detail`, `unreadable`, `missing` в соотношения не входят; нарушение относится ко всем строкам его кодов.
|
||||
`duplicate_line_codes` не выдаётся.
|
||||
@ -543,8 +545,10 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
1200 (ББ) и 2100 / 2200 / 2300 (ОФР), свои равенства по бланку упрощённой формы (приказ 66н, прил. 5;
|
||||
`ASSUMPTION` до первого образца в fixtures): ББ — `1600 = 1150 + 1170 + 1210 + 1230 + 1250` (жёсткое), `1700 = 1300 +
|
||||
1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550` (жёсткое), `1600 = 1700` (жёсткое); ОФР — `2400 = 2110 − 2120 − 2330 +
|
||||
2340 − 2350 − 2410` (жёсткое, `abs_terms` те же). Оба набора — в одном словаре `statement_checks.yaml`. Без
|
||||
упрощённого набора полный дал бы нарушения «пустой цели» на каждой упрощённой форме.
|
||||
2340 − 2350 − 2410` (**мягкое**, как `2400` полного набора: знак 2410 не определён, OQ-12; `abs_terms` те же; решение
|
||||
владельца 18.10.2026, Р-22 / S-5). Оба набора — в одном словаре `statement_checks.yaml`. Без упрощённого набора полный
|
||||
дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (результат 1.x, документ до v5.19) —
|
||||
полный набор.
|
||||
- **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
|
||||
из первых четырёх цифр (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code`.
|
||||
- **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже).
|
||||
@ -552,7 +556,11 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
**Ядро.** При загрузке результата v2.0 ядро §7 **не пересчитывает**: `checks`, `needs_review` и статус документа
|
||||
берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 копией
|
||||
`arm_statements` по текущим суммам (`financial_statement_value.amount` после правок) — с теми же правилами, что в
|
||||
сервисах; при приёме результата 1.x (окно выкладки) — прежним правилом D-6. Межформенная `1370(отчётная дата) − 1370(начало
|
||||
сервисах. Результат 1.x (окно выкладки) и формы, существовавшие до миграции v5.19, `checks` не несут: ядро **один раз**
|
||||
считает §7 копией `arm_statements` (`form_variant = null` — полный набор) — при приёме 1.x и в миграции — и пишет
|
||||
нарушения в `document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из
|
||||
этого расчёта. Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
|
||||
`document_issue` — у любой формы, какой бы версией контракта она ни пришла. Межформенная `1370(отчётная дата) − 1370(начало
|
||||
года) = 2400` — **мягкая**, считается ядром по агрегату формы по коду (§9.5 п. 2) и не блокирует подтверждение: пара —
|
||||
другой документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый
|
||||
предпочтительнее, далее самый поздний `created_at`); считается, только если найдены пара и обе колонки. Результат в
|
||||
@ -561,22 +569,26 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
Показ пятизначных подстрок (AURORA-1527, решение владельца 16.10.2026): ядро отдаёт у каждой строки формы вычисляемое
|
||||
`parent_code` (правило выше); экран показывает подстроки под родителем, пустые подстроки свёрнуты за переключателем.
|
||||
**Акцепт** — по жёстким нарушениям, с комментарием (1–2000 символов), `POST /documents/{id}/statement/checks/accept`;
|
||||
без нарушений — 409 `NOTHING_TO_ACCEPT`. Акцепт **сбрасывается любой правкой формы**. Аудит акцепта —
|
||||
без нарушений — 409 `NOTHING_TO_ACCEPT`. Акцепт **сбрасывается любой правкой формы** (с v2.0 — правкой данных:
|
||||
подтверждение ячейки или кода без изменения его не сбрасывает, §9.5 п. 9). Аудит акцепта —
|
||||
`RECOGNIZED_DATA_EDIT`. Формулы одни для редакций `66n` и `fsbu4` (`ASSUMPTION(OQ-12, OQ-146)`); редакцию ядро определяет
|
||||
по дате периода документа и отдаёт в ответе (`edition`: `66n` при дате ≤ 2024-12-31, `fsbu4` — позже, `null` без даты;
|
||||
OQ-145); тем же правилом сервисы выбирают редакцию упорядоченного перечня кодов (§11.6).
|
||||
OQ-145); тем же правилом сервисы разрешают ничью между редакциями упорядоченного перечня кодов (§11.6, §11.9).
|
||||
|
||||
**Полный состав формы (arm-core#80, решение владельца 09.10.2026).** Экран проверки показывает все строки формы
|
||||
редакции документа (`edition`): справочник `arm-core/api/rules/statement_lines.yaml` (66н и ФСБУ 4/2023, ББ и ОФР, из
|
||||
шаблонов банка `fixtures/statements-xlsx/templates/`). Строки и ячейки, которых нет в источнике, ядро достраивает в
|
||||
ответе (`id = null`, `origin = template`) и в БД не пишет; первая непустая правка создаёт строку
|
||||
(`line_code_original_value = NULL` — признак «не из источника») и ячейку как правку аналитика (аудит
|
||||
`RECOGNIZED_DATA_EDIT`). Пустая добавленная строка не влияет на статус, проверки §7 и автоподтверждение; созданная
|
||||
ответе (`id = null`, `origin = template`) и в БД не пишет; первая непустая правка создаёт строку (признак «не из
|
||||
источника» — до v5.19 `line_code_original_value = NULL`, с v5.19 — `row_index = NULL`) и ячейку как правку аналитика
|
||||
(аудит `RECOGNIZED_DATA_EDIT`). Пустая добавленная строка не влияет на статус, проверки §7 и автоподтверждение; созданная
|
||||
правкой строка участвует в проверках, пока у неё есть хоть одна сумма. Без редакции (нет даты периода) строки не
|
||||
достраиваются. Строки «БАЛАНС» (1600, 1700) на экране выделяются цветом. **С v2.0** (change
|
||||
`unified-statement-normalization`): справочник ядра — только словарь имён (код → наименование по редакциям); порядок и
|
||||
состав редакций ядро берёт из копии упорядоченного перечня кодов `arm_statements` (`sync-statements --check` сверяет, что
|
||||
все коды словаря имён есть в перечне); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не
|
||||
`unified-statement-normalization`): справочник ядра — только словарь имён (код → наименование по редакциям; у кода может
|
||||
быть несколько имён — синонимы и законные повторы кода в бланке, Р-20: 1220 «Налог на добавленную стоимость по
|
||||
приобретенным ценностям» и «НДС, начисленный по отгрузке», 1520 «Кредиторская задолженность» и «Обязательство по
|
||||
аренде»; загрузчик повтор кода не запрещает, сверка наименования — с любым именем кода, шаблонная строка берёт первое);
|
||||
порядок и состав редакций ядро берёт из копии упорядоченного перечня кодов `arm_statements` (данные —
|
||||
`contracts/statement-code-orders.yaml`; `sync-statements --check` сверяет, что все коды словаря имён есть в перечне); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не
|
||||
достраивается; строки `unreadable` / `missing` шаблонной строкой молча не заменяются — показываются отдельно со статусом.
|
||||
Наименование строки из источника хранится как распознано; экран показывает справочное имя по коду, файловое — рядом
|
||||
(Р-3, AURORA-1406 / 1522).
|
||||
@ -686,14 +698,19 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
|
||||
`DOCUMENT_NOT_STATEMENT`.
|
||||
|
||||
**Документ формы при приёме контракта v2.0** (change `unified-statement-normalization`, Р-19; правило выше остаётся для
|
||||
результатов 1.x на окне выкладки). `RECOGNIZED` — только если одновременно: `document.status = done` из результата; в
|
||||
`metrics.warnings` файла нет предупреждений уровня `error` (потерянный json или таблица ИС — решение № 28 среза 3a;
|
||||
`needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены
|
||||
результатов 1.x на окне выкладки). `RECOGNIZED` — только если одновременно: `document.status = done` из результата и он согласован с
|
||||
содержимым документа (нет строк `unreadable` / `missing` с суммой или текстом в значениях, нет строк, значений и полей
|
||||
шапки с `needs_review`, нет `checks.relations_failed` и предупреждений документа уровня `error`; иначе ядро считает
|
||||
документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса); в `metrics.warnings`
|
||||
файла нет предупреждений уровня `error` (потерянный json, таблица ИС или исключение разбора таблицы — решение № 28 среза
|
||||
3a; `needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены
|
||||
`period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence
|
||||
< 100`) распознанное наименование совпало со справочным по коду (Р-16, правило совпадения — §11.9); в файле нет второй
|
||||
формы того же типа и периода (Р-18). Иначе `NEEDS_CHECK`. Проверки §7, `needs_review` и предупреждения сервиса при
|
||||
загрузке не пересчитываются. После правки или акцепта статус пересчитывается общим кодом (§11.9) с подтверждениями
|
||||
аналитика и теми же условиями ядра; `RECOGNIZED` после правки — только вручную, как и прежде.
|
||||
формы того же типа и периода (Р-18; «того же периода» — та же пара `period_year`, `period_quarter`, формы без квартала не
|
||||
считаются). Иначе `NEEDS_CHECK`. Проверки §7, `needs_review` и предупреждения сервиса при загрузке не пересчитываются.
|
||||
После правки или акцепта статус пересчитывается общим кодом (§11.9) с подтверждениями аналитика и теми же условиями ядра;
|
||||
`RECOGNIZED` после правки — только вручную, как и прежде. Документ, принятый по 1.x, после правки пересчитывается так же
|
||||
(общим кодом), его `LEGACY_WARNING` и `MAPPING_WARNING` — `info` и статус не держат.
|
||||
|
||||
### 9.4 Задание (`recognition_status`): queued → published → processing → done | needs_check | error | timeout → dlq;
|
||||
replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 20 мин (решение владельца 09.10.2026; note DBML «5 мин» — OQ-159), парсер 5 мин.
|
||||
@ -733,15 +750,29 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
формы из разных файлов не склеиваются (OQ-140), строки без кода не хранятся (OQ-36, OQ-141, OQ-149). Статус документа формы —
|
||||
§9.3.
|
||||
**Формы по контракту v2.0** (change `unified-statement-normalization`; правила выше — для результатов 1.x на окне
|
||||
выкладки, у них поля v2.0 заполняются по умолчанию: `line_status = ok`, `row_index` по порядку, `text =
|
||||
string_value`, `needs_review` значений — по прежнему правилу «правлено или уверенность 100 и `is_recognized`»).
|
||||
Результат пишется как пришёл, без повторной дедупликации: каждая строка — `financial_statement_line` с `row_index`,
|
||||
выкладки, у них поля v2.0 заполняются по умолчанию: `line_status = ok`, `row_index` по порядку строк результата с 0,
|
||||
`line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений — по
|
||||
прежнему правилу «правлено или уверенность 100 и `is_recognized`», `form_variant = null` (§7 — полный набор),
|
||||
`header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз копией `arm_statements` и пишет в
|
||||
`document_issue` (`source = core`, §7 «Ядро»); предупреждения маппинга 1.x по документу (повтор кода, значение вне
|
||||
колонок) — `document_issue` `MAPPING_WARNING` `info` (`source = core`; статус при приёме — по D-6, после правки они
|
||||
статус не держат); строки `metrics.warnings` 1.x — в `recognition_job.metrics.warnings` структурами `{code:
|
||||
"LEGACY_WARNING", severity: "info", message}` уровня файла; `statements_version` задания — `null`, и
|
||||
`STATEMENTS_VERSION_MISMATCH` у такого документа не пишется).
|
||||
Результат пишется как пришёл, без повторной дедупликации: каждая строка — `financial_statement_line` с `row_index`
|
||||
(у строки из результата всегда не `null`: `row_index IS NULL` — признак строки, созданной правкой),
|
||||
`source_ref` (`ref`, уникален в форме), `line_status`, `line_code` (`null` у `unreadable` / `missing` / `detail`),
|
||||
`line_code_text`, `parent_row_index` (родитель `detail`), `needs_review`; повтор кода — законный случай, хранятся все
|
||||
`line_code_original_value` (= `line_code` результата; правками не меняется), `line_code_text`, `parent_row_index`
|
||||
(родитель `detail`), `needs_review`; повтор кода — законный случай, хранятся все
|
||||
строки (Р-4; строки без кода хранятся — Р-8, отменяет OQ-36 / OQ-141). Значения — с `source_text` (`values[].text`) и
|
||||
`needs_review`; колонки — с `period_kind` и `label_confidence`; шапка — с `form_variant` и `needs_review_fields`;
|
||||
`checks` и предупреждения документа — в `document_issue` (источник `service`) вместе с проверками ядра уровня `info`
|
||||
(`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`); версия общего кода —
|
||||
`needs_review`; колонки — с `period_kind` и `label_confidence`; шапка — с `form_variant`, `needs_review_fields` и
|
||||
`header_source` (`<поле>_text` и `<поле>_confidence` всех полей; `*_original_value` — как прежде: `inn`,
|
||||
`report_date_text`, `units_text`; `inn_confidence` результата, `organization_name_original_value` =
|
||||
`organization_name`); `checks`
|
||||
и предупреждения документа — в `document_issue` (источник `service`; нарушение — `kind = relation`, `code =
|
||||
RELATION_FAILED`, `error` или `kind = soft`, `code = RELATION_SOFT`, `info`, с `codes`, `relation`, `expected`,
|
||||
`actual`, `column_id`; предупреждение о столбце вне колонок формы — с `column_index`) вместе с проверками ядра уровня
|
||||
`info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`); версия общего кода —
|
||||
`recognition_job.statements_version`. Каждый `BALANCE` / `INCOME` становится отдельным документом с `file_ordinal`:
|
||||
вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не
|
||||
автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
@ -774,9 +805,11 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`CHECKS_NOT_PASSED` (список нарушений — в `detail.failures`: `column_index`, `relation`, `expected`, `actual`); порядок
|
||||
409: `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` → `DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → `CHECKS_NOT_PASSED`.
|
||||
**С v2.0** (Р-13, Р-18) порядок: `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` → `DOCUMENT_NOT_CONFIRMABLE` →
|
||||
`PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing` с непустой суммой; `detail.lines[]
|
||||
{line_id, row_index, line_code_text}`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле уже подтверждена другая
|
||||
форма того же типа и периода; `detail.document_id`; между файлами — автозамена п. 3) → `CHECKS_NOT_PASSED`.
|
||||
`PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing`, у которых хотя бы в одной ячейке
|
||||
непустая сумма или нераспознанный текст — `amount` или `string_value` не `null`; `detail.lines[] {line_id, row_index,
|
||||
line_code_text}`; строки без значений не мешают) → `DUPLICATE_FORM` (в файле уже подтверждена другая форма того же
|
||||
типа и периода — та же пара `period_year`, `period_quarter`; `detail.document_id`; между файлами — автозамена п. 3) →
|
||||
`CHECKS_NOT_PASSED` (нарушения — записи `document_issue` вида `relation` без акцепта, у любой формы).
|
||||
Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не допускает).
|
||||
При подтверждении пишется `financial_statement_meta.reviewed_by/at`. Несовпадение контрагента (`MISMATCH`) и низкая
|
||||
уверенность значений подтверждение **не блокируют** (как у ОСВ): защита — статус `NEEDS_CHECK` и подсветка. Мягкие проверки
|
||||
@ -809,7 +842,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
Правятся **суммы** ячеек (`values[] {line_id, column_id, amount}`) и три поля шапки: `inn` (10 или 12 цифр, иначе 422
|
||||
`INVALID_INN`, в том числе `null` — очистить ИНН нельзя; пересчитывается `counterparty_check`), `report_date` (пересчитываются `period_year`, `period_quarter`,
|
||||
`reporting_date` по правилу периода п. 2) и `units` (непустая строка до 50 символов). Код и название строки, добавление и
|
||||
удаление строк и колонок не правятся. **Только до подтверждения:** документ `CONFIRMED` — 409 `DOCUMENT_READONLY`
|
||||
удаление строк и колонок не правятся (до v2.0; с v2.0 код и статус строки, дата колонки и наименование организации
|
||||
правятся — см. ниже, название строки и удаление строк и колонок — по-прежнему нет). **Только до подтверждения:** документ `CONFIRMED` — 409 `DOCUMENT_READONLY`
|
||||
(исправление подтверждённой формы — новым файлом через автозамену, п. 3); файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`; не
|
||||
форма — 409 `DOCUMENT_NOT_STATEMENT`; ячейка не этого документа — 422 `UNKNOWN_CELL`. Исходное сохраняется: у значения при
|
||||
первой правке `original_amount` = прежняя сумма, `is_modified = (amount ≠ original_amount)` (возврат к исходному снимает
|
||||
@ -819,25 +853,39 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
другого файла ЮЛ, привязывает документ к версии лишь при подтверждении. Порядок блокировок — как у подтверждения (файл →
|
||||
документ).
|
||||
**С v2.0** (Р-6, Р-13; change `unified-statement-normalization`) PATCH дополнительно принимает:
|
||||
`values[] {line_id, column_id, confirmed: true}` — подтвердить ячейку без изменения (`is_confirmed`,
|
||||
`confirmed_by/at`, `needs_review = false`, `is_modified` не ставится); `columns[] {column_id, period_date}` — дата
|
||||
колонке без подписи или с неуверенной подписью (пересчитывает `is_current` и период документа); `lines[] {line_id,
|
||||
line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить расшифровкой; `lines[]
|
||||
{line_id, confirmed: true}` — подтвердить неуверенный код. Переходы: `unreadable` / `missing` / `detail` → `ok`
|
||||
назначением кода; `unreadable` / `missing` / `ok` → `detail` отметкой; `ok` → `ok` сменой кода (прежний — в
|
||||
`line_code_original_value`). Назначенный или подтверждённый код — `line_code_confidence = 100`, `line_code_modified`,
|
||||
`needs_review` строки = `false`; у `detail` родитель пересчитывается как ближайшая строка `ok` выше по `row_index`. Код
|
||||
— 4–5 цифр (422 `INVALID_LINE_CODE`), может совпадать с существующим (Р-4); незнакомый словарю — `info`; шаблонная
|
||||
строка с тем же кодом, созданная правкой с суммами, — 409 `LINE_EXISTS`, без сумм — удаляется; `line_status = ok`
|
||||
без кода — 422 `LINE_CODE_REQUIRED`; элемент без действия или с несколькими — 422 `INVALID_LINE_PATCH`; чужая строка —
|
||||
422 `UNKNOWN_LINE`, чужая колонка — 422 `UNKNOWN_COLUMN`; правка суммы по `line_code` при наличии строки `ok` с этим
|
||||
кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления строк нет (первичка не мутируется, `detail` — способ вывести
|
||||
строку из сумм). Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`. Правка
|
||||
`header.report_date` пересчитывает `period_end` колонок с `period_kind = relative:*` общим кодом (`null` — `null`).
|
||||
`header.organization_name` (непустая строка до 1000 символов; `organization_name_modified = true`,
|
||||
`organization_name_original_value` не меняется; пересчитывается `counterparty_check`; снимает `NO_ORGANIZATION_NO_INN`;
|
||||
аудит — как у ИНН); `header.report_date`, равный текущему, у поля из `needs_review_fields` — подтверждение даты без
|
||||
изменения: поле уходит из `needs_review_fields`, снимается `REPORT_DATE_UNCERTAIN`, аудит (у бланка 66н только с
|
||||
относительными подписями неуверенная дата иначе не подтверждается ничем, §11.9);
|
||||
`values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения (`is_confirmed`,
|
||||
`confirmed_by/at`, `needs_review = false`, `is_modified` не ставится) или снять подтверждение (`is_confirmed = false`,
|
||||
`needs_review` пересчитывается); `columns[] {column_id, period_date}` — дата колонке без подписи или с неуверенной
|
||||
подписью (обязательная дата, `null` — 422 ошибка валидации; пересчитывает `is_current` и период документа);
|
||||
`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` сменой кода. Обратного перехода `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` выше по `row_index`. Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
|
||||
совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL`
|
||||
(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она
|
||||
удаляется (её `id` убирается из `document_issue.line_ids`), назначенная строка источника занимает её место. Строки
|
||||
источника (`row_index` не `null`) с тем же кодом не удаляются и не мешают — повтор кода законен. Элемент без действия
|
||||
или с несколькими — 422 `INVALID_LINE_PATCH`; чужая строка — 422 `UNKNOWN_LINE`, чужая колонка — 422 `UNKNOWN_COLUMN`;
|
||||
правка суммы по `line_code` при наличии строки `ok` с этим кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления
|
||||
строк источника нет (первичка не мутируется, `detail` — способ вывести строку из сумм). Строки, созданные правкой:
|
||||
`row_index = null`, `source_ref = null`, `line_status = ok`, `line_code_original_value = null`. Правка
|
||||
`header.report_date` пересчитывает `period_date` колонок с `period_kind = relative:*` общим кодом (`null` — `null`).
|
||||
После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue`
|
||||
видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица
|
||||
§11.9). Расхождение `statements_version` задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH`. Один PATCH —
|
||||
§11.9). **Акцепт** сбрасывается правкой данных — суммы, кода или статуса строки, даты колонки, поля шапки;
|
||||
подтверждение ячейки или кода (`confirmed`) суммы не меняет и акцепт не сбрасывает. Расхождение `statements_version`
|
||||
задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH —
|
||||
один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям).
|
||||
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
|
||||
объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует
|
||||
@ -1064,8 +1112,9 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
**Ошибки набора.** Ошибочный json (`parse_error`, непустой `error.code`, корень не объект) при хотя бы одном успешном
|
||||
соседе — предупреждение файла `JSON_FAILED` (`error`, `locator = file:<имя json>`, в тексте код и сообщение ИС; решение
|
||||
№ 28 среза 3a: потерянный документ, например ОФР, не должен остаться незамеченным); все json ошибочны — конверт `error`
|
||||
с кодом первого. Исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`) документа, остальные таблицы
|
||||
разбираются; ни одной таблицы не разобрано — `error RECOGNITION_FAILED`.
|
||||
с кодом первого. Исключение при разборе таблицы — предупреждение **файла** `TABLE_PARSE_ERROR` (`error`, `locator =
|
||||
file:<имя json>;table:<i>`; документа у такой таблицы нет), остальные таблицы разбираются; как и `JSON_FAILED`, оно
|
||||
блокирует `RECOGNIZED` всех документов файла (§9.3); ни одной таблицы не разобрано — `error RECOGNITION_FAILED`.
|
||||
|
||||
**Перенос из старого ядра.** Источник очистки и разборов — модуль `DKB_ARMKA` `api/src/services/ofr_result_conversion/`
|
||||
(`utils.py`, `date_utils.py`, `consts.py`; локально — `reference-old-arm/api/src/core/PDFWorker/`); решение владельца
|
||||
@ -1090,14 +1139,17 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
иначе текст с признаком ОПФ строкой выше метки), ИНН 10/12 цифр у метки «ИНН» (ИНН аудитора не берётся: строки после
|
||||
метки «аудитор» исключены), ОКПО, ОКОПФ, ОКФС, ОКЕИ, адрес, единицы, дата (подзаголовок формы или тройка «число | месяц
|
||||
| год» у метки «Дата»; для ОФР — конец периода). Шапка другого листа не используется. Найденное поле — `Cell` с
|
||||
`confidence = 100`, не найденное — `null`.
|
||||
`confidence = 100`, не найденное — `null`. ОКЕИ — поле `okei` шапки контракта.
|
||||
3. **Ячейки**: `text` — как показывает числовой формат ячейки, `value` — число по формату (tp#42, OQ-138 пересмотрен
|
||||
15.10.2026): масштабирующие запятые (`#,##0,` → тысячи), число знаков после точки, секции и условия (`[=0]"-"`,
|
||||
`[=-646532]"(647)"`; литерал условной секции разбирается как текст), округление half-up, знак — от сырого значения,
|
||||
условия сравниваются с точностью 15 значащих цифр; `General` / пустой формат — значение как в ячейке; формат, который не
|
||||
удалось разобрать (`%`, экспонента, дробь, даты, `@`, неизвестные знаки вне кавычек, больше четырёх секций, условие не
|
||||
по шаблону, результат вне `numeric(18,2)`), — значение как в ячейке и одно `NUMBER_FORMAT_UNPARSED` (`info`) на документ
|
||||
с перечнем форматов; xlsb — стили не читаются, `XLSB_NO_FORMATS` (`info`). `ref = <лист>!<A1>`, `confidence = 100`.
|
||||
с перечнем форматов; xlsb — стили не читаются, `XLSB_NO_FORMATS` (`info`). `ref = <лист>!<A1>` по правилу Excel: имя
|
||||
листа берётся в одинарные кавычки, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры, апостроф
|
||||
внутри имени удваивается (`Баланс!C27`, `'Бухгалтерский баланс. Лист 1'!C14`; решение владельца 18.10.2026, Р-22 /
|
||||
S-15); `confidence = 100`.
|
||||
4. **Склейка листов-продолжений — до раскладки.** Строки продолжения дописываются в конец сетки открытой формы по
|
||||
маркеру «Форма … с.2» или по позиции листа; граница листа — в `segment_breaks` (всегда граница сегмента раскладки).
|
||||
Тип маркера сверяется с типом формы: расхождение — отдельный кандидат и `CONTINUATION_TYPE_CONFLICT` (`error`). Лист без
|
||||
@ -1116,7 +1168,10 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
|
||||
**Шапка — одно правило разбора** по `Cell.text`: ИНН — только цифры, **без замены двойников** (иначе «3|1 21 2|» из aep_3
|
||||
даёт лишние единицы и `MISMATCH` с ЮЛ), длина 10 или 12, иначе `null`; дата отчёта — словарь периодов §11.3 п. 3;
|
||||
единицы — словарь единиц. Сырой текст и уверенность каждого поля — в контракт (`<поле>_text`, `<поле>_confidence`).
|
||||
единицы — словарь единиц пакета `statement_units.yaml` (написания «тыс. руб.», «в тыс. рублей», «руб.», «млн руб.» → `тыс.
|
||||
руб.` / `руб.` / `млн руб.`); единицы не найдены по тексту, а ОКЕИ есть — по коду ОКЕИ тем же словарём (384 → `тыс. руб.`,
|
||||
383 → `руб.`, 385 → `млн руб.`); ОКЕИ — только цифры. Сырой текст и уверенность каждого поля — в контракт
|
||||
(`<поле>_text`, `<поле>_confidence`); ядро хранит их в `financial_statement_meta.header_source`.
|
||||
|
||||
### §11.3 Шаг 2 — раскладка таблицы
|
||||
|
||||
@ -1161,7 +1216,8 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
«2025 03 31», «30/09/25»; иначе склеенные цифры — 8 → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный), 4 → год (31.12,
|
||||
1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается; двузначный год → 20YY; квартал / полугодие / N месяцев → последний
|
||||
день последнего месяца; не дата → `null`). Строки `test_dates.py::test_parses_real_forms` шлюза — обязательные тесты
|
||||
словаря. Результат — `period_end` и `period_kind`:
|
||||
словаря. Результат — `period_date` (конец периода колонки; во внутренних типах пакета может называться иначе, в
|
||||
контракте и БД — `period_date`) и `period_kind`:
|
||||
- абсолютные подписи → `absolute`;
|
||||
- относительные подписи бланка 66н без впечатанных дат (Р-6): «На отчётную дату», «За отчётный период» →
|
||||
`relative:current` (= эффективная дата); «На 31 декабря предыдущего года» → `relative:prev_year_end` (31.12 года
|
||||
@ -1170,7 +1226,7 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
- подписи нет → `none`.
|
||||
|
||||
**Эффективная дата документа** — дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок (та же формула,
|
||||
что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_end = null`, вид
|
||||
что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_date = null`, вид
|
||||
остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в
|
||||
дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря
|
||||
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка).
|
||||
@ -1179,26 +1235,40 @@ json>;table:<i>` (как в golden-эталонах); шапка json копир
|
||||
заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки
|
||||
заголовка (титульный блок «Дата 30 | 9 | 2025»); в сегменте без заголовка роли `title` нет, строки выше первого кода с
|
||||
содержимым идут в данные. Заголовка нет ни у одного сегмента → колонки значений первого сегмента — столбцы правее кода,
|
||||
где в строках с кодами числа, `label` пустая, `period_end = null`, `period_kind = none`, одно `HEADER_ROW_NOT_FOUND`
|
||||
где в строках с кодами числа, `label` пустая, `period_date = null`, `period_kind = none`, одно `HEADER_ROW_NOT_FOUND`
|
||||
(`error`; прежний синтез шапки шлюза и «рулинг 06.10.2026» заменены этим правилом). Заголовок есть хотя бы у одного
|
||||
сегмента — остальные берут его колонки без предупреждения (листы 2–5 ББ НПЗ без шапки остаются `done`).
|
||||
5. **Строка нумерации** «1 2 3 4 5» — строка, где не меньше трёх непустых ячеек, каждая — целое число, и числа идут
|
||||
подряд с 1 слева направо — роль `numbering`, в данные не попадает (AURORA-1517).
|
||||
5. **Строка нумерации** «1 2 3 4 5» — роль `numbering`, в данные не попадает (AURORA-1517). Строка — нумерация, если в
|
||||
ней не меньше трёх непустых ячеек, каждая после замены букв-двойников (§11.3, «1 2 З 4» → 1 2 3 4) — целое число,
|
||||
числа слева направо возрастают на 1, начинаются с 1 или 2 и допускают **один пропуск** («1 2 4 5», «2 3 4 5» без
|
||||
первой ячейки; в Excel Межениновской в столбце кодов строк нумерации стоит «2»). Решение владельца 18.10.2026 (Р-22,
|
||||
S-13): иначе сбой OCR в нумерации превращает её в строку `unreadable` с «суммами» и блокирует подтверждение.
|
||||
Запись нумерации в столбце кодов («2», «3») кодом не считается — вне кодовых диапазонов.
|
||||
6. **Подпись без периода** (Р-6): период по словарю (абсолютный или относительный) → колонка с датой; подпись из
|
||||
словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск, `COLUMN_SKIPPED` (`info`);
|
||||
иначе, если в столбце есть числа в строках с кодами (OCR-мусор «| | г» с уверенностью 17, обрезанная или самодельная
|
||||
подпись), — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, `COLUMN_WITHOUT_DATE`
|
||||
подпись), — колонка остаётся с подписью как есть, `period_date = null`, `period_kind = none`, `COLUMN_WITHOUT_DATE`
|
||||
(`error`, колонка); аналитик ставит дату через PATCH (§9.5 п. 9); иначе пропуск. Колонка без даты не мешает периоду
|
||||
формы (эффективная дата), но межформенная проверка и гудвилл «на отчётную дату» по ней не считаются, пока дата не
|
||||
проставлена. Уверенность подписи — `label_confidence` (Excel — 100, подписи нет — `null`).
|
||||
7. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с
|
||||
наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца,
|
||||
пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||||
8. **Номер колонки** (`columns[].column_index`, `values[].column_index`, `column_index` предупреждений) — номер столбца
|
||||
в сетке кандидата шага 1: у OCR — `column_index` ИС как есть (не обязательно подряд: 2, 4, 5), у Excel — номер
|
||||
столбца листа с 0 (столбец D — 3). Номер уникален в документе; колонки сегментов без шапки сопоставляются с колонками
|
||||
первого сегмента по порядку и несут их номера. Предупреждение о столбце, которого нет среди `columns[]`
|
||||
(`COLUMN_SKIPPED`), несёт номер этого столбца в сетке; ядро хранит его в `document_issue.column_index` (колонки формы
|
||||
нет — `column_id = null`).
|
||||
|
||||
### §11.4 Шаг 3 — отбор строк данных и статусы
|
||||
|
||||
Строка данных — строка сетки с ролью `data`, у которой есть содержимое: код, число или непустой текст в колонке значений
|
||||
с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3). У строки: `row_index` (номер в документе с
|
||||
Строка данных — строка сетки с ролью `data`, у которой есть **содержимое**: в ячейке кода есть хотя бы одна цифра
|
||||
(разобранный код или мусор вроде «44 30\nI |» aep_6), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`),
|
||||
или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3). Строка только с текстом
|
||||
наименования — не строка данных (заголовок раздела). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
|
||||
— строка данных: документ `needs_check` (не все строки `ok` / `detail`), но подтверждение она не блокирует
|
||||
(`LINES_UNRESOLVED` — только строки со значениями, §9.5 п. 4). У строки: `row_index` (номер в документе с
|
||||
0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||||
ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:**
|
||||
@ -1280,19 +1350,24 @@ parent_ref, needs_review, values}`. `line_code` — разобранный ко
|
||||
**без срезания до 99 по справочнику**; `line_name` — распознанный текст наименования как есть, с уверенностью источника
|
||||
(Р-3: имя из справочника шлюза `api/rules/statement_lines.yaml` отменено, файл удаляется; соответствие «код →
|
||||
наименование» — только в ядре, §7 «Полный состав формы»). **Редакция** (`66n` / `fsbu4`) в сервисах нужна только для
|
||||
упорядоченного перечня кодов (§11.9) и выбирается по эффективной дате (`fsbu4` — после 2024-12-31, как `edition` ядра,
|
||||
OQ-145); без даты проверяются обе редакции. Прежний выбор редакции шлюза для имён (по дате, по кодам, «редакция не
|
||||
упорядоченного перечня кодов (§11.9): проверяются **обе** редакции, берётся та, где подпоследовательность длиннее, а при
|
||||
равенстве — по эффективной дате (`fsbu4` — после 2024-12-31, как `edition` ядра, OQ-145): в 2025 году формы сдают и на
|
||||
бланке 66н (Межениновская, 3 кв. 2025), и выбор только по дате не подтвердил бы её код 1120 (в перечне `fsbu4` его нет). Прежний выбор редакции шлюза для имён (по дате, по кодам, «редакция не
|
||||
определена» / «не заполнена» → `needs_check`) отменён.
|
||||
|
||||
### §11.7 Шаг 6 — тип формы, вариант, документ
|
||||
|
||||
1. **Тип** — большинство кодов строк `ok` в отрезке ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений →
|
||||
`DISCLOSURE`; совпало с `type_hint` или `type_hint = null` — тип по кодам; противоречит — тип по кодам и `TYPE_CONFLICT`
|
||||
(`error`); кодов нет — `type_hint`. Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`,
|
||||
иначе `needs_check` с `CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым —
|
||||
предупреждение файла `TABLE_LOST` (`error`), у Excel — лист «прочее», `SHEET_OTHER` (`info`). `DISCLOSURE` по `type_hint`
|
||||
— шаги 2–7 не выполняются; `DISCLOSURE` по кодам — строки и колонки отбрасываются (ядро строки пояснений не хранит),
|
||||
проверки не считаются, документ `done`.
|
||||
(`error`); кодов нет — `type_hint`. Поиск столбца кодов (шаг 2 п. 1) выполняется **всегда**, в том числе при
|
||||
`type_hint = DISCLOSURE`: подпись «Пояснения» или имя листа «Расшифровка» над таблицей с кодами 1110–1700 не должны
|
||||
превратить баланс в пустое пояснение — большинство кодов в отрезке ББ или ОФР даёт тип по кодам и `TYPE_CONFLICT`
|
||||
(`error`). Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с
|
||||
`CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым — предупреждение **файла**
|
||||
`TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`), у Excel — лист «прочее», предупреждение **файла**
|
||||
`SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа в обоих случаях нет. `DISCLOSURE` (по `type_hint` без
|
||||
кодов форм или по кодам прочих форм) — шаги 3–7 не выполняются, `columns` и `lines` — пустые массивы (ядро строки
|
||||
пояснений не хранит), `checks` нет, документ `done`.
|
||||
2. **Вариант формы** `form_variant` — только по структуре кодов (ОКУД 0710096 — код КНД комплекта, на бланках
|
||||
0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или
|
||||
2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` / `OTHER` — `null`. Шаг 7 и перечень
|
||||
@ -1310,58 +1385,84 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
|
||||
### §11.9 Шаг 8 — `needs_review`, статус, предупреждения
|
||||
|
||||
**Подтверждение проверкой** (Р-5, Р-7, Р-11, Р-17). Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет.
|
||||
**Подтверждение проверкой** (Р-5, Р-7, Р-11, Р-17, Р-21). Проверка §7 — арбитр, уверенность — подсказка там, где
|
||||
проверки нет.
|
||||
|
||||
- Значение **нуждается в покрытии**, если `confidence < 100` (Р-5: у ИС уверенность дискретная — 100, ~80, 71, 67, 60,
|
||||
50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false`
|
||||
(грязный текст, замена двойников, разлепленная строка). Чистые значения в покрытии не нуждаются.
|
||||
- Значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось (в
|
||||
пределах допуска или округления), и `amount` значения не `null`.
|
||||
- Значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
|
||||
посчитано и сошлось: **жёсткое** — в пределах допуска ±1 или округления 0,001 %; **мягкое** — только точно, в пределах
|
||||
допуска ±1, без «округления» (Р-21, решение владельца 18.10.2026). Мягким покрываются строки, у которых жёстких
|
||||
соотношений нет: 2400, 2410, 2411, 2412, 2460 (ОФР полного набора; `2400` упрощённого — тоже мягкое, S-5), 2510, 2520,
|
||||
2530, 2500 (`2500 = 2400 + 2510 + 2520 + 2530`), пятизначные подстроки («сумма подстрок = родитель»).
|
||||
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма записи знака (скобки, минус, без знака —
|
||||
по `text`) совпадает с формой у большинства членов `abs_terms` той же колонки с непустым `amount`, `is_recognized =
|
||||
true` и без правки аналитика; не совпадает или сравнивать не с чем — не подтверждён. «(7744536}» подтверждается, если
|
||||
2210 и 2220 той же колонки в скобках; «{38378)» при остальных в скобках — подсвечивается.
|
||||
true` и без правки аналитика; не совпадает или сравнивать не с чем — не подтверждён (то же правило — при покрытии
|
||||
мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и 2220 той же
|
||||
колонки в скобках; «{38378)» при остальных в скобках — подсвечивается.
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой
|
||||
текст и уверенность остаются в подсказке ячейки.
|
||||
текст и уверенность остаются в подсказке ячейки. Мягкое соотношение, не сошедшееся точно, подсветки не даёт.
|
||||
|
||||
Не покрыто проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя,
|
||||
`DISCLOSURE`, колонки только с мягкими соотношениями. Риск двух взаимно компенсирующих ошибок в одном соотношении
|
||||
принимается. Закрывает AURORA-1524 без ущерба для 1519.
|
||||
Не покрыто проверками и остаётся на уверенности: строки 29xx (прибыль на акцию), 2420 и 2530 вне сошедшихся мягких
|
||||
соотношений, детализация без строки-родителя, `DISCLOSURE`, колонки, где код не участвует ни в одном посчитанном
|
||||
соотношении. Риск двух взаимно компенсирующих ошибок в одном соотношении принимается. Закрывает AURORA-1524 без ущерба
|
||||
для 1519.
|
||||
|
||||
**Код строки** (Р-5, Р-12, Р-16). Код неуверенный — `line_code_confidence < 100` или заменён двойник; структурная
|
||||
**Код строки** (Р-5, Р-12, Р-16, Р-20). Код неуверенный — `line_code_confidence < 100` или заменён двойник; структурная
|
||||
проверка — **только для неуверенных кодов** (у Excel и чистых OCR-кодов расхождение с перечнем — `info` ядра
|
||||
`CODE_NOT_IN_EDITION`, не `needs_check`: пятизначные подстроки 1С и нестандартные коды клиента не блокируют
|
||||
автоподтверждение). В пакете — **упорядоченный перечень кодов** каждой формы по редакциям (66н, ФСБУ 4) и вариантам, без
|
||||
наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100); источник — объединение кодов
|
||||
шаблонов банка (словарь ядра) и кодов печатных форм 1С по редакции (1105, 1530, 2420, 2460, 2930 и др.). Алгоритм:
|
||||
четырёхзначные коды строк `ok` документа — последовательность; для каждой проверяемой редакции — наибольшая
|
||||
подпоследовательность, согласованная с порядком бланка; редакция — с более длинной (при равенстве — по эффективной дате);
|
||||
уверенные коды — подтверждённые якоря; при нескольких наибольших подпоследовательностях подтверждены только общие коды.
|
||||
Пятизначный код подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, у которого
|
||||
хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии. Неподтверждённый неуверенный код — `needs_review`
|
||||
строки, документ `needs_check`. Подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120)
|
||||
структура не ловит — это закрывает ядро: для строк с неуверенным кодом условие `RECOGNIZED` включает совпадение
|
||||
распознанного наименования со справочным (§9.3). **Совпадение наименований:** обе строки — нижний регистр, `ё` → `е`,
|
||||
всё кроме букв и цифр → пробел, пробелы схлопываются; совпало, если строки равны, или более короткая (≥ 5 символов)
|
||||
входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с любым справочным именем кода в любой редакции словаря
|
||||
ядра; кода нет в словаре или наименование пусто — не совпало; несовпадение — `NAME_MISMATCH` (`info`) у строки. По
|
||||
наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня —
|
||||
`contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`):
|
||||
объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215,
|
||||
1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; код без образца в перечень не входит (кандидаты — OQ-168),
|
||||
упрощённые варианты — по бланку (OQ-166). **Законные повторы кода** (Р-20, решение владельца 18.10.2026) — часть перечня
|
||||
(`repeats`): у `fsbu4` — 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощённой
|
||||
системе налогообложения» (печатная форма 1С, AURORA-1431), у `66n` — раздел III некоммерческой организации после раздела
|
||||
III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность; для **каждой** редакции перечня
|
||||
(66н и ФСБУ 4 — всегда обе: Межениновская за 3 кв. 2025 сдана на бланке 66н) — наибольшая подпоследовательность,
|
||||
согласованная с порядком перечня варианта формы документа (повтор допустим столько раз, сколько код стоит в перечне);
|
||||
редакция — с более длинной (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); уверенные коды —
|
||||
подтверждённые якоря; при нескольких наибольших подпоследовательностях подтверждены только общие коды. Пятизначный код
|
||||
подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, которого **нет в перечне**
|
||||
(или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии (Р-16);
|
||||
повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `needs_review` строки,
|
||||
документ `needs_check`. Подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120) структура не
|
||||
ловит — это закрывает ядро: для строк с неуверенным кодом условие `RECOGNIZED` включает совпадение распознанного
|
||||
наименования со справочным (§9.3).
|
||||
|
||||
**Совпадение наименований** (ядро; S-4 с оговорками ревью, решение владельца 18.10.2026): обе строки — нижний регистр,
|
||||
`ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются; совпало, если строки равны, или более короткая (≥ 5
|
||||
символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым** справочным именем кода в любой
|
||||
редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы — Р-20). Исключение: если хотя бы
|
||||
одно из сравниваемых имён начинается с «итого по разделу», совпадение — **только равенство** («итого по разделу i»
|
||||
входит в «итого по разделу ii» и похоже на него — без исключения подмена 1100 ↔ 1200 ↔ 1300 не ловилась бы именем).
|
||||
Кода нет в словаре или наименование пусто — не совпало; несовпадение — `NAME_MISMATCH` (`info`) у строки. По
|
||||
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя.
|
||||
|
||||
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок;
|
||||
иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал
|
||||
бы неверный период и автоподтверждение. ИНН и организация с уверенностью ниже 100 — `info` (их сверяет ядро: ИНН —
|
||||
`MISMATCH` с ЮЛ, организация — по алиасам), в `needs_review_fields` не попадают; единицы и прочие поля на статус не
|
||||
влияют.
|
||||
бы неверный период и автоподтверждение. **У бланка 66н только с относительными подписями** («На отчётную дату», …)
|
||||
абсолютных дат колонок нет, и неуверенная дата отчёта структурой не подтверждается никогда — такой документ всегда
|
||||
`needs_check` с `REPORT_DATE_UNCERTAIN`; аналитик сверяет дату с бланком и подтверждает её (PATCH `header.report_date`
|
||||
тем же значением или исправленным, §9.5 п. 9). ИНН и организация с уверенностью ниже 100 — `info` (их сверяет ядро: ИНН
|
||||
— `MISMATCH` с ЮЛ, организация — по алиасам), в `needs_review_fields` не попадают (`needs_review_fields` — поля, которые
|
||||
держат документ в `needs_check`; подсветка ИНН и организации на экране — по предупреждениям с `field`; S-10, решение
|
||||
владельца 18.10.2026); единицы и прочие поля на статус не влияют.
|
||||
|
||||
**Предупреждение** — структура `{code, severity, message, line_refs?, column_index?, field?}`; уровень файла
|
||||
(`metrics.warnings`) — ещё `locator?`. Место — `locator` документа, не текст. Принцип: `error` — только то, что меняет
|
||||
данные или их полноту; `error` → `needs_check`, `info` на статус не влияет.
|
||||
(`metrics.warnings`) — ещё `locator?`. Место — `locator` документа (у предупреждения файла — его `locator`), не текст.
|
||||
`line_refs` — только `ref` строк из `lines[]` (о строках ролей `title` / `header` / `numbering` — в `message`);
|
||||
`column_index` — номер столбца сетки (§11.3 п. 8). Принцип: `error` — только то, что меняет данные или их полноту;
|
||||
`error` → `needs_check`, `info` на статус не влияет. Предупреждение уровня файла с `error` блокирует `RECOGNIZED` всех
|
||||
документов файла (§9.3).
|
||||
|
||||
| Код | Уровень | Когда | Чем снимается в ядре |
|
||||
|---|---|---|---|
|
||||
| `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка |
|
||||
| `TABLE_PARSE_ERROR` | error | исключение при разборе таблицы ИС | перезагрузка |
|
||||
| `TABLE_PARSE_ERROR` | error, файл | исключение при разборе таблицы ИС (`locator` таблицы; документа нет) | перезагрузка |
|
||||
| `TABLE_LOST` | error, файл | таблица ИС с содержимым без столбца кодов и без типа | перезагрузка |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | форма `BALANCE` / `INCOME` без столбца кодов | перезагрузка |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в т. ч. лист-продолжение) | перезагрузка |
|
||||
@ -1374,7 +1475,7 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` | перезагрузка |
|
||||
| `NO_ORGANIZATION_NO_INN` | error | нет ни организации, ни ИНН | PATCH `header.inn` / `organization_name` |
|
||||
| `PERIOD_UNKNOWN` | error | нет ни даты отчёта, ни абсолютных дат колонок | PATCH `header.report_date` |
|
||||
| `REPORT_DATE_UNCERTAIN` | error | неуверенная дата отчёта не совпала с датами колонок | PATCH `header.report_date` |
|
||||
| `REPORT_DATE_UNCERTAIN` | error | неуверенная дата отчёта не совпала с датами колонок (у бланка только с относительными подписями — всегда) | PATCH `header.report_date` (в том числе тем же значением — подтверждение) |
|
||||
| `XLSB_SCALE_SUSPECT` | error | xlsb без форматов, «тыс. руб.», ≥ 10 % дробных | перезагрузка |
|
||||
| `INN_NOT_FOUND`, `INN_UNCERTAIN` | info | ИНН не найден / неуверен при найденной организации | — |
|
||||
| `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN` | info | организация не найдена / неуверенна при найденном ИНН | — |
|
||||
@ -1385,10 +1486,14 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
| `FRACTIONAL_IN_THOUSANDS` | info | дробные при «тыс. руб.» (кроме xlsb без форматов) | — |
|
||||
| `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED` | info | числовые форматы недоступны или не разобраны | — |
|
||||
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр | — |
|
||||
| `SHEET_OTHER` | info | лист Excel «прочее» без типа и кодов | — |
|
||||
| `SHEET_OTHER` | info, файл | лист Excel «прочее» без типа и кодов (`locator = sheet:<имя>`; документа нет) | — |
|
||||
| `ROW_SPLIT` | info | разлеплена склеенная строка | — |
|
||||
| `OSV_*` | info | предупреждения листа ОСВ (§1) | — |
|
||||
| `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH` | info, ядро | проверки ядра (§9.3, §9.5) | пересчёт |
|
||||
| `RESULT_STATUS_INCONSISTENT` | info, ядро | `document.status = done` из результата противоречит строкам, `needs_review`, нарушениям или предупреждениям документа — ядро считает его `needs_check` (§9.3) | пересчёт после правки |
|
||||
| `MAPPING_WARNING` | info, ядро | предупреждение маппера 1.x по документу (повтор кода, значение вне колонок) | — |
|
||||
| `LEGACY_WARNING` | info, файл | строка `metrics.warnings` результата 1.x (`recognition_job.metrics`) | — |
|
||||
| `RELATION_FAILED` / `RELATION_SOFT` | error / info | код записи `document_issue` вида `relation` / `soft` — нарушение §7 из `checks` (или пересчёта ядра) с `codes`, `relation`, `expected`, `actual` | пересчёт после правки; `relation` — ещё акцепт |
|
||||
|
||||
Назначение кода и отметка `detail` снимают нарушения, пересчитываемые §7. Новый код предупреждения вводится словарём
|
||||
пакета и строкой этой таблицы; в контракте `code` — строка, не перечисление.
|
||||
@ -1396,10 +1501,27 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
**Статус документа** — одна функция общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧ период определён
|
||||
(дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей
|
||||
шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет предупреждений документа уровня
|
||||
`error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках. Ядро вызывает ту же
|
||||
функцию после правок, с подтверждениями (`is_modified` / `is_confirmed` ⇒ `needs_review = false`) и акцептом (жёсткие
|
||||
нарушения не учитываются). Квартал периода, сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ
|
||||
`document.status = done`, когда ведомость разобрана (§1.6), её предупреждения — `info`.
|
||||
`error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и проверках. Квартал периода,
|
||||
сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ `document.status = done`, когда контрольные
|
||||
суммы §1.4 листа сошлись (§1.6, S-22), её предупреждения — `info`.
|
||||
|
||||
**Интерфейс функции статуса** (K-14) — один для сервиса и ядра, на входе только данные, без типов источника:
|
||||
`document_type`; `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть хотя бы
|
||||
одна строка из источника, `row_index` не `null`); `has_organization_or_inn`; `period_known` (дата отчёта или абсолютная
|
||||
дата хотя бы одной колонки — у ядра `document.reporting_date` не `null`); строки `{line_status, needs_review}`; значения
|
||||
`{needs_review}`; число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не
|
||||
учитываются); уровни предупреждений документа (у ядра — записи `document_issue` вида `warning` с `severity = error`).
|
||||
`needs_review` строк и значений на входе уже учитывает подтверждения: у ядра правленое (`is_modified`,
|
||||
`line_code_modified`) или подтверждённое (`is_confirmed`) — `false`. Сервис собирает вход из `FormCandidate` и
|
||||
результата шагов 2–7, ядро — из строк БД; так один код даёт один статус.
|
||||
|
||||
**Ожидание на реальных сканах** (`fixtures/ocr`, оценка ревью спеки 18.10.2026, K-4): после Р-20 (повторы 1220 / 1520 /
|
||||
2410 в перечне `fsbu4` и синонимы в словаре имён) и Р-21 (мягкие соотношения ОФР) законные повторы и строки 24xx / 25xx
|
||||
перестают держать документ. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с суммами
|
||||
(aep_3), значения с уверенностью ниже 100 вне соотношений и в несошедшихся соотношениях, разлепленные строки без
|
||||
покрытия, неуверенная дата отчёта. Доля `done` на текущих шести документах ожидается небольшой; golden-эталоны
|
||||
`fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля — в его
|
||||
описании.
|
||||
|
||||
**Статус конверта:** `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`;
|
||||
документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser — коды описывают
|
||||
@ -1419,7 +1541,7 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
| Без шапки — синтез шапки и `needs_check` (рулинг 06.10.2026) | правило строки заголовка §11.3 п. 4 |
|
||||
| Подпись без периода не читается (§12 п. 4) | колонка без даты и `error` (Р-6) |
|
||||
| Закрывающая `}` / `]` не даёт минус (решение № 12, OQ-147) | `}` / `]` — закрывающая скобка, `is_recognized = false` |
|
||||
| Длинное тире → `is_recognized = false` (решение № 20, OQ-148) | прочерк любого вида — `null`, `is_recognized = true` |
|
||||
| Длинное тире → `is_recognized = false` (решение № 20, OQ-148) | прочерк любого вида — `null`, `is_recognized = true` (OQ-148 закрыт решением владельца 18.10.2026, Р-22 / S-7) |
|
||||
| `is_recognized` OCR = уверенность 100 ∧ ничего не выброшено; любое предупреждение → `needs_check` | `is_recognized` — только разбор; статус — §11.9 |
|
||||
| `checks` ядро игнорирует и пересчитывает §7 при загрузке | хранит как пришли, пересчитывает только после правок |
|
||||
|
||||
@ -1427,7 +1549,8 @@ OQ-145); без даты проверяются обе редакции. Пре
|
||||
|
||||
Восстановление строк по наименованию (OQ-149); сопоставление формы без столбца кодов по именам строк (OQ-139); склейка
|
||||
страниц из разных файлов (OQ-140) и половин формы из одного файла (OQ-165, Р-15); приведение единиц сверх числового
|
||||
формата (OQ-138); формулы §7 по редакциям и знаки 2410/2420 (OQ-12, OQ-146); образец упрощённой формы (OQ-166);
|
||||
формата (OQ-138); формулы §7 по редакциям и знаки 2410/2420 (OQ-12, OQ-146); образец упрощённой формы (OQ-166); коды
|
||||
перечня без образца в fixtures (OQ-168);
|
||||
несколько периодов в одном файле ИС (OQ-167, допущение владельца); переключатель «как в файле» на экране (Р-14).
|
||||
|
||||
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_* + document_issue` для PDF и Excel;
|
||||
|
||||
@ -153,7 +153,7 @@
|
||||
| OQ-145 | Редакция формы (`66n` / `fsbu4`) в контракте: нужна ли ядру редакция документа в конверте (необязательное поле `statementDocument`, кандидат MINOR 1.3), или ядро выбирает редакцию по `report_date` само, по тому же правилу §11.1 п. 10. Сейчас scan-gateway выбирает редакцию внутри и наружу её не отдаёт | change `slice-3a-ocr-normalizer`, открытый вопрос 2 | контракт `docs/contracts/` (v1.3), Срез 3b (маппинг форм, справочник кодов в ядре) | **закрыт решением владельца 07.10.2026:** редакция формы в контракт не добавляется (контракт v1.2); ядро определяет её по дате периода документа (`66n` при дате ≤ 2024-12-31, иначе `fsbu4`) и отдаёт в `GET /documents/{id}/statement` (`edition`), как реализовано в Срезе 3b. Досрочное применение ФСБУ 4/2023 до 2025 года — новый OQ при первом реальном случае. **18.10.2026 (change `unified-statement-normalization`):** решение сохраняется — редакция в контракт v2.0 не добавлена; сервисы выбирают редакцию тем же правилом только для упорядоченного перечня кодов (структурная проверка неуверенных кодов, `05-domain-rules.md` §11.6, §11.9) |
|
||||
| OQ-146 | Словари §7 (проверки форм) и §12 (справочник кодов `statement_lines.yaml`, формулы ОФР) **по редакциям** 66н / ФСБУ 4/2023 для tabular-parser (2b) и ядра (3b): один набор правил на обе редакции или раздельные; как выбирать редакцию в ядре для Excel-пути | change `slice-3a-ocr-normalizer` (справочник с редакциями только в scan-gateway), OQ-12, OQ-139 | tabular-parser `rules/*.yaml`, ядро (Срез 3b), §7, §12 п. 2 | открыт, **решение владельца 07.10.2026: решается вместе с OQ-150** (состав редакции `fsbu4`); до тех пор допущение прежнее — scan-gateway ведёт свой `api/rules/statement_lines.yaml`, tabular-parser и ядро словарь по редакциям не используют, формулы §7 одни на обе редакции; расширяет OQ-12 и OQ-139. **18.10.2026:** словарь §7 — единственный, в общем коде `arm_statements` (копия ядра удаляется), полный и упрощённый наборы по `form_variant`; раздельные формулы по редакциям по-прежнему не вводятся |
|
||||
| OQ-147 | Закрывающая скобка, распознанная OCR как `}` или `]` (`(7744536}` в `aep_3.json`): дословный перенос `parse_amount` минус не ставит — сумма положительная, `is_recognized = false`. Расширить ли множество закрывающих скобок в §11.1 п. 7 после проверки на стенде | change `slice-3a-ocr-normalizer`, решение №12; владелец 06.10.2026: «пока устроит как есть» | scan-gateway `normalizer/ocr/amounts.py`, §11.1 п. 7 | открыт: допущение — перенос дословный; знак теряется, строка подсвечена `is_recognized = false`, исходный текст — в `raw/`. **Закрыт дизайном, согласованным владельцем 17.10.2026 (change `unified-statement-normalization`, шаг 4, Р-7):** `}` и `]` принимаются как закрывающая скобка — «(7744536}» → −7 744 536, `is_recognized = false` (исправление); знак члена `abs_terms` подтверждается конвенцией колонки. `05-domain-rules.md` §11.5 п. 2. Действует с переходом scan-gateway на v2.0 |
|
||||
| OQ-148 | Длинное/среднее тире «—»/«–» как прочерк: дословный `parse_amount` вырезает тире как «лишний» символ → сумма `null`, но `is_recognized = false` даже при `confidence = 100` (дефис «-» даёт `true`). Лишний `needs_check` на формах с длинным тире. Считать ли прочерк любого вида распознанным | change `slice-3a-ocr-normalizer`, решение №20 | scan-gateway `amounts.py`, §11.1 п. 7, доля `done` на реальных сканах | открыт: допущение — перенос дословный; правило уточняется после прогона на стенде. **Допущение спеки `unified-statement-normalization` (18.10.2026, решение S-7, ждёт подтверждения владельца):** прочерк любого вида (`-`, `–`, `—`, `−` без цифр) — пустое значение, `amount = null`, `is_recognized = true` (как пустая ячейка, Р-2); `05-domain-rules.md` §11.5 п. 2 |
|
||||
| OQ-148 | Длинное/среднее тире «—»/«–» как прочерк: дословный `parse_amount` вырезает тире как «лишний» символ → сумма `null`, но `is_recognized = false` даже при `confidence = 100` (дефис «-» даёт `true`). Лишний `needs_check` на формах с длинным тире. Считать ли прочерк любого вида распознанным | change `slice-3a-ocr-normalizer`, решение №20 | scan-gateway `amounts.py`, §11.1 п. 7, доля `done` на реальных сканах | открыт: допущение — перенос дословный; правило уточняется после прогона на стенде. **Закрыт решением владельца 18.10.2026 (Р-22, утверждено решение спеки S-7, change `unified-statement-normalization`):** прочерк любого вида (`-`, `–`, `—`, `−` без цифр) — пустое значение, `amount = null`, `is_recognized = true` (как пустая ячейка, Р-2); тире на месте потерянной цифры ловит §7 (пустой член посчитанного соотношения); `05-domain-rules.md` §11.5 п. 2. Действует с переходом scan-gateway на контракт 2.0 |
|
||||
| OQ-149 | Строки таблицы OCR, отброшенные классификацией (поведение источника, §11.1 п. 4), не восстанавливаются: с непустым нечисловым кодом (`1150 :`) и с пустым кодом, но с суммами (в `aep_3.json` — «БАЛАНС» и «Выручка»: код пуст, мусор `211(1 \|` стоит в колонке значения). Нормализатор выдаёт предупреждения `строк с нечисловым кодом пропущено: N` и `строк без кода с суммами пропущено: N` (как OQ-141 для Excel), документ — `needs_check`, содержимое строк остаётся в `raw/`. Восстанавливать ли такие строки по наименованию (словарь редакции) или по структуре формы | `fixtures/ocr/aep_3.json`, `aep_6.json`, change `slice-3a-ocr-normalizer`, решение №29, §11.1 п. 4 | scan-gateway `rows.py`/`document.py`, полнота форм, OQ-36, OQ-139, OQ-141 | открыт: допущение — строки пропускаются с предупреждением; аналитик видит расхождение итогов в проверках 3b. **18.10.2026:** остаётся открытым (Р-8); с v2.0 такие строки не пропадают — приходят со статусами `unreadable` / `missing` / `detail` и сырым текстом, аналитик назначает код или отмечает расшифровкой (`PATCH …/statement lines`); восстановление по наименованию не делается |
|
||||
| OQ-150 | Состав редакции `fsbu4` (ФСБУ 4/2023, отчётность с 2025 года) в `api/rules/statement_lines.yaml` scan-gateway: коды и наименования строк ББ и ОФР; источник — Приложения 3–4 к ФСБУ 4/2023 (черновой перечень кодов — шаблоны `fixtures/statements-xlsx/templates/`, OQ-142). Пока `lines: {}` | change `slice-3a-ocr-normalizer`, D-7 | scan-gateway `api/rules/`, эталоны `fixtures/ocr/expected_normalize/` для документов 2025 года, статус `done` | открыт: **данные даёт владелец**, код нормализатора не меняется; до заполнения документы 2025 года идут с именами из OCR, предупреждением «редакция fsbu4 не заполнена» и `needs_check`. Порядок заполнения — `scan-gateway/README.md` **09.10.2026, решение владельца:** состав `fsbu4` (и `66n`) для ядра принят из шаблонов банка — `arm-core/api/rules/statement_lines.yaml` (arm-core#80); словарь scan-gateway — follow-up, копии расходятся (66н scan-gateway — словарь старого ядра). **18.10.2026:** словарь имён шлюза `api/rules/statement_lines.yaml` удаляется (Р-3: наименования — только из источника, справочное имя — в ядре); для сервисов нужен состав кодов `fsbu4` в упорядоченном перечне кодов `arm_statements` — источник тот же (шаблоны банка + печатные формы 1С) |
|
||||
| OQ-151 | Эмулятор ИС Сканирование: файл, совпавший по sha256 с PDF из `SCAN_FIXTURES_DIR`, у которого нет JSON-набора (`<имя>.json`), отвечает «нет пары» (`INVALID_DOCUMENT`) и **не** уходит на `SCAN_EMULATOR_DEFAULT`. Пробел спеки эмулятора: считать ли такой файл промахом и отдавать набор по умолчанию | change `slice-3a-ocr-normalizer`, спека `scan-emulator` | scan-gateway `scan_emulator`, dev-стенд (`dev/qa-guide.md` §6) | открыт: допущение — поведение как есть; PDF без JSON-пары в `fixtures/ocr` не класть |
|
||||
@ -171,8 +171,6 @@
|
||||
| OQ-163 | ОСВ, где ИНН контрагента выведен **отдельной строкой** рядом с наименованием, в любой раскладке: над / под × в дереве / не в дереве × с суммами / без (требование владельца 13.10.2026). До решения парсер склеивал только «над, не в дереве, без сумм» (OQ-130) и «под, в дереве» при подсказке «Контрагенты.ИНН» (OQ-162); «под, не в дереве, без сумм» отдавал ИНН следующему контрагенту | владелец, 13.10.2026; RSHB/arm-tabular-parser#35, #34 | tabular-parser §1.1, §1.3 | **решено владельцем 13.10.2026:** без подсказки в шапке строка ИНН — только с верной контрольной суммой ИНН (во всех раскладках, включая OQ-130); с подсказкой — без проверки, порядок элементов задаёт направление; «над, в дереве» — только единственный ребёнок; соседи — по суммам, без сумм — по направлению листа (шапка → пары по суммам → однозначные блоки); неоднозначное — строка `OTHER`, `needs_check`; ведущий ноль числовой строки ИНН (#34). Уточнения владельца (H-1): строка из 10/12 цифр с неверной контрольной суммой не склеивается, остаётся обычной строкой, предупреждение «похожа на ИНН …», `needs_check` (синтетика `s28`); (H-2): если пара найдена, но раскладка противоречит направлению подсказки `<группировка>.ИНН`, строка склеивается, но выдаётся отдельное предупреждение «стоит под/над наименованием … а шапка указывает над/под — проверьте» и `needs_check`. `05-domain-rules.md` §1.3; **реализовано** RSHB/arm-tabular-parser#35 |
|
||||
| OQ-164 | Субсчета 76 с налоговой природой в ВГО: исключать ли **76.АВ** «НДС по авансам и предоплатам» из ДЗ и зеркально **76.ВА** «НДС по авансам выданным» из КЗ (правила № 5–8 и результирующая таблица §6: «76 кроме 76.07.5/76.07.9»). В ОСВ Счастливочной на 30.09.2024 на 76.АВ — 2 366 666,69 с контрагентом АО «Русский бисквит»: результирующая таблица даёт ДЗ 110 429 113,15, эталон (`fixtures/golden/Консолидация_Группа РБ.xlsx`, лист `01.10.2024`, строка 1230 «в т. ч. взаимные») — 108 062 тыс., ровно без 76.АВ (107 712 446,46 + 350 000 по 76.06 = 108 062 446,46). Есть ли ещё субсчета 76, которые нужно исключать | ревью RSHB/arm-core#114 (шахматка, AURORA-1537), 15.10.2026; вопрос аналитику — AURORA-1373 | `elimination_rule` (Прил. 3, данные), §4, §6; `RESULT_ARTICLES` шахматки; сверка с эталоном (OQ-155) | открыт: до ответа аналитика/методологов расчёт по §4/§6 как есть (76.АВ входит в ДЗ); исключение — правка правил-данных и §4/§6, не кода шахматки |
|
||||
| OQ-165 | Склейка двух половинок одной формы из **одного** файла (Р-15): Excel с листом-продолжением другого типа, двойная печать формы, две таблицы ИС одного типа и периода. Сейчас вторая форма того же типа и периода хранится отдельным документом (Р-10), ни одна не автоподтверждается (Р-18), подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM`; склейки нет — файл перезагружают. Нужна ли склейка (кнопка «объединить формы») и по каким правилам | change `unified-statement-normalization`, Р-15, Р-18 | arm-core (`document.file_ordinal`, подтверждение), arm-web, §9.5 п. 2 | открыт: от ИС Сканирование такой случай не ожидается (владелец 17.10.2026), основной источник — Excel; допущение — склейки нет, файл перезагружают |
|
||||
| OQ-166 | Упрощённая форма ББ/ОФР (приказ 66н, прил. 5): признак — по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или 2110, нет 2100, 2200, 2300), ОКУД 0710096 признаком не служит. Набор соотношений §7 и перечень кодов упрощённой формы заданы по бланку (`05-domain-rules.md` §7, решение S-5 спеки) без образца в `fixtures/` | change `unified-statement-normalization`, design (Decisions № 14) | `arm_statements` `statement_checks.yaml`, перечень кодов, `form_variant` | открыт: допущение — формулы §7 «Упрощённый набор»; нужен реальный образец упрощённой отчётности (Excel и скан) в `fixtures/` для проверки признака и формул |
|
||||
| OQ-166 | Упрощённая форма ББ/ОФР (приказ 66н, прил. 5): признак — по структуре кодов (ББ: есть 1600, нет 1100 и 1200; ОФР: есть 2400 или 2110, нет 2100, 2200, 2300), ОКУД 0710096 признаком не служит. Набор соотношений §7 и перечень кодов упрощённой формы заданы по бланку (`05-domain-rules.md` §7, решение S-5 спеки; ББ — жёсткие, ОФР `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` — мягкое, как `2400` полного набора, решение владельца 18.10.2026, Р-22; перечни — `contracts/statement-code-orders.yaml`, у `fsbu4` — как у 66н) без образца в `fixtures/` | change `unified-statement-normalization`, design (Decisions № 14) | `arm_statements` `statement_checks.yaml`, перечень кодов, `form_variant` | открыт: допущение — формулы §7 «Упрощённый набор»; нужен реальный образец упрощённой отчётности (Excel и скан) в `fixtures/` для проверки признака и формул |
|
||||
| OQ-167 | Несколько отчётных периодов в одном файле, отданном в ИС Сканирование (например, ББ за два квартала в одном PDF): шапка в json ИС одна на json и копируется во все его таблицы | change `unified-statement-normalization`, design шаг 1 (допущение владельца 17.10.2026) | scan-gateway (адаптер шага 1), период документа | допущение владельца: ИС отдаёт разные периоды разными json; образца нет — проверить на первом таком файле, при расхождении — шапка из json неверна для части таблиц |
|
||||
|
||||
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
|
||||
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.
|
||||
| OQ-168 | Коды упорядоченного перечня форм без образца в `fixtures/` (`contracts/statement-code-orders.yaml`, §11.9): в перечень вошли только коды шаблонов банка и коды печатных форм 1С и сканов, найденные в `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930, повторы 1220 / 1520 / 2410 у `fsbu4`, раздел III НКО у `66n`). Не подтверждены образцом: 1120 в `fsbu4`; 1105, 1215, 2420 в `66n`; 2421, 2430, 2450 (прежние строки ОФР 66н); 2930 и раздел III НКО в `fsbu4`; клиентский 1231 в `fsbu4`; упрощённые перечни обеих редакций (OQ-166). Добавлять ли их без образца | change `unified-statement-normalization`, ревью спеки K-5 (18.10.2026) | `arm_statements` (перечень кодов, структурное подтверждение неуверенного кода), ядро (`CODE_NOT_IN_EDITION`) | открыт: допущение — код без образца в перечень не входит; на скане такой неуверенный код не подтверждается структурой (`needs_review`, `needs_check`), у Excel и уверенного кода — только `info` ядра `CODE_NOT_IN_EDITION`. Пополнение — строкой в yaml с источником (файл fixtures) |
|
||||
|
||||
@ -22,33 +22,34 @@
|
||||
"units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100,
|
||||
"okud": "0710001", "okud_text": "0710001", "okud_confidence": 100,
|
||||
"okpo": "12345678", "okpo_text": "12345678", "okpo_confidence": 100,
|
||||
"okei": "384", "okei_text": "384", "okei_confidence": 100,
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" },
|
||||
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "label_confidence": 100, "period_date": "2024-12-31", "period_kind": "absolute" }
|
||||
{ "column_index": 3, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" },
|
||||
{ "column_index": 4, "label": "На 31 декабря 2024 г.", "label_confidence": 100, "period_date": "2024-12-31", "period_kind": "absolute" }
|
||||
],
|
||||
"lines": [
|
||||
{ "row_index": 0, "ref": "Бухгалтерский баланс. Лист 1!C14", "line_status": "ok", "line_code": "1110", "line_code_text": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 1, "ref": "Бухгалтерский баланс. Лист 1!C15", "line_status": "ok", "line_code": "11101", "line_code_text": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "900000.00", "text": "900 000,00", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 2, "ref": "Бухгалтерский баланс. Лист 1!C16", "line_status": "ok", "line_code": "1100", "line_code_text": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 3, "ref": "Бухгалтерский баланс. Лист 1!C17", "line_status": "ok", "line_code": "1200", "line_code_text": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": null, "text": "-", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": null, "text": "", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 4, "ref": "Бухгалтерский баланс. Лист 1!C18", "line_status": "ok", "line_code": "1600", "line_code_text": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 5, "ref": "Бухгалтерский баланс. Лист 2!C5", "line_status": "ok", "line_code": "1370", "line_code_text": "1370", "line_code_confidence": 100, "line_name": "Нераспределенная прибыль (непокрытый убыток)", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 6, "ref": "Бухгалтерский баланс. Лист 2!C6", "line_status": "ok", "line_code": "1300", "line_code_text": "1300", "line_code_confidence": 100, "line_name": "Итого по разделу III", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 7, "ref": "Бухгалтерский баланс. Лист 2!C7", "line_status": "ok", "line_code": "1700", "line_code_text": "1700", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 1, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }
|
||||
{ "row_index": 0, "ref": "'Бухгалтерский баланс. Лист 1'!C14", "line_status": "ok", "line_code": "1110", "line_code_text": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 1, "ref": "'Бухгалтерский баланс. Лист 1'!C15", "line_status": "ok", "line_code": "11101", "line_code_text": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "900000.00", "text": "900 000,00", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 2, "ref": "'Бухгалтерский баланс. Лист 1'!C16", "line_status": "ok", "line_code": "1100", "line_code_text": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 3, "ref": "'Бухгалтерский баланс. Лист 1'!C17", "line_status": "ok", "line_code": "1200", "line_code_text": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": null, "text": "-", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": null, "text": "", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 4, "ref": "'Бухгалтерский баланс. Лист 1'!C18", "line_status": "ok", "line_code": "1600", "line_code_text": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 5, "ref": "'Бухгалтерский баланс. Лист 2'!C5", "line_status": "ok", "line_code": "1370", "line_code_text": "1370", "line_code_confidence": 100, "line_name": "Нераспределенная прибыль (непокрытый убыток)", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 6, "ref": "'Бухгалтерский баланс. Лист 2'!C6", "line_status": "ok", "line_code": "1300", "line_code_text": "1300", "line_code_confidence": 100, "line_name": "Итого по разделу III", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 7, "ref": "'Бухгалтерский баланс. Лист 2'!C7", "line_status": "ok", "line_code": "1700", "line_code_text": "1700", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "586260.26", "text": "586 260,26", "confidence": 100, "is_recognized": true, "needs_review": false }, { "column_index": 4, "amount": "905786.99", "text": "905 786,99", "confidence": 100, "is_recognized": true, "needs_review": false } ] }
|
||||
],
|
||||
"checks": {
|
||||
"relations_failed": [],
|
||||
"soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ]
|
||||
"soft_relations_failed": [ { "column_index": 4, "relation": "1110 = 11101", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -69,20 +70,20 @@
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "За январь - сентябрь 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }
|
||||
{ "column_index": 3, "label": "За январь - сентябрь 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }
|
||||
],
|
||||
"lines": [
|
||||
{ "row_index": 0, "ref": "Отчет о финансовых результатах!C12", "line_status": "ok", "line_code": "2110", "line_code_text": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "1000.00", "text": "1 000", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 1, "ref": "Отчет о финансовых результатах!C13", "line_status": "detail", "line_code": null, "line_code_text": "", "line_code_confidence": 100, "line_name": "в том числе от продажи продукции", "line_name_confidence": 100, "parent_ref": "Отчет о финансовых результатах!C12", "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "800.00", "text": "800", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 2, "ref": "Отчет о финансовых результатах!C14", "line_status": "ok", "line_code": "2120", "line_code_text": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "-600.00", "text": "(600)", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 3, "ref": "Отчет о финансовых результатах!C15", "line_status": "ok", "line_code": "2100", "line_code_text": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "400.00", "text": "400", "confidence": 100, "is_recognized": true, "needs_review": true } ] }
|
||||
{ "row_index": 0, "ref": "'Отчет о финансовых результатах'!C12", "line_status": "ok", "line_code": "2110", "line_code_text": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "1000.00", "text": "1 000", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 1, "ref": "'Отчет о финансовых результатах'!C13", "line_status": "detail", "line_code": null, "line_code_text": "", "line_code_confidence": 100, "line_name": "в том числе от продажи продукции", "line_name_confidence": 100, "parent_ref": "'Отчет о финансовых результатах'!C12", "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "800.00", "text": "800", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 2, "ref": "'Отчет о финансовых результатах'!C14", "line_status": "ok", "line_code": "2120", "line_code_text": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "-600.00", "text": "(600)", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 3, "ref": "'Отчет о финансовых результатах'!C15", "line_status": "ok", "line_code": "2100", "line_code_text": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 3, "amount": "400.00", "text": "400", "confidence": 100, "is_recognized": true, "needs_review": true } ] }
|
||||
],
|
||||
"checks": {
|
||||
"relations_failed": [ { "column_index": 0, "relation": "2200 = 2100 - 2210 - 2220", "codes": ["2200", "2100", "2210", "2220"], "expected": null, "actual": "400.00" } ],
|
||||
"relations_failed": [ { "column_index": 3, "relation": "2200 = 2100 - 2210 - 2220", "codes": ["2200", "2100", "2210", "2220"], "expected": null, "actual": "400.00" } ],
|
||||
"soft_relations_failed": []
|
||||
}
|
||||
},
|
||||
|
||||
@ -25,25 +25,25 @@
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }
|
||||
{ "column_index": 2, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }
|
||||
],
|
||||
"lines": [
|
||||
{ "row_index": 0, "ref": "row:1", "line_status": "ok", "line_code": "1210", "line_code_text": "1210", "line_code_confidence": 100, "line_name": "Запасы", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "100", "text": "100", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "100", "text": "100", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 1, "ref": "row:2", "line_status": "ok", "line_code": "1230", "line_code_text": "1230", "line_code_confidence": 100, "line_name": "Дебиторская задолжен-ность", "line_name_confidence": 80, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428311", "string_value": null, "text": "42831 1", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "428311", "string_value": null, "text": "42831 1", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
{ "row_index": 2, "ref": "row:3", "line_status": "unreadable", "line_code": null, "line_code_text": "ИЧ,250", "line_code_confidence": 25, "line_name": "Финансовые вложения", "line_name_confidence": 67, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "50", "text": "50", "confidence": 67, "is_recognized": true, "needs_review": true } ] },
|
||||
"values": [ { "column_index": 2, "amount": "50", "text": "50", "confidence": 67, "is_recognized": true, "needs_review": true } ] },
|
||||
{ "row_index": 3, "ref": "row:4", "line_status": "ok", "line_code": "1200", "line_code_text": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 4, "ref": "row:5", "line_status": "ok", "line_code": "1600", "line_code_text": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] },
|
||||
{ "row_index": 5, "ref": "row:14#1", "line_status": "ok", "line_code": "1370", "line_code_text": "1370", "line_code_confidence": 100, "line_name": "Нераспределенная прибыль (непокрытый убыток)", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
{ "row_index": 6, "ref": "row:14#2", "line_status": "ok", "line_code": "1300", "line_code_text": "1300", "line_code_confidence": 100, "line_name": "Итого по разделу III", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
"values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": false, "needs_review": false } ] },
|
||||
{ "row_index": 7, "ref": "row:15", "line_status": "ok", "line_code": "1700", "line_code_text": "1700", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "needs_review": false,
|
||||
"values": [ { "column_index": 0, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] }
|
||||
"values": [ { "column_index": 2, "amount": "428411", "text": "428 411", "confidence": 100, "is_recognized": true, "needs_review": false } ] }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [] }
|
||||
}
|
||||
|
||||
12
contracts/examples/v1/result-error.json
Normal file
12
contracts/examples/v1/result-error.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "NOT_IMPLEMENTED",
|
||||
"message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet"
|
||||
},
|
||||
"documents": []
|
||||
}
|
||||
21
contracts/examples/v1/result-message-done.json
Normal file
21
contracts/examples/v1/result-message-done.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
|
||||
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
|
||||
"status": "done",
|
||||
"service": { "name": "scan-gateway", "version": "0.1.0" },
|
||||
"result_s3": {
|
||||
"bucket": "arm-ka",
|
||||
"key": "results/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/result.json"
|
||||
},
|
||||
"result_schema_version": "1.0",
|
||||
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
|
||||
"metrics": {
|
||||
"duration_ms": 18400,
|
||||
"pages": 2,
|
||||
"fields_total": 4,
|
||||
"fields_low_confidence": 0,
|
||||
"fields_unrecognized": 0,
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
11
contracts/examples/v1/result-message-error.json
Normal file
11
contracts/examples/v1/result-message-error.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"status": "error",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"error": {
|
||||
"code": "NOT_IMPLEMENTED",
|
||||
"message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet"
|
||||
}
|
||||
}
|
||||
7
contracts/examples/v1/result-message-processing.json
Normal file
7
contracts/examples/v1/result-message-processing.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"status": "processing",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" }
|
||||
}
|
||||
83
contracts/examples/v1/result-osv-minimal.json
Normal file
83
contracts/examples/v1/result-osv-minimal.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"status": "done",
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "OSV",
|
||||
"locator": "sheet:Лист_1",
|
||||
"header": {
|
||||
"organization_name": "ООО Ромашка",
|
||||
"organization_inn": "7700000000",
|
||||
"account_code_raw": "60",
|
||||
"period_text": "1 квартал 2025 г.",
|
||||
"period_year": 2025,
|
||||
"period_quarter": "Q1",
|
||||
"is_cumulative": false,
|
||||
"period_start": "2025-01-01",
|
||||
"period_end": "2025-03-31",
|
||||
"is_from_year_start": true,
|
||||
"data_type": "BU",
|
||||
"currency": "RUB",
|
||||
"header_variant": 1
|
||||
},
|
||||
"group_schema": [
|
||||
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
|
||||
],
|
||||
"columns": {
|
||||
"opening_debit": "C",
|
||||
"opening_credit": "D",
|
||||
"turnover_debit": "E",
|
||||
"turnover_credit": "F",
|
||||
"closing_debit": "G",
|
||||
"closing_credit": "H"
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"row_number": 1,
|
||||
"level": 0,
|
||||
"parent_row_number": null,
|
||||
"row_value": "60",
|
||||
"group_type": "ACCOUNT",
|
||||
"group_name": null,
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": null,
|
||||
"counterparty_inn": null,
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
},
|
||||
{
|
||||
"row_number": 2,
|
||||
"level": 1,
|
||||
"parent_row_number": 1,
|
||||
"row_value": "ООО Лютик, ИНН 7711111111",
|
||||
"group_type": "COUNTERPARTY",
|
||||
"group_name": "Контрагенты",
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": "ООО Лютик",
|
||||
"counterparty_inn": "7711111111",
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50",
|
||||
"extra": { "КПП": "352801001" }
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"total_row_matches_root": true,
|
||||
"balance_identity_violations": [],
|
||||
"parent_sum_violations": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
125
contracts/examples/v1/result-osv-needs-check.json
Normal file
125
contracts/examples/v1/result-osv-needs-check.json
Normal file
@ -0,0 +1,125 @@
|
||||
{
|
||||
"schema_version": "1.1",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "1.0.0" },
|
||||
"status": "needs_check",
|
||||
"metrics": {
|
||||
"rows": 3,
|
||||
"warnings": [
|
||||
"sheet=Лист_1 hierarchy=outline",
|
||||
"§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3"
|
||||
]
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "OSV",
|
||||
"locator": "sheet:Лист_1",
|
||||
"header": {
|
||||
"organization_name": "ООО Ромашка",
|
||||
"organization_inn": null,
|
||||
"account_code_raw": "60",
|
||||
"period_text": "1 квартал 2025 г.",
|
||||
"period_year": 2025,
|
||||
"period_quarter": "Q1",
|
||||
"is_cumulative": false,
|
||||
"data_type": "BU",
|
||||
"currency": "RUB",
|
||||
"header_variant": 1
|
||||
},
|
||||
"group_schema": [
|
||||
{ "type": "ACCOUNT", "name": "Счет" },
|
||||
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
|
||||
],
|
||||
"columns": {
|
||||
"opening_debit": "D",
|
||||
"opening_credit": "E",
|
||||
"turnover_debit": "F",
|
||||
"turnover_credit": "G",
|
||||
"closing_debit": "H",
|
||||
"closing_credit": "I",
|
||||
"inn": "B"
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"row_number": 1,
|
||||
"level": 0,
|
||||
"parent_row_number": null,
|
||||
"row_value": "60",
|
||||
"group_type": "ACCOUNT",
|
||||
"group_name": "Счет",
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": null,
|
||||
"counterparty_inn": null,
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
},
|
||||
{
|
||||
"row_number": 2,
|
||||
"level": 1,
|
||||
"parent_row_number": 1,
|
||||
"row_value": "ООО Лютик",
|
||||
"group_type": "COUNTERPARTY",
|
||||
"group_name": "Контрагенты",
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": "ООО Лютик",
|
||||
"counterparty_inn": "7711111111",
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50",
|
||||
"extra": { "КПП": "352801001" }
|
||||
},
|
||||
{
|
||||
"row_number": 3,
|
||||
"level": 2,
|
||||
"parent_row_number": 2,
|
||||
"row_value": "Вал.",
|
||||
"group_type": "OTHER",
|
||||
"group_name": null,
|
||||
"account_code_raw": "60",
|
||||
"counterparty_name": null,
|
||||
"counterparty_inn": null,
|
||||
"data_type": "CURRENCY",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "400.00",
|
||||
"turnover_debit": "100.00",
|
||||
"turnover_credit": "50.00",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "400.00"
|
||||
},
|
||||
{
|
||||
"row_number": 4,
|
||||
"level": 0,
|
||||
"parent_row_number": null,
|
||||
"row_value": "Итого",
|
||||
"group_type": "TOTAL",
|
||||
"group_name": null,
|
||||
"account_code_raw": null,
|
||||
"counterparty_name": null,
|
||||
"counterparty_inn": null,
|
||||
"data_type": "BU",
|
||||
"opening_debit": "0.00",
|
||||
"opening_credit": "1000.00",
|
||||
"turnover_debit": "500.00",
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50"
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"total_row_matches_root": true,
|
||||
"balance_identity_violations": [3],
|
||||
"parent_sum_violations": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
46
contracts/examples/v1/result-statement-excel.json
Normal file
46
contracts/examples/v1/result-statement-excel.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"job_id": "3f1c7a2e-5b64-4d0a-9c1e-8a7b6d5e4f30",
|
||||
"file_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
||||
"service": { "name": "tabular-parser", "version": "0.3.0" },
|
||||
"status": "done",
|
||||
"metrics": { "duration_ms": 412, "rows": 7, "warnings": ["sheet=Бухгалтерский баланс. Лист 1 значений с дробной частью при единице тыс. руб.: 3"] },
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "BALANCE",
|
||||
"locator": "sheet:Бухгалтерский баланс. Лист 1,Бухгалтерский баланс. Лист 2",
|
||||
"header": { "form_name": "Бухгалтерский баланс", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "inn": "7700000000", "inn_confidence": 100, "report_date_text": "на 30 сентября 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710001", "okpo": "12345678" },
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
|
||||
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
|
||||
],
|
||||
"lines": [
|
||||
{ "line_code": "1110", "line_code_confidence": 100, "line_name": "Нематериальные активы", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "11101", "line_code_confidence": 100, "line_name": "Нематериальные активы, готовые к использованию", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "900000.00", "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "1100", "line_code_confidence": 100, "line_name": "Итого по разделу I", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "1200", "line_code_confidence": 100, "line_name": "Итого по разделу II", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": null, "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": null, "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "1600", "line_code_confidence": 100, "line_name": "БАЛАНС", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "586260.26", "confidence": 100, "is_recognized": true }, { "column_index": 1, "amount": "905786.99", "confidence": 100, "is_recognized": true } ] }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "expected": "905786.99", "actual": "900000.00" } ], "duplicate_line_codes": [] }
|
||||
},
|
||||
{
|
||||
"document_type": "INCOME",
|
||||
"locator": "sheet:Отчет о финансовых результатах",
|
||||
"header": { "form_name": "Отчет о финансовых результатах", "organization_name": "ООО \"Пример\"", "organization_name_confidence": 100, "report_date_text": "за январь - сентябрь 2025 г.", "report_date": "2025-09-30", "units": "тыс. руб.", "okud": "0710002" },
|
||||
"columns": [ { "column_index": 0, "label": "За январь - сентябрь 2025 г.", "period_date": "2025-09-30" } ],
|
||||
"lines": [
|
||||
{ "line_code": "2110", "line_code_confidence": 100, "line_name": "Выручка", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "1000.00", "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "2120", "line_code_confidence": 100, "line_name": "Себестоимость продаж", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "-600.00", "confidence": 100, "is_recognized": true } ] },
|
||||
{ "line_code": "2100", "line_code_confidence": 100, "line_name": "Валовая прибыль (убыток)", "line_name_confidence": 100, "values": [ { "column_index": 0, "amount": "400.00", "confidence": 100, "is_recognized": true } ] }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] }
|
||||
},
|
||||
{
|
||||
"document_type": "DISCLOSURE",
|
||||
"locator": "sheet:Пояснения",
|
||||
"header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах" },
|
||||
"columns": [],
|
||||
"lines": []
|
||||
}
|
||||
]
|
||||
}
|
||||
50
contracts/examples/v1/result-statement-minimal.json
Normal file
50
contracts/examples/v1/result-statement-minimal.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
|
||||
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
|
||||
"service": { "name": "scan-gateway", "version": "0.1.0" },
|
||||
"status": "done",
|
||||
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "BALANCE",
|
||||
"locator": "pages:1-2",
|
||||
"header": {
|
||||
"form_name": "Бухгалтерский баланс",
|
||||
"organization_name": "ООО Ромашка",
|
||||
"organization_name_confidence": 97.5,
|
||||
"inn": "7700000000",
|
||||
"inn_confidence": 99,
|
||||
"report_date_text": "30 сентября 2025 г.",
|
||||
"report_date": "2025-09-30",
|
||||
"units": "тыс. руб."
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
|
||||
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
|
||||
],
|
||||
"lines": [
|
||||
{
|
||||
"line_code": "1100",
|
||||
"line_code_confidence": 99,
|
||||
"line_name": "Итого по разделу I",
|
||||
"line_name_confidence": 96,
|
||||
"values": [
|
||||
{ "column_index": 0, "amount": "1234.50", "confidence": 95, "is_recognized": true },
|
||||
{ "column_index": 1, "amount": "1100", "confidence": 93, "is_recognized": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"line_code": "1600",
|
||||
"line_code_confidence": 99,
|
||||
"line_name": "БАЛАНС (актив)",
|
||||
"line_name_confidence": 98,
|
||||
"values": [
|
||||
{ "column_index": 0, "amount": "5678.00", "confidence": 94.5, "is_recognized": true },
|
||||
{ "column_index": 1, "amount": "-12.30", "confidence": 90, "is_recognized": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
"$id": "arm-ka/recognition-result-message/v2",
|
||||
"title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v2.0",
|
||||
"description": "Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed). Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:<job_id>:<attempt> — для status = processing; result:<job_id>:<attempt> — для done, needs_check и error. Форма запроса <job_id>:<attempt> описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения; новый MAJOR — «сначала ядро»: ядро на время перехода принимает конверты (включая ack processing) и прежнего, и нового MAJOR. Changelog: 1.0 — исходный контракт; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — metrics.warnings[] стали структурами {code, severity, message, line_refs?, column_index?, field?, locator?} (как metrics.warnings файла результата), производитель пишет в конверт тот же MAJOR, что и в файл результата.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения; новый MAJOR — «сначала ядро»: ядро на время перехода принимает конверты (включая ack processing) и прежнего, и нового MAJOR. Changelog: 1.0 — исходный контракт; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — metrics.warnings[] стали структурами {code, severity, message, line_refs?, column_index?, field?, locator?} (как metrics.warnings файла результата), производитель пишет в конверт тот же MAJOR, что и в файл результата. Схема 1.0 на окне перехода лежит рядом — recognition-result-message.v1.schema.json (примеры — examples/v1/).",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "status", "service"],
|
||||
"properties": {
|
||||
@ -61,7 +61,7 @@
|
||||
"message": { "type": "string" },
|
||||
"line_refs": { "type": "array", "items": { "type": "string" } },
|
||||
"column_index": { "type": "integer" },
|
||||
"field": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "address"] },
|
||||
"field": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "okei", "address"] },
|
||||
"locator": { "type": "string" }
|
||||
}
|
||||
}
|
||||
|
||||
70
contracts/recognition-result-message.v1.schema.json
Normal file
70
contracts/recognition-result-message.v1.schema.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "arm-ka/recognition-result-message/v1",
|
||||
"title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v1.0",
|
||||
"description": "Тело сообщения на subject ответа (настройка окружения NATS_RESULT_SUBJECT, по умолчанию files.tabular_parsed). Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:<job_id>:<attempt> — для status = processing; result:<job_id>:<attempt> — для done, needs_check и error. Форма запроса <job_id>:<attempt> описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "status", "service"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" },
|
||||
"job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" },
|
||||
"file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" },
|
||||
"status": { "enum": ["processing", "done", "needs_check", "error"] },
|
||||
"service": {
|
||||
"type": "object",
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": { "enum": ["tabular-parser", "scan-gateway"] },
|
||||
"version": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"result_s3": {
|
||||
"type": "object",
|
||||
"description": "файл результата (recognition-result): results/{file_id}/{job_id}/result.json",
|
||||
"required": ["bucket", "key"],
|
||||
"properties": {
|
||||
"bucket": { "type": "string", "minLength": 1 },
|
||||
"key": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
},
|
||||
"result_schema_version": { "type": "string", "pattern": "^\\d+\\.\\d+$", "description": "schema_version файла результата, MAJOR.MINOR" },
|
||||
"error": {
|
||||
"type": "object",
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] },
|
||||
"message": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" },
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_ms": { "type": "integer" },
|
||||
"pages": { "type": "integer" },
|
||||
"rows": { "type": "integer" },
|
||||
"fields_total": { "type": "integer" },
|
||||
"fields_low_confidence": { "type": "integer" },
|
||||
"fields_unrecognized": { "type": "integer" },
|
||||
"warnings": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "status": { "enum": ["done", "needs_check"] } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": { "required": ["result_s3", "result_schema_version"] }
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "status": { "const": "error" } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": { "required": ["error"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
"$id": "arm-ka/recognition-result/v2",
|
||||
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0",
|
||||
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, смысл is_recognized у OCR отвязан от confidence. Схема 1.2 — в истории git (коммит 43b4b80 репозитория arm-ka-docs) и в моделях маппера 1.x ядра.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, header.okei (+ _text, _confidence), смысл is_recognized у OCR отвязан от confidence. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
|
||||
"properties": {
|
||||
@ -69,7 +69,7 @@
|
||||
"money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" },
|
||||
"confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"documentStatus": { "enum": ["done", "needs_check"] },
|
||||
"headerField": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "address"] },
|
||||
"headerField": { "enum": ["organization_name", "inn", "report_date", "units", "okud", "okpo", "okopf", "okfs", "okei", "address"] },
|
||||
"warning": {
|
||||
"type": "object",
|
||||
"required": ["code", "severity", "message"],
|
||||
@ -77,8 +77,8 @@
|
||||
"code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$", "examples": ["COLUMN_WITHOUT_DATE", "ROW_SPLIT"], "description": "код по таблице 05-domain-rules.md §11; не перечисление — новый код не меняет MAJOR" },
|
||||
"severity": { "enum": ["error", "info"], "description": "error → документ needs_check; info на статус не влияет" },
|
||||
"message": { "type": "string" },
|
||||
"line_refs": { "type": "array", "items": { "type": "string" }, "description": "ref строк документа (statementDocument.lines[].ref)" },
|
||||
"column_index": { "type": "integer" },
|
||||
"line_refs": { "type": "array", "items": { "type": "string" }, "description": "ref строк документа — только строк из statementDocument.lines[]; строки ролей title / header / numbering в line_refs не попадают (о них — message)" },
|
||||
"column_index": { "type": "integer", "description": "столбец сетки кандидата (как columns[].column_index); у COLUMN_SKIPPED и других предупреждений о столбце вне columns[] — номер этого столбца" },
|
||||
"field": { "$ref": "#/$defs/headerField" }
|
||||
}
|
||||
},
|
||||
@ -105,7 +105,7 @@
|
||||
"properties": {
|
||||
"document_type": { "const": "OSV" },
|
||||
"locator": { "type": "string", "examples": ["sheet:Лист_1"] },
|
||||
"status": { "$ref": "#/$defs/documentStatus", "description": "done — ведомость разобрана (§1.6)" },
|
||||
"status": { "$ref": "#/$defs/documentStatus", "description": "done — контрольные суммы §1.4 листа сошлись, иначе needs_check (05-domain-rules.md §1.6)" },
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" }, "description": "предупреждения листа ОСВ; все уровня info, код с префиксом OSV_" },
|
||||
"header": {
|
||||
"type": "object",
|
||||
@ -184,7 +184,7 @@
|
||||
"type": "object",
|
||||
"required": ["column_index", "amount", "text", "confidence", "is_recognized", "needs_review"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"column_index": { "type": "integer", "description": "колонка значений — columns[].column_index" },
|
||||
"amount": { "$ref": "#/$defs/money" },
|
||||
"string_value": { "type": ["string", "null"], "description": "текст ячейки, только когда amount = null и текст непуст и не прочерк" },
|
||||
"text": { "type": ["string", "null"], "description": "сырой текст источника всегда (у Excel — как показывает числовой формат); null — позиции нет" },
|
||||
@ -198,7 +198,7 @@
|
||||
"required": ["row_index", "ref", "line_status", "line_code", "line_code_text", "line_name", "needs_review", "values"],
|
||||
"properties": {
|
||||
"row_index": { "type": "integer", "minimum": 0, "description": "порядковый номер строки в документе с 0, по порядку сетки после разлепления" },
|
||||
"ref": { "type": "string", "minLength": 1, "examples": ["row:8", "row:14#2", "Баланс!C27"], "description": "положение в источнике; уникален в документе" },
|
||||
"ref": { "type": "string", "minLength": 1, "examples": ["row:8", "row:14#2", "Баланс!C27", "'Бухгалтерский баланс. Лист 1'!C14"], "description": "положение в источнике; уникален в документе. OCR — row:<row_index ИС>[#k]; Excel — <лист>!<A1> по правилу Excel: имя листа в одинарных кавычках, если в нём есть что-то кроме букв, цифр и _ или оно начинается с цифры; апостроф внутри имени удваивается" },
|
||||
"line_status": { "enum": ["ok", "unreadable", "missing", "detail"] },
|
||||
"line_code": { "type": ["string", "null"], "pattern": "^\\d{4,5}$", "description": "разобранный код; null ⇔ line_status ≠ ok" },
|
||||
"line_code_text": { "type": ["string", "null"], "description": "сырой текст ячейки кода" },
|
||||
@ -258,10 +258,13 @@
|
||||
"okfs": { "type": ["string", "null"] },
|
||||
"okfs_text": { "type": ["string", "null"] },
|
||||
"okfs_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"okei": { "type": ["string", "null"], "examples": ["384"], "description": "код ОКЕИ единицы (384 — тыс. руб., 383 — руб., 385 — млн руб.); запасной источник units (§11.2)" },
|
||||
"okei_text": { "type": ["string", "null"] },
|
||||
"okei_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"address": { "type": ["string", "null"] },
|
||||
"address_text": { "type": ["string", "null"] },
|
||||
"address_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"needs_review_fields": { "type": "array", "items": { "$ref": "#/$defs/headerField" }, "uniqueItems": true, "description": "поля шапки с needs_review = true (сейчас — только report_date)" }
|
||||
"needs_review_fields": { "type": "array", "items": { "$ref": "#/$defs/headerField" }, "uniqueItems": true, "description": "поля шапки, значение которых требует проверки и держит документ в needs_check (сейчас — только неуверенная дата отчёта, не подтверждённая датами колонок); сомнения в ИНН и организации — предупреждения info с field, сюда не входят (§11.9)" }
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
@ -270,7 +273,7 @@
|
||||
"type": "object",
|
||||
"required": ["column_index", "label", "period_kind"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"column_index": { "type": "integer", "minimum": 0, "description": "номер столбца в сетке кандидата шага 1 (у OCR — column_index ИС, не обязательно подряд; у Excel — номер столбца листа первого сегмента, с 0); уникален в документе; колонки сегментов без шапки сопоставляются с колонками первого сегмента по порядку и несут их номер" },
|
||||
"label": { "type": "string", "examples": ["На 30 сентября 2025 г.", "На отчетную дату"] },
|
||||
"label_confidence": { "$ref": "#/$defs/confidence", "description": "у Excel 100; подписи нет — null" },
|
||||
"period_date": { "type": ["string", "null"], "format": "date", "description": "конец периода колонки (period_end); у относительной подписи — от эффективной даты документа" },
|
||||
@ -278,7 +281,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" } },
|
||||
"lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" }, "description": "у документа без строк (DISCLOSURE, форма без столбца кодов) — пустой массив" },
|
||||
"checks": {
|
||||
"type": "object",
|
||||
"required": ["relations_failed", "soft_relations_failed"],
|
||||
|
||||
220
contracts/recognition-result.v1.schema.json
Normal file
220
contracts/recognition-result.v1.schema.json
Normal file
@ -0,0 +1,220 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "arm-ka/recognition-result/v1",
|
||||
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.2",
|
||||
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.",
|
||||
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1, 1.2, …) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
|
||||
"properties": {
|
||||
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.2 пишет \"1.2\"" },
|
||||
"job_id": { "type": "string", "format": "uuid" },
|
||||
"file_id": { "type": "string", "format": "uuid" },
|
||||
"service": {
|
||||
"type": "object",
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": { "enum": ["tabular-parser", "scan-gateway"] },
|
||||
"version": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"status": { "enum": ["done", "needs_check", "error"] },
|
||||
"error": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] },
|
||||
"message": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"duration_ms": { "type": "integer" },
|
||||
"pages": { "type": "integer" },
|
||||
"rows": { "type": "integer" },
|
||||
"fields_total": { "type": "integer" },
|
||||
"fields_low_confidence": { "type": "integer" },
|
||||
"fields_unrecognized": { "type": "integer" },
|
||||
"warnings": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" },
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] }
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": { "status": { "enum": ["done", "needs_check"] } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": {
|
||||
"properties": { "documents": { "minItems": 1 } }
|
||||
},
|
||||
"$defs": {
|
||||
"money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" },
|
||||
"failedRelation": {
|
||||
"type": "object",
|
||||
"required": ["column_index", "relation", "expected", "actual"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] },
|
||||
"expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения как в файле" },
|
||||
"actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам" }
|
||||
}
|
||||
},
|
||||
"osvDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "group_schema", "rows"],
|
||||
"properties": {
|
||||
"document_type": { "const": "OSV" },
|
||||
"locator": { "type": "string", "examples": ["sheet:Лист_1"] },
|
||||
"header": {
|
||||
"type": "object",
|
||||
"required": ["organization_name", "account_code_raw", "period_text"],
|
||||
"properties": {
|
||||
"organization_name": { "type": "string", "description": "как в r1 файла" },
|
||||
"organization_inn": { "type": ["string", "null"] },
|
||||
"account_code_raw": { "type": "string", "examples": ["60", "58.03"] },
|
||||
"period_text": { "type": "string", "examples": ["1 квартал 2025 г.", "2024 г."] },
|
||||
"period_year": { "type": ["integer", "null"] },
|
||||
"period_quarter": { "enum": ["Q1", "Q2", "Q3", "Q4", null] },
|
||||
"is_cumulative": { "type": ["boolean", "null"], "description": "по тексту; ядро может переопределить по данным (OQ-02)" },
|
||||
"period_start": { "type": ["string", "null"], "format": "date", "description": "начало периода по шапке: первое число квартала/года либо начало диапазона месяцев (OQ-131)" },
|
||||
"period_end": { "type": ["string", "null"], "format": "date", "description": "конец периода по шапке" },
|
||||
"is_from_year_start": { "type": ["boolean", "null"], "description": "period_start — 1 января; is_cumulative не переопределяет" },
|
||||
"data_type": { "enum": ["BU", "NU", "SUM", "CURRENCY", "QUANTITY", null] },
|
||||
"currency": { "type": ["string", "null"], "examples": ["RUB", "руб."] },
|
||||
"header_variant": { "enum": [1, 2] }
|
||||
}
|
||||
},
|
||||
"group_schema": {
|
||||
"type": "array",
|
||||
"description": "порядок группировок из шапки, после первого ACCOUNT",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": { "enum": ["ACCOUNT", "DEPARTMENT", "COUNTERPARTY", "CONTRACT", "OTHER"] },
|
||||
"name": { "type": "string", "description": "исходный текст из шапки" },
|
||||
"has_inn": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"type": "object",
|
||||
"description": "диагностика: какие колонки листа были приняты за шесть сумм и ИНН",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["row_number", "level", "row_value", "group_type"],
|
||||
"properties": {
|
||||
"row_number": { "type": "integer" },
|
||||
"level": { "type": "integer" },
|
||||
"parent_row_number": { "type": ["integer", "null"] },
|
||||
"row_value": { "type": "string" },
|
||||
"group_type": { "enum": ["ACCOUNT", "DEPARTMENT", "COUNTERPARTY", "CONTRACT", "OTHER", "TOTAL"] },
|
||||
"group_name": { "type": ["string", "null"] },
|
||||
"account_code_raw": { "type": ["string", "null"], "description": "код текущей ветки, как в файле (60.1)" },
|
||||
"counterparty_name": { "type": ["string", "null"] },
|
||||
"counterparty_inn": { "type": ["string", "null"] },
|
||||
"data_type": { "enum": ["BU", "NU", "SUM", "CURRENCY", "QUANTITY", null] },
|
||||
"opening_debit": { "$ref": "#/$defs/money" },
|
||||
"opening_credit": { "$ref": "#/$defs/money" },
|
||||
"turnover_debit": { "$ref": "#/$defs/money" },
|
||||
"turnover_credit": { "$ref": "#/$defs/money" },
|
||||
"closing_debit": { "$ref": "#/$defs/money" },
|
||||
"closing_credit": { "$ref": "#/$defs/money" },
|
||||
"extra": { "type": ["object", "null"], "description": "нераспознанные колонки внутри таблицы: заголовок колонки → текст ячейки (§1.2)", "additionalProperties": { "type": ["string", "null"] } }
|
||||
}
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total_row_matches_root": { "type": "boolean" },
|
||||
"balance_identity_violations": { "type": "array", "items": { "type": "integer" }, "description": "row_number строк, где opening−turnover≠closing" },
|
||||
"parent_sum_violations": { "type": "array", "items": { "type": "integer" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"statementDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "columns", "lines"],
|
||||
"properties": {
|
||||
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
|
||||
"locator": { "type": "string", "examples": ["pages:1-2"] },
|
||||
"header": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"form_name": { "type": "string" },
|
||||
"organization_name": { "type": ["string", "null"] },
|
||||
"organization_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"inn": { "type": ["string", "null"] },
|
||||
"inn_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"report_date_text": { "type": ["string", "null"] },
|
||||
"report_date": { "type": ["string", "null"], "format": "date" },
|
||||
"units": { "type": ["string", "null"], "examples": ["тыс. руб."] },
|
||||
"okud": { "type": ["string", "null"] },
|
||||
"okpo": { "type": ["string", "null"] },
|
||||
"okopf": { "type": ["string", "null"] },
|
||||
"okfs": { "type": ["string", "null"] },
|
||||
"address": { "type": ["string", "null"] }
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["column_index", "label"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"label": { "type": "string", "examples": ["На 30 сентября 2025 г."] },
|
||||
"period_date": { "type": ["string", "null"], "format": "date" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["line_code", "line_name", "values"],
|
||||
"properties": {
|
||||
"line_code": { "type": "string" },
|
||||
"line_code_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"line_name": { "type": "string" },
|
||||
"line_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["column_index"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"amount": { "$ref": "#/$defs/money" },
|
||||
"string_value": { "type": ["string", "null"] },
|
||||
"confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
|
||||
"is_recognized": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"type": "object",
|
||||
"description": "Результаты самопроверки формы по соотношениям §7 (tabular-parser). Отсутствие объекта — «не проверялось» (OCR-путь); пустые массивы — «проверено, сошлось».",
|
||||
"properties": {
|
||||
"relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения жёстких соотношений: документ needs_check" },
|
||||
"soft_relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "нарушения мягких соотношений (OQ-12, подстроки «в том числе») и жёстких в пределах округления итогов (OQ-138): только предупреждение" },
|
||||
"duplicate_line_codes": { "type": "array", "items": { "type": "string" }, "description": "коды строк, встретившиеся в документе больше одного раза (осталась первая строка)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
contracts/statement-code-orders.yaml
Normal file
193
contracts/statement-code-orders.yaml
Normal file
@ -0,0 +1,193 @@
|
||||
# Упорядоченные перечни кодов строк форм ББ/ОФР по редакциям и вариантам — данные для структурного подтверждения
|
||||
# неуверенного кода (05-domain-rules.md §11.9 «Код строки»; change unified-statement-normalization, K-5, Р-20).
|
||||
#
|
||||
# Что это: порядок кодов бланка (итоги после слагаемых: 1110 … 1190, 1100), без наименований (Р-3: «код → наименование»
|
||||
# знает только ядро, arm-core/api/rules/statement_lines.yaml). Только четырёхзначные коды: пятизначная подстрока
|
||||
# подтверждается через родителя (§11.9). Перечень — объединение кодов шаблонов банка (словарь имён ядра,
|
||||
# fixtures/statements-xlsx/templates/) и кодов печатных форм 1С и сканов, найденных в fixtures (источник — комментарий
|
||||
# у кода). Код без образца в fixtures сюда не добавляется: кандидаты — OQ-168.
|
||||
#
|
||||
# Где живёт: этот файл — источник; в пакете arm_statements лежит его байтовая копия (rules/statement_code_orders.yaml),
|
||||
# идентичность проверяет sync-statements --check (как копии схем контракта в сервисах). Ядро берёт порядок и состав
|
||||
# редакций из копии пакета; sync-statements --check сверяет, что все коды словаря имён ядра есть в перечне своей
|
||||
# редакции. Правка перечня — PR сюда, затем в arm-statements, затем sync-statements в потребителях («сначала ядро»).
|
||||
#
|
||||
# Как читается (§11.9): четырёхзначные коды строк ok документа — последовательность; для каждой редакции берётся
|
||||
# наибольшая подпоследовательность, согласованная с порядком перечня варианта формы документа (form_variant); выбирается
|
||||
# редакция с более длинной (при равенстве — по эффективной дате: fsbu4 после 2024-12-31). Повтор кода допустим только
|
||||
# там, где он есть в перечне (repeats): тогда структура подтверждает обе копии; повтор вне перечня с неуверенной копией —
|
||||
# needs_review у неуверенной копии (Р-16, Р-20).
|
||||
#
|
||||
# Проверка формы файла — scripts/validate_contracts.py (make validate-contracts).
|
||||
version: 1
|
||||
|
||||
# Законные повторы кода в одном перечне (Р-20). Ключ — редакция и форма; значение — коды, которые встречаются в
|
||||
# перечне больше одного раза.
|
||||
repeats:
|
||||
66n:
|
||||
# раздел III для некоммерческих организаций («Целевое финансирование») печатается после раздела III коммерческих
|
||||
# (fixtures/ocr/aep_3.json, 31.12.2024: 1310 «Паевой фонд», 1320 «Целевой капитал», 1350 «Целевые средства»,
|
||||
# 1360 «Фонд недвижимого и особо ценного движимого имущества», 1370 «Резервный и иные целевые фонды»)
|
||||
BALANCE: ["1310", "1320", "1350", "1360", "1370", "1300"]
|
||||
INCOME: []
|
||||
fsbu4:
|
||||
# печатная форма 1С по ФСБУ 4/2023 (fixtures/ocr/aep_6.json, «БО 1 кв 2025.json», «Бухгалтерская отчетность
|
||||
# 1 кв.25.json»): 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде» (AURORA-1431)
|
||||
BALANCE: ["1220", "1520"]
|
||||
# 2410 «Налог при упрощенной системе налогообложения» после 2412 (fixtures/ocr/aep_6.json, OQ-152)
|
||||
INCOME: ["2410"]
|
||||
|
||||
editions:
|
||||
66n:
|
||||
BALANCE:
|
||||
full:
|
||||
- "1110"
|
||||
- "1120"
|
||||
- "1130"
|
||||
- "1140"
|
||||
- "1150"
|
||||
- "1160"
|
||||
- "1170"
|
||||
- "1180"
|
||||
- "1190"
|
||||
- "1100"
|
||||
- "1210"
|
||||
- "1220"
|
||||
- "1230"
|
||||
- "1231" # код клиента: unagrande-group/2024-FY/ББ. Лист 1 .xls
|
||||
- "1240"
|
||||
- "1250"
|
||||
- "1260"
|
||||
- "1200"
|
||||
- "1600"
|
||||
- "1310"
|
||||
- "1320"
|
||||
- "1340"
|
||||
- "1350"
|
||||
- "1360"
|
||||
- "1370"
|
||||
- "1300"
|
||||
- "1310" # раздел III НКО (aep_3), см. repeats
|
||||
- "1320"
|
||||
- "1350"
|
||||
- "1360"
|
||||
- "1370"
|
||||
- "1300"
|
||||
- "1410"
|
||||
- "1420"
|
||||
- "1430"
|
||||
- "1450"
|
||||
- "1400"
|
||||
- "1510"
|
||||
- "1520"
|
||||
- "1530"
|
||||
- "1540"
|
||||
- "1550"
|
||||
- "1500"
|
||||
- "1700"
|
||||
# упрощённая форма, приказ 66н прил. 5 — по бланку, образца в fixtures нет (OQ-166)
|
||||
simplified: ["1150", "1170", "1210", "1250", "1230", "1600", "1300", "1350", "1360", "1410", "1450", "1510",
|
||||
"1520", "1550", "1700"]
|
||||
INCOME:
|
||||
full:
|
||||
- "2110"
|
||||
- "2120"
|
||||
- "2100"
|
||||
- "2210"
|
||||
- "2220"
|
||||
- "2200"
|
||||
- "2310"
|
||||
- "2320"
|
||||
- "2330"
|
||||
- "2340"
|
||||
- "2350"
|
||||
- "2300"
|
||||
- "2410"
|
||||
- "2411"
|
||||
- "2412"
|
||||
- "2460"
|
||||
- "2400"
|
||||
- "2510"
|
||||
- "2520"
|
||||
- "2530" # печатная форма 1С: unagrande-co/2024-FY/ОФР. Лист 2 .xls, mezheninovskaya/2025-Q3
|
||||
- "2500"
|
||||
- "2900"
|
||||
- "2910"
|
||||
- "2930" # печатная форма 1С: fixtures/ocr/aep_3.json (после 2900)
|
||||
# упрощённая форма, приказ 66н прил. 5 — по бланку, образца нет (OQ-166)
|
||||
simplified: ["2110", "2120", "2330", "2340", "2350", "2410", "2400"]
|
||||
fsbu4:
|
||||
BALANCE:
|
||||
full:
|
||||
- "1105" # печатная форма 1С: npz/2025-Q3, fixtures/ocr/aep_6.json, «БО 1 кв 2025.json»
|
||||
- "1110"
|
||||
- "1130" # печатная форма 1С: npz/2025-Q3, «БО 1 кв 2025.json», «Бухгалтерская отчетность 1 кв.25.json»
|
||||
- "1140" # то же и aep_6.json
|
||||
- "1150"
|
||||
- "1160"
|
||||
- "1170"
|
||||
- "1180"
|
||||
- "1190"
|
||||
- "1100"
|
||||
- "1210"
|
||||
- "1215"
|
||||
- "1220"
|
||||
- "1220" # «НДС, начисленный по отгрузке», см. repeats
|
||||
- "1230"
|
||||
- "1240"
|
||||
- "1250"
|
||||
- "1260"
|
||||
- "1200"
|
||||
- "1600"
|
||||
- "1310"
|
||||
- "1320"
|
||||
- "1340"
|
||||
- "1350"
|
||||
- "1360"
|
||||
- "1370"
|
||||
- "1300"
|
||||
- "1410"
|
||||
- "1420"
|
||||
- "1430"
|
||||
- "1450"
|
||||
- "1400"
|
||||
- "1510"
|
||||
- "1520"
|
||||
- "1520" # «Обязательство по аренде», см. repeats
|
||||
- "1530" # печатная форма 1С: npz/2025-Q3, aep_6.json, оба json 1 кв. 2025
|
||||
- "1540"
|
||||
- "1550"
|
||||
- "1500"
|
||||
- "1700"
|
||||
# упрощённая форма по ФСБУ 4/2023 — состав принят как у 66н до образца (OQ-166, OQ-168)
|
||||
simplified: ["1150", "1170", "1210", "1250", "1230", "1600", "1300", "1350", "1360", "1410", "1450", "1510",
|
||||
"1520", "1550", "1700"]
|
||||
INCOME:
|
||||
full:
|
||||
- "2110"
|
||||
- "2120"
|
||||
- "2100"
|
||||
- "2210"
|
||||
- "2220"
|
||||
- "2200"
|
||||
- "2310"
|
||||
- "2320"
|
||||
- "2330"
|
||||
- "2340"
|
||||
- "2350"
|
||||
- "2300"
|
||||
- "2410"
|
||||
- "2411"
|
||||
- "2412"
|
||||
- "2410" # «Налог при упрощенной системе налогообложения», см. repeats
|
||||
- "2420"
|
||||
- "2460" # печатная форма 1С: npz/2025-Q3, aep_6.json, оба json 1 кв. 2025
|
||||
- "2400"
|
||||
- "2510"
|
||||
- "2520"
|
||||
- "2530"
|
||||
- "2500"
|
||||
- "2900"
|
||||
- "2910"
|
||||
# упрощённая форма по ФСБУ 4/2023 — как у 66н до образца (OQ-166, OQ-168)
|
||||
simplified: ["2110", "2120", "2330", "2340", "2350", "2410", "2400"]
|
||||
@ -21,7 +21,7 @@
|
||||
| `make test` | `pytest -m "not integration"`: юнит-тесты; **включает тест идентичности вендоренного контракта** `api/contracts/` с `../docs/contracts` | нет |
|
||||
| `make test-integration` | `pytest -m integration` против инфраструктуры compose (PostgreSQL 16, NATS, MinIO; бакет `arm-ka` должен существовать, его создаёт `make up`) | да, `make up` |
|
||||
| `make check` | `make lint` затем `make test`; останавливается на первой упавшей проверке (только при последовательном запуске: `lint` и `test` — пререквизиты, при `make -j` они идут параллельно) | нет |
|
||||
| `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker |
|
||||
| `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`) + `statements-check` (свежесть копии общего кода `arm_statements`, см. ниже); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker |
|
||||
| `make up` | поднимает инфраструктуру из `../docs/dev/docker-compose.yml`; постоянные сервисы `postgres`, `nats`, `minio` | Docker |
|
||||
|
||||
Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` (в `arm-core` — `mypy src scripts`) →
|
||||
@ -51,9 +51,45 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm
|
||||
|
||||
| Репозиторий | Шаги по порядку |
|
||||
|---|---|
|
||||
| `scan-gateway` | `check` → `test-integration` → `docker-build` (образ `scan-gateway:dev`); интеграционный тест поднимает эмулятор ИС Сканирование в процессе теста |
|
||||
| `tabular-parser` | `check` → `test-integration` → `docker-build` (образ `tabular-parser`) |
|
||||
| `arm-core` | `check` → `dbml-check` → `check-infra` → `test-integration` → `docker-build` → `web-check` |
|
||||
| `scan-gateway` | `check` → `statements-check` → `test-integration` → `docker-build` (образ `scan-gateway:dev`); интеграционный тест поднимает эмулятор ИС Сканирование в процессе теста |
|
||||
| `tabular-parser` | `check` → `statements-check` → `test-integration` → `docker-build` (образ `tabular-parser`) |
|
||||
| `arm-core` | `check` → `statements-check` → `dbml-check` → `check-infra` → `test-integration` → `docker-build` → `web-check` |
|
||||
|
||||
`statements-check` появляется в сервисе вместе с копией пакета `arm_statements` (change `unified-statement-normalization`,
|
||||
шаги 3 и 5 выкладки); до этого цели нет.
|
||||
|
||||
### Копия общего кода `arm_statements` и `sync-statements`
|
||||
|
||||
Общий код нормализации ББ/ОФР (`05-domain-rules.md` §11) живёт в репозитории `arm-statements` пакетом `arm_statements/`;
|
||||
в `arm-core`, `tabular-parser` и `scan-gateway` он лежит **копией** — прямые правки копии запрещены (Р-1, design
|
||||
`openspec/changes/unified-statement-normalization/design.md`). **Пока не реализовано:** скрипт, цель `make -C docs
|
||||
sync-statements` и цель `statements-check` сервисов появляются на шагах 1, 3 и 5 выкладки change
|
||||
`unified-statement-normalization`; раздел описывает целевое поведение (спека `statement-normalization`, требование
|
||||
«`sync-statements`…»).
|
||||
|
||||
- **Где копия:** `api/arm_statements/` (рядом с `api/src/`; импорт `import arm_statements` — `api/` в `sys.path`, как для
|
||||
`src`). Пакет приносит свои тесты (`api/arm_statements/tests/`) и конфиг линтеров; `ruff`, `pytest` и `mypy` хоста
|
||||
охватывают копию: `ruff` и `pytest` — по каталогу `api/` (тесты пакета подключены как `testpaths`), `mypy` — `MYPY_PATHS`
|
||||
хоста дополняется `arm_statements` (`mypy src arm_statements`, в `arm-core` — `mypy src scripts arm_statements`).
|
||||
- **Маркер версии:** `api/arm_statements/VERSION` — полный хэш коммита `arm-statements`, из которого разложена копия;
|
||||
пишет только скрипт. Сервис при старте читает его и пишет в результат `service.statements_version`; ядро сравнивает его
|
||||
с `statements_version` задания при пересчёте после правки (`STATEMENTS_VERSION_MISMATCH`, `info`).
|
||||
- **Скрипт:** `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`), запуск из workspace или `make -C docs
|
||||
sync-statements SYNC_ARGS=…`. Источник — локальный checkout `arm-statements` рядом (`../arm-statements`) или
|
||||
`--source <dir>`, ссылка — `origin/main` или `--ref <ref>`; хост — `--host arm-core|tabular-parser|scan-gateway` или
|
||||
`--host-dir <dir>`. Без `--check` — раскладывает копию нужного коммита в новый worktree хоста на ветке фичи и пишет
|
||||
`VERSION`; issue и PR — как у `sync-web.sh`.
|
||||
- **`--check`, коды возврата:** `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`;
|
||||
`10` — копия равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала; предупреждение, не
|
||||
ошибка); `20` — копия отличается от дерева коммита из `VERSION` (прямая правка) или `VERSION` нет в истории источника,
|
||||
либо байтовая копия перечня кодов `arm_statements/rules/statement_code_orders.yaml` отличается от
|
||||
`docs/contracts/statement-code-orders.yaml`, либо (только `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
|
||||
отсутствует в перечне своей редакции; `2` — нет checkout `arm-statements` или хоста, неизвестный аргумент. Файлы
|
||||
расхождения перечисляются в выводе.
|
||||
- **`statements-check` в `make check-full`:** `../docs/scripts/sync-statements.sh --check --host-dir .` (docs — `../docs`
|
||||
или `ARM_KA_DOCS`); код `0` и `10` — цель зелёная (при `10` предупреждение печатается, в описание PR вписывается, что копия
|
||||
отстала), `20` и `2` — цель красная. В `make check` и в пайплайн платформы не входит: тест, читающий соседний
|
||||
репозиторий, уже ломал `main` (arm-core #33).
|
||||
|
||||
Для `arm-core` дополнительно:
|
||||
|
||||
@ -130,14 +166,18 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу
|
||||
|
||||
| Цель | Что делает |
|
||||
|---|---|
|
||||
| `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` — единственной зависимостью скриптов |
|
||||
| `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против трёх схем JSON Schema 2020-12 (3 схемы, 8 примеров); код 1 при любой ошибке |
|
||||
| `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` и `PyYAML` — зависимостями скриптов (после обновления репозитория — перезапустить) |
|
||||
| `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против текущих схем JSON Schema 2020-12, примеры `contracts/examples/v1/` — против схем прежнего MAJOR `*.v1.schema.json` (окно перехода на контракт 2.0), форма упорядоченных перечней кодов `contracts/statement-code-orders.yaml`; код 1 при любой ошибке |
|
||||
| `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) |
|
||||
| `make check` | обе проверки выше |
|
||||
|
||||
Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`.
|
||||
Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их
|
||||
нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт.
|
||||
нужно пересинхронизировать (`cp`), иначе тест идентичности в сервисах упадёт. На окне перехода между MAJOR (сейчас 1 → 2)
|
||||
тест идентичности сервиса сверяет свою копию со схемой **того MAJOR, который сервис пишет**: копия с `$id` `…/v1` — с
|
||||
`*.v1.schema.json`, иначе — с основным файлом (`openspec/specs/recognition-contract`, требование «Копия контракта в
|
||||
сервисе идентична источнику»). Ядро держит копии обоих MAJOR. Перечень кодов `contracts/statement-code-orders.yaml`
|
||||
копируется не в сервисы, а в пакет `arm_statements` (сверяет `sync-statements --check`).
|
||||
|
||||
## Порядок перед PR
|
||||
|
||||
|
||||
@ -422,12 +422,15 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
**`GET /documents/{id}/statement` — новые поля `StatementOut`:**
|
||||
|
||||
- `document.form_variant` — `full` | `simplified` | `null` (упрощённая форма — свой набор проверок §7).
|
||||
- `header.needs_review_fields[]` — поля шапки, требующие проверки (сейчас бывает только `report_date`); сырые тексты полей.
|
||||
- `header.needs_review_fields[]` — поля шапки, которые держат документ в «Требует проверки» (сейчас бывает только
|
||||
`report_date`; подсветить поле). Сомнения в ИНН и организации — не здесь, а в `issues` с `field` (`INN_UNCERTAIN`,
|
||||
`ORGANIZATION_UNCERTAIN`, уровень `info`). `header.source` — `{<поле>: {text, confidence}}`: как поле распознано
|
||||
(сырой текст и уверенность) — в подсказку у поля; `header.organization_name_modified` — наименование правилось.
|
||||
- `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` у шаблонных и
|
||||
созданных правкой), `source_ref`, `line_status` (`ok` | `unreadable` | `missing` | `detail`), `line_code` (`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` — его показывать основным,
|
||||
файловое рядом, если отличается), `parent_row_index` (родитель расшифровки `detail`), `needs_review` (подсветить строку),
|
||||
@ -437,37 +440,45 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
**Подсветка ячейки — только по `needs_review`**; по `confidence < 100` и `is_recognized = false` больше не подсвечивать
|
||||
(сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается).
|
||||
- `issues[]` — предупреждения и нарушения документа: `{id, kind: "warning" | "relation" | "soft", code, severity: "error" |
|
||||
"info", message, line_ids[], column_id?, field?, source: "service" | "core", relation?, expected?, actual?}`; показывать у
|
||||
места (строки, ячейки, поле шапки) или у документа; `error` отличать от `info`.
|
||||
"info", message, line_ids[], column_id?, column_index?, field?, source: "service" | "core", relation?, codes?, expected?,
|
||||
actual?}`; показывать у места (строки, ячейки, поле шапки) или у документа; `error` отличать от `info`. `column_index` без
|
||||
`column_id` — столбец, которого нет среди колонок формы (пропущенная колонка «Изменение, %»): показывать у документа.
|
||||
Нарушения §7 приходят и здесь (`kind = relation` / `soft`, `code = RELATION_FAILED` / `RELATION_SOFT`) — у любой формы,
|
||||
в том числе загруженной до перехода.
|
||||
- `checks.failed[]` / `soft_failed[]` — у нарушения ещё `codes[]` и `line_ids[]` (все строки кодов соотношения).
|
||||
- Порядок `lines` — уже «как на бумаге»: строки `ok` по коду (одинаковые коды по `row_index`), `unreadable` / `missing` —
|
||||
после ближайшей предыдущей по `row_index` строки с кодом (без такой — в начале), `detail` — сразу после родителя.
|
||||
- Порядок `lines` — по возрастанию кода, как сейчас (одинаковые коды — по `row_index`); `unreadable` / `missing` — после
|
||||
ближайшей предыдущей по `row_index` строки с кодом (без такой — в начале), `detail` — сразу после родителя. Разделы и
|
||||
размещение итогов «как на бумаге» строит экран, как и раньше (`formLayout`).
|
||||
Расшифровки (`detail`) по умолчанию скрывать за переключателем «Показать расшифровки (N)».
|
||||
|
||||
**`PATCH /documents/{id}/statement` — новые части тела** (можно вместе в одном запросе; один запрос — один пересчёт):
|
||||
|
||||
| Часть | Тело | Эффект | Ошибки |
|
||||
|---|---|---|---|
|
||||
| подтвердить ячейку | `values: [{line_id, column_id, confirmed: true}]` | `is_confirmed`, `needs_review = false`, сумма не меняется | 422 `UNKNOWN_CELL` |
|
||||
| дата колонки | `columns: [{column_id, period_date}]` | дата колонки, `is_current`, период документа; снимает `COLUMN_WITHOUT_DATE` / `COLUMN_DATE_UNCERTAIN` | 422 `UNKNOWN_COLUMN` |
|
||||
| назначить / сменить код | `lines: [{line_id, line_code}]` | `unreadable` / `missing` / `detail` / `ok` → `ok` с этим кодом, `line_code_confidence = 100` | 422 `INVALID_LINE_CODE` (не 4–5 цифр), 409 `LINE_EXISTS` (код занят строкой, созданной правкой, с суммами — сначала очистить её) |
|
||||
| отметить расшифровкой | `lines: [{line_id, line_status: "detail"}]` | строка выходит из сумм и не блокирует подтверждение | 422 `LINE_CODE_REQUIRED` (`line_status: "ok"` без кода) |
|
||||
| подтвердить код | `lines: [{line_id, confirmed: true}]` | `is_confirmed`, `needs_review` строки = `false` | — |
|
||||
| подтвердить ячейку / снять подтверждение | `values: [{line_id, column_id, confirmed: true \| false}]` | `is_confirmed`, `needs_review = false` (при `false` — пересчитывается), сумма не меняется, акцепт не сбрасывается | 422 `UNKNOWN_CELL` |
|
||||
| дата колонки | `columns: [{column_id, period_date}]` (дата обязательна) | дата колонки, `is_current`, период документа; снимает `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` — пересчитывается) | — |
|
||||
| наименование организации | `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
|
||||
`UNKNOWN_CELL`: передайте `line_id`. Правка `header.report_date` пересчитывает даты колонок с `period_kind = relative:*`.
|
||||
Удалять строки нельзя — для этого «Сделать расшифровкой». Ответ — `StatementOut` после пересчёта.
|
||||
Удалять строки нельзя — для этого «Сделать расшифровкой». Акцепт проверок сбрасывает правка данных (сумма, код, статус
|
||||
строки, дата колонки, поле шапки), подтверждения (`confirmed`) его не сбрасывают. Ответ — `StatementOut` после пересчёта.
|
||||
|
||||
**`POST /documents/{id}/confirm` для формы — порядок 409:** `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` →
|
||||
`DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → **`LINES_UNRESOLVED`** (`detail.lines[] {line_id, row_index,
|
||||
line_code_text}` — строки `unreadable` / `missing` с суммами; кнопку «Подтвердить данные» отключать заранее с подсказкой
|
||||
«Разберите строки без кода: N») → **`DUPLICATE_FORM`** (`detail.document_id` — в этом файле уже подтверждена форма того
|
||||
же типа и периода) → `CHECKS_NOT_PASSED`.
|
||||
line_code_text}` — строки `unreadable` / `missing`, у которых в ячейках есть сумма или нераспознанный текст; кнопку
|
||||
«Подтвердить данные» отключать заранее с подсказкой «Разберите строки без кода: N») → **`DUPLICATE_FORM`**
|
||||
(`detail.document_id` — в этом файле уже подтверждена форма того же типа, года и квартала) → `CHECKS_NOT_PASSED`.
|
||||
|
||||
**Статус документа.** Форма из OCR теперь бывает `RECOGNIZED` (и сразу автоподтверждается), если сервис всё подтвердил
|
||||
проверками; `NEEDS_CHECK` — когда есть что смотреть: подсвеченные ячейки и строки, `issues` уровня `error`, неразобранные
|
||||
строки. Две формы одного типа в одном файле приходят двумя документами; ни одна не автоподтверждается.
|
||||
**Статус документа.** Форма из OCR может стать `RECOGNIZED` (и сразу автоподтвердиться), если сервис всё подтвердил
|
||||
проверками; на текущих реальных сканах это редкость — `NEEDS_CHECK` остаётся, когда есть что смотреть: подсвеченные ячейки
|
||||
и строки, `issues` уровня `error`, неразобранные строки. Две формы одного типа в одном файле приходят двумя документами;
|
||||
ни одна не автоподтверждается.
|
||||
|
||||
**ГСК/ГСЗ.** `crm_id` в `POST …/related-groups` и `PATCH /directories/related-groups/{id}` сохраняется и возвращается в
|
||||
`RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409 `CRM_ID_EXISTS` (arm-core#112, DBML v5.19).
|
||||
@ -669,7 +680,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
|
||||
| `CHECKS_NOT_PASSED` | 409 | подтверждение формы | `failures[] {column_index, relation, expected, actual}` | показать нарушения, предложить акцепт |
|
||||
| `LINES_UNRESOLVED` | 409 | подтверждение формы (после #116, §5.6.1) | `lines[] {line_id, row_index, line_code_text}` | перейти к первой строке без кода, предложить назначить код или «Это расшифровка» |
|
||||
| `DUPLICATE_FORM` | 409 | подтверждение формы (после #116) | `document_id` | ссылка на уже подтверждённую форму того же типа и периода в файле |
|
||||
| `LINE_EXISTS` | 409 | правка формы, назначение кода (после #116) | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами |
|
||||
| `LINE_CODE_TAKEN` | 409 | правка формы, назначение кода (после #116) | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами |
|
||||
| `INVALID_LINE_CODE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN` | 422 | правка формы `lines` / `columns` (после #116) | — | ошибка поля / ошибка клиента (перечитать форму) |
|
||||
| `DOCUMENT_NOT_STATEMENT` | 409 | `…/statement` не по форме ББ/ОФР | — | не открывать экран [41] для такого документа |
|
||||
| `DOCUMENT_READONLY` | 409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр |
|
||||
|
||||
@ -27,10 +27,25 @@
|
||||
// (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа
|
||||
// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — порядковый номер документа в результате, уникален в файле.
|
||||
// 8. related_group.crm_id (varchar, nullable, unique) — ID ГСК/ГСЗ в ЕЦП CRM (AURORA-1619, arm-core#112, OQ-33).
|
||||
// Заполнение при миграции: row_index строк — по возрастанию line_code внутри формы с 0, line_status = ok, needs_review
|
||||
// строк = false, needs_review значений — по прежнему правилу values_ok (правлено или is_recognized не false и confidence
|
||||
// 100 / NULL), source_text = string_value, is_confirmed = false, period_kind = absolute при period_date, иначе none,
|
||||
// form_variant = NULL, file_ordinal — по (created_at, id) внутри файла с 0, related_group.crm_id = NULL.
|
||||
// 9. Признак «строка создана правкой аналитика» — financial_statement_line.row_index IS NULL (заменяет прежний
|
||||
// признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет).
|
||||
// line_code_original_value — код строки из результата как пришёл (NULL у статусов кроме ok и у строк, созданных
|
||||
// правкой); правками не меняется.
|
||||
// 10. financial_statement_meta: header_source (jsonb — сырой текст и уверенность всех полей шапки результата,
|
||||
// {"<поле>": {"text", "confidence"}}), organization_name_original_value и organization_name_modified (наименование
|
||||
// правится PATCH шапки, как ИНН).
|
||||
// 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок
|
||||
// формы — COLUMN_SKIPPED и др.).
|
||||
// Заполнение при миграции: row_index строк — NULL у строк с line_code_original_value IS NULL (созданных правкой), у
|
||||
// остальных — по возрастанию line_code внутри формы с 0; line_status = ok, needs_review строк = false, needs_review
|
||||
// значений — по прежнему правилу values_ok (правлено или is_recognized не false и confidence 100 / NULL), source_text =
|
||||
// string_value, is_confirmed = false, period_kind = absolute при period_date, иначе none, form_variant = NULL (набор §7 —
|
||||
// полный), header_source — из *_original_value и inn_confidence, organization_name_original_value = organization_name,
|
||||
// file_ordinal — по (created_at, id) внутри файла с 0, related_group.crm_id = NULL; document_issue каждой формы —
|
||||
// нарушения §7, посчитанные копией arm_statements по текущим суммам (source = core, kind relation / soft).
|
||||
// Откат гарантирован на пустой БД и на данных, совместимых с v5.18 (нет повторов кода в форме, строк без кода, двух
|
||||
// форм одного типа в файле, crm_id групп); на несовместимых данных downgrade останавливается с ошибкой и ничего не
|
||||
// удаляет молча.
|
||||
//
|
||||
// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files):
|
||||
// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте
|
||||
@ -963,8 +978,11 @@ Table financial_statement_meta {
|
||||
checks_accepted_by uuid [ref: > app_user.id, note: 'v5.9 ТЗ 6.3 п.7: акцепт КА непройденных проверок (напр. 1370/2400) с комментарием']
|
||||
checks_accepted_at timestamptz
|
||||
checks_accepted_comment text
|
||||
form_variant statement_form_variant [note: 'v5.19: full | simplified — по структуре кодов (§11.7); выбирает набор соотношений §7; NULL — документ до v5.19']
|
||||
needs_review_fields jsonb [note: 'v5.19: имена полей шапки с needs_review (["report_date"]), как пришло от сервиса; правка поля убирает его из списка']
|
||||
form_variant statement_form_variant [note: 'v5.19: full | simplified — по структуре кодов (§11.7); выбирает набор соотношений §7; NULL — документ до v5.19 или результат 1.x: набор полный']
|
||||
needs_review_fields jsonb [note: 'v5.19: имена полей шапки, требующих проверки (["report_date"]), как пришло от сервиса; правка поля или подтверждение тем же значением убирает его из списка']
|
||||
header_source jsonb [note: 'v5.19: шапка результата как пришла — {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для organization_name, inn, report_date, units, okud, okpo, okopf, okfs, okei, address; не меняется. *_original_value — как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; inn_confidence = header_source.inn.confidence']
|
||||
organization_name_original_value varchar [note: 'v5.19: наименование организации из результата; правками не меняется']
|
||||
organization_name_modified boolean [note: 'v5.19: наименование правилось аналитиком (PATCH шапки)']
|
||||
Note: 'Шапка распознанной формы ББ/ОФР. v5.19: результат проверок §7 и предупреждения хранятся в document_issue как пришли от сервиса и пересчитываются ядром только после правок (пересматривает «не хранятся — считаются на лету»)'
|
||||
}
|
||||
|
||||
@ -996,14 +1014,14 @@ Table financial_statement_line {
|
||||
id uuid [pk]
|
||||
meta_id uuid [not null, ref: > financial_statement_meta.id]
|
||||
line_code varchar [note: 'код строки ОКУД, напр. 1230']
|
||||
line_code_original_value varchar
|
||||
line_code_original_value varchar [note: 'v5.19: line_code строки из результата как пришёл (NULL у unreadable / missing / detail и у строк, созданных правкой); правками кода не меняется. До v5.19 NULL был признаком «создана правкой» — теперь это row_index IS NULL']
|
||||
line_code_confidence numeric(5,2)
|
||||
line_code_modified boolean
|
||||
line_name varchar
|
||||
line_name_original_value varchar
|
||||
line_name_confidence numeric(5,2)
|
||||
line_name_modified boolean [note: 'FIX: был numeric']
|
||||
row_index int [note: 'v5.19: порядок строки в документе источника (с 0); NULL — строка шаблона, созданная правкой аналитика']
|
||||
row_index int [note: 'v5.19: порядок строки в документе источника (с 0). NULL ⇔ строка создана правкой аналитика (шаблонная, origin = template); у строк из результата — всегда не NULL (маппер 1.x и 2.0)']
|
||||
source_ref varchar [note: 'v5.19: положение в источнике (row:8, row:14#2, Баланс!C27); NULL у строк, созданных правкой']
|
||||
line_status statement_line_status [not null, default: 'ok', note: 'v5.19: ok | unreadable | missing | detail; line_code пуст ⇔ статус не ok']
|
||||
line_code_text varchar [note: 'v5.19: сырой текст ячейки кода, как распознано']
|
||||
@ -1053,11 +1071,13 @@ Table document_issue {
|
||||
id uuid [pk]
|
||||
document_id uuid [not null, ref: > document.id]
|
||||
kind document_issue_kind [not null]
|
||||
code varchar [not null, note: 'код предупреждения (05-domain-rules.md §11.9) или код вида нарушения']
|
||||
code varchar [not null, note: 'код предупреждения (05-domain-rules.md §11.9); у relation — RELATION_FAILED (severity error), у soft — RELATION_SOFT (severity info)']
|
||||
severity issue_severity [not null]
|
||||
message text [not null]
|
||||
line_ids jsonb [note: 'массив financial_statement_line.id, к которым относится запись (нарушение — все строки кодов соотношения)']
|
||||
line_ids jsonb [note: 'массив financial_statement_line.id, к которым относится запись (нарушение — все строки кодов соотношения); FK нет — при удалении шаблонной строки (назначение её кода строке источника) ядро убирает её id из line_ids всех записей документа']
|
||||
column_id uuid [ref: > financial_statement_column.id]
|
||||
column_index smallint [note: 'столбец сетки из предупреждения, если колонки нет среди financial_statement_column (COLUMN_SKIPPED и др.); у колонки формы — column_id']
|
||||
codes jsonb [note: 'для relation / soft: коды цели и членов соотношения (["1600","1100","1200"]); экран подсвечивает строки этих кодов']
|
||||
field varchar [note: 'поле шапки (report_date, inn, organization_name, …)']
|
||||
source issue_source [not null]
|
||||
relation varchar [note: 'для relation / soft: текст соотношения (1600 = 1100 + 1200)']
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 548 KiB |
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 93 KiB |
@ -9,19 +9,22 @@ Change `openspec/changes/unified-statement-normalization`: формы ББ/ОФ
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | `financial_statement_line`: снята уникальность `(meta_id, line_code)` (индекс остаётся неуникальным); новые `row_index`, `source_ref` (частичный уникальный `(meta_id, source_ref) WHERE source_ref IS NOT NULL`), `line_status` (enum `statement_line_status`: `ok` / `unreadable` / `missing` / `detail`), `line_code_text`, `parent_row_index`, `needs_review`, `is_confirmed` | Повтор кода — законный случай (один код, разные наименования; Р-4): хранятся все строки, чтения «по коду» идут через агрегат формы по коду (сумма строк `ok`, не хранится — DECISION-5). Строки без кода хранятся со статусом (Р-8, отменяет OQ-36 / OQ-141): `detail` — расшифровка под строкой с кодом, в §7, агрегаты, КФО и ЕХ не входит; `unreadable` / `missing` блокируют подтверждение, пока у них есть суммы (Р-13). Порядок и положение в источнике (`row_index`, `source_ref`) нужны экрану, чтобы показать строку и предупреждение у места |
|
||||
| 1 | `financial_statement_line`: снята уникальность `(meta_id, line_code)` (индекс остаётся неуникальным); новые `row_index`, `source_ref` (частичный уникальный `(meta_id, source_ref) WHERE source_ref IS NOT NULL`), `line_status` (enum `statement_line_status`: `ok` / `unreadable` / `missing` / `detail`), `line_code_text`, `parent_row_index`, `needs_review`, `is_confirmed` | Повтор кода — законный случай (один код, разные наименования; Р-4): хранятся все строки, чтения «по коду» идут через агрегат формы по коду (сумма строк `ok`, не хранится — DECISION-5). Строки без кода хранятся со статусом (Р-8, отменяет OQ-36 / OQ-141): `detail` — расшифровка под строкой с кодом, в §7, агрегаты, КФО и ЕХ не входит; `unreadable` / `missing` блокируют подтверждение, пока у них есть суммы (Р-13). Порядок и положение в источнике (`row_index`, `source_ref`) нужны экрану, чтобы показать строку и предупреждение у места. **Признак «строка создана правкой аналитика» — `row_index IS NULL`** (прежний признак `line_code_original_value IS NULL` не годится: у строк `unreadable` / `missing` / `detail` источника кода тоже нет); `line_code_original_value` — код из результата как пришёл, правками не меняется |
|
||||
| 2 | `financial_statement_value`: `source_text`, `needs_review`, `is_confirmed`, `confirmed_by`, `confirmed_at` | `needs_review` **хранится** (пересматривает п. 4 v5.13): его считает сервис («подтверждение проверкой» §7 — Р-5, Р-7), ядро пересчитывает только после правок; подтверждение ячейки без правки аналитиком переживает пересчёт. Сырой текст источника — подсказка ячейки |
|
||||
| 3 | `financial_statement_column`: `period_kind`, `label_confidence` | Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года») — дата колонки от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6); колонка без даты остаётся с `period_kind = none` |
|
||||
| 4 | `financial_statement_meta`: `form_variant` (enum `statement_form_variant`), `needs_review_fields` | Упрощённая форма — свой набор соотношений §7; поля шапки, требующие проверки (сейчас — неуверенная дата отчёта) |
|
||||
| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке, полю шапки; как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой |
|
||||
| 4 | `financial_statement_meta`: `form_variant` (enum `statement_form_variant`), `needs_review_fields`, `header_source` (jsonb), `organization_name_original_value`, `organization_name_modified` | Упрощённая форма — свой набор соотношений §7 (`NULL` — документ до v5.19 или результат 1.x, набор полный); поля шапки, требующие проверки (сейчас — неуверенная дата отчёта); сырой текст и уверенность всех полей шапки результата (`<поле>_text`, `<поле>_confidence`) — подсказка у поля на экране; наименование организации правится, как ИНН |
|
||||
| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке (`column_id`, а вне колонок формы — `column_index`), полю шапки; у нарушения — `codes` (коды цели и членов); как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой. **Единственный источник нарушений** для экрана и подтверждения: для результатов 1.x и форм, существовавших до миграции, ядро один раз считает §7 копией `arm_statements` и пишет записи с `source = core` |
|
||||
| 6 | `recognition_job.statements_version` | Коммит копии общего кода `arm_statements`, которым посчитан результат (`service.statements_version`); расхождение с копией ядра при пересчёте — `info` |
|
||||
| 7 | `document`: уникальность `(stored_file_id, document_type, osv_account_code)` — частичная, кроме `BALANCE` / `INCOME`; новая колонка `file_ordinal` + уникальный `(stored_file_id, file_ordinal)` | Вторая форма того же типа в файле хранится (Р-10, Р-15); при двух формах одного типа и периода ни одна не автоподтверждается, подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` (Р-18). ОСВ по счёту, расшифровка и прочий — по-прежнему один на тип на файл (§9.5 п. 2) |
|
||||
| 8 | `related_group.crm_id` (`varchar`, nullable, unique) | ID ГСК/ГСЗ в ЕЦП CRM из форм `51-03`, `51-04`, `77-02` (AURORA-1619, arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); уникален, как `crm_id` ГК и ЮЛ |
|
||||
|
||||
Остальные таблицы и enum — как в v5.18 (44 таблицы, 33 enum). Реализация — одна миграция в `arm-core`; заполнение:
|
||||
`row_index` — по возрастанию `line_code` в форме, `line_status = ok`, `needs_review` значений — по прежнему правилу
|
||||
`values_ok`, `source_text = string_value`, `period_kind` — `absolute` при `period_date`, иначе `none`, `file_ordinal` — по
|
||||
`(created_at, id)` в файле.
|
||||
Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; заполнение: `row_index` — `NULL` у строк,
|
||||
созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status =
|
||||
ok`, `needs_review` значений — по прежнему правилу `values_ok`, `source_text = string_value`, `period_kind` — `absolute`
|
||||
при `period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле;
|
||||
`document_issue` — нарушения §7 каждой формы, посчитанные копией `arm_statements` (`source = core`). Откат миграции
|
||||
гарантирован на пустой БД и на данных, совместимых с v5.18 (без повторов кода, строк без кода и двух форм одного типа в
|
||||
файле); на несовместимых данных `downgrade` останавливается с ошибкой.
|
||||
|
||||
## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90)
|
||||
|
||||
@ -145,11 +148,11 @@ Change `openspec/changes/unified-statement-normalization`: формы ББ/ОФ
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `financial_statement_meta` | Шапка распознанной формы (1:1 с `document` BALANCE/INCOME): ИНН и наименование как в документе, отчётная дата, единица измерения, реквизиты. Ревью формы (`reviewed_by/at`) и акцепт КА непройденных логических проверок с комментарием. Подтверждение документа — `document.confirmed_by`. **v5.19:** `form_variant` (полная / упрощённая — набор соотношений §7), `needs_review_fields`; результат проверок §7 и предупреждения хранятся в `document_issue` как пришли от сервиса, ядро пересчитывает их только после правок. | → `document`, `recognition_job`, `app_user`; ← `column`, `line` | ТЗ 6.2/6.3; FNR01; макет |
|
||||
| `financial_statement_meta` | Шапка распознанной формы (1:1 с `document` BALANCE/INCOME): ИНН и наименование как в документе, отчётная дата, единица измерения, реквизиты. Ревью формы (`reviewed_by/at`) и акцепт КА непройденных логических проверок с комментарием. Подтверждение документа — `document.confirmed_by`. **v5.19:** `form_variant` (полная / упрощённая — набор соотношений §7), `needs_review_fields`, `header_source` (сырой текст и уверенность полей шапки), правка наименования (`organization_name_original_value`, `organization_name_modified`); результат проверок §7 и предупреждения хранятся в `document_issue` как пришли от сервиса, ядро пересчитывает их только после правок. | → `document`, `recognition_job`, `app_user`; ← `column`, `line` | ТЗ 6.2/6.3; FNR01; макет |
|
||||
| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. **v5.19:** `period_kind` (абсолютная / относительная подпись бланка / без даты), `label_confidence`. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 |
|
||||
| `financial_statement_line` | Строка формы как в источнике: код (или его сырой текст) и наименование как распознано. **v5.19:** все строки источника — с кодом (`ok`, повторы кода тоже) и без (`unreadable`, `missing`, `detail` — расшифровка под родителем `parent_row_index`); порядок `row_index`, положение `source_ref`, `needs_review` неуверенного кода, подтверждение кода `is_confirmed`. Раскладка экрана — по коду «как на бумаге». | → `meta`; ← `value`, `document_issue.line_ids` | ТЗ 6.2, Прил. 2 |
|
||||
| `financial_statement_line` | Строка формы как в источнике: код (или его сырой текст) и наименование как распознано. **v5.19:** все строки источника — с кодом (`ok`, повторы кода тоже) и без (`unreadable`, `missing`, `detail` — расшифровка под родителем `parent_row_index`); порядок `row_index` (`NULL` ⇔ строка создана правкой аналитика), положение `source_ref`, `needs_review` неуверенного кода, подтверждение кода `is_confirmed`. Раскладка экрана — по коду «как на бумаге». | → `meta`; ← `value`, `document_issue.line_ids` | ТЗ 6.2, Прил. 2 |
|
||||
| `financial_statement_value` | Число в ячейке (строка × колонка): сумма, уверенность, `is_recognized`, ручная правка с исходным значением. **v5.19:** подсветка — только `needs_review` (хранится: уверенность, `is_recognized` и §7 сведены сервисом, ядро пересчитывает после правок); `source_text` — сырой текст источника; `is_confirmed` / `confirmed_by/at` — подтверждено без правки. (v5.13 этот флаг убирала как производный.) | → `line`, `column`, `app_user` | ТЗ 6.2, Прил. 2 |
|
||||
| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа формы: вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) |
|
||||
| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа формы: вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке (`column_id` или `column_index`) и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `codes`, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) |
|
||||
|
||||
## Часть 07 — Консолидация
|
||||
|
||||
|
||||
@ -3,8 +3,10 @@
|
||||
Issue: arm-ka-docs#116. Статус: **финальная версия** — целевая картина по шагам 1–9 согласована с владельцем
|
||||
(решения Р-1…Р-19, 17.10.2026) и прошла три раунда независимого ревью (`review-2026-10-17.md`,
|
||||
`review-2026-10-17-closure.md`, `review-2026-10-17-blind.md`, `review-2026-10-17-closure-2.md`,
|
||||
`review-2026-10-17-blind-2.md`); следующий шаг — спека (дельты OpenSpec, контракт v2.0, §11/§12 в
|
||||
`05-domain-rules.md`, DBML v5.19).
|
||||
`review-2026-10-17-blind-2.md`). Спека (дельты OpenSpec, контракт v2.0, §7/§9/§11 в `05-domain-rules.md`, DBML v5.19)
|
||||
написана 18.10.2026 (решения S-1…S-25), прошла ревью вслепую (`review-2026-10-18-spec.md`, находки K-1…K-25) и
|
||||
доработана по нему (решения владельца Р-20…Р-22, решения S-26…S-52 ниже). Где текст дизайна выше расходится с
|
||||
решениями Р-20…Р-22 и S-n, действуют они и спека.
|
||||
|
||||
## Цель
|
||||
|
||||
@ -773,7 +775,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
`07-open-questions.md` (Р-15). Вопрос о сопоставлении файлового наименования со справочным закрыт Р-16: для
|
||||
неуверенных кодов это условие `RECOGNIZED`, для остальных — только показ справочного имени.
|
||||
|
||||
### Решения владельца (Р-1…Р-19, 17.10.2026)
|
||||
### Решения владельца (Р-1…Р-19 — 17.10.2026, Р-20…Р-22 — 18.10.2026)
|
||||
|
||||
- **Р-1** (шаг 1, п. 2) — **копия по скрипту.** Платформа не собирает Python-пакеты и не кладёт их в свой Nexus, поэтому
|
||||
общий код — dataclass-ы шага 1 (сериализуемые в json), шаги 2–8, словари §7, периодов, заголовков форм, перечни и
|
||||
@ -855,6 +857,24 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
предупреждений `error` уровня файла (потерянный json или таблица ИС блокируют, `needs_check` соседнего документа
|
||||
из-за одной ячейки — нет).
|
||||
|
||||
- **Р-20** (18.10.2026; ревью спеки K-4, повторы кодов) — законные повторы кодов (в реальных ББ 2025 — 1220 «НДС,
|
||||
начисленный по отгрузке» и 1520 «Обязательство по аренде») допускаются в упорядоченном перечне кодов: структура
|
||||
подтверждает обе копии. Правило Р-16 «повтор с неуверенной копией — `needs_review`» применяется только к повтору,
|
||||
которого нет в перечне. Словарь имён ядра — список имён (синонимов) на код; загрузчик перестаёт запрещать повтор кода в
|
||||
этом смысле (сверка наименования — с любым из имён). В спеке: перечень `contracts/statement-code-orders.yaml`
|
||||
(`repeats`: у `fsbu4` — 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО, см. S-28), §11.9, §7 «Полный
|
||||
состав формы», спеки `statement-normalization` и `arm-core`.
|
||||
- **Р-21** (18.10.2026; ревью спеки K-4, мягкие соотношения ОФР) — неуверенное значение подтверждается и мягким
|
||||
соотношением, если оно сошлось точно (расхождение 0 или в пределах допуска ±1; не «в пределах округления»). Применяется
|
||||
к 2400, 2410, 2411, 2412, 2460 и прочим строкам, у которых есть только мягкие соотношения. Знак для `abs_terms` — по
|
||||
прежнему правилу конвенции колонки. В спеке: §7, §11.9, требование «Шаг 8 — `needs_review` значения».
|
||||
- **Р-22** (18.10.2026; решения писателя S-1…S-25) — приняты все, с оговорками ревьюера из таблицы S-n в
|
||||
`review-2026-10-18-spec.md`: S-4 — синонимы на код и исключение «итого по разделу» из правила вхождения; S-5 — в
|
||||
упрощённом ОФР `2400` мягкое, как в полном, ББ — жёсткие; S-7 — OQ-148 закрывается этим решением; S-10 / S-22 — одна
|
||||
формулировка; S-13 — двойники внутри детектора нумерации и допуск одного пропуска; S-15 — экранирование имени листа в
|
||||
Excel-`ref`; S-17 — доопределить по K-23; S-19 — `codes` и `column_index` у `document_issue`; S-21 — по K-2 / K-21.
|
||||
Явно утверждены владельцем S-4, S-5, S-7, S-10, S-12, S-13, S-14, S-20, S-22, S-25 (отмечены ниже).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
Решения агента 17.10.2026, принятые при оформлении целевой картины без отдельного обсуждения с владельцем; все
|
||||
@ -936,14 +956,16 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
27. **S-4. Совпадение наименований (Р-16):** нижний регистр, `ё` → `е`, всё кроме букв и цифр — пробел; совпало при
|
||||
равенстве, вхождении более короткой строки (≥ 5 символов) или `difflib.SequenceMatcher` ≥ 0,8 с любым справочным
|
||||
именем кода в любой редакции; нет кода в словаре имён — не совпало. Цена: ложное несовпадение — `NEEDS_CHECK` и
|
||||
ручное подтверждение; ложное совпадение маловероятно у разных статей одного раздела.
|
||||
ручное подтверждение; ложное совпадение маловероятно у разных статей одного раздела. **Утверждено владельцем 18.10.2026 (Р-22)** с оговоркой: у кода несколько имён (синонимы, Р-20); если
|
||||
одно из имён начинается с «итого по разделу», совпадение — только равенство (S-49).
|
||||
28. **S-5. Формулы упрощённого набора** по бланку приказа 66н, прил. 5 (ББ: `1600 = 1150 + 1170 + 1210 + 1230 + 1250`,
|
||||
`1700 = 1300 + 1350 + 1360 + 1410 + 1450 + 1510 + 1520 + 1550`, `1600 = 1700`; ОФР: `2400 = 2110 − 2120 − 2330 + 2340
|
||||
− 2350 − 2410`), все жёсткие; `ASSUMPTION` до образца (OQ-166). Цена: неверная формула — `needs_check` упрощённых форм.
|
||||
− 2350 − 2410`), все жёсткие; `ASSUMPTION` до образца (OQ-166). Цена: неверная формула — `needs_check` упрощённых форм. **Утверждено владельцем 18.10.2026 (Р-22) с изменением:** ОФР `2400 = …` упрощённого набора —
|
||||
мягкое, как `2400` полного (знак 2410, OQ-12; ревью K-11); соотношения ББ — жёсткие.
|
||||
29. **S-6. Словари подписей:** кода — «код», «коды», «код строки», «код показателя»; наименования — подпись,
|
||||
начинающаяся с «наименование». Цена: пополняется словарём.
|
||||
30. **S-7. Прочерк** (`-`, `–`, `—`, `−` без цифр) — пустое значение, `is_recognized = true` (как пустая ячейка, Р-2);
|
||||
OQ-148 — допущение. Цена: тире-мусор на месте цифры не подсвечивается сам по себе, его ловит §7 (пустой член).
|
||||
OQ-148 — допущение. Цена: тире-мусор на месте цифры не подсвечивается сам по себе, его ловит §7 (пустой член). **Утверждено владельцем 18.10.2026 (Р-22):** OQ-148 закрыт этим решением.
|
||||
31. **S-8. Каталог кодов предупреждений** (таблица §11.9 `05-domain-rules.md`); `code` в контракте — строка
|
||||
`^[A-Z][A-Z0-9_]*$`, не перечисление (новый код — не MAJOR); `VALUE_OUT_OF_RANGE` — `info` (значение и так
|
||||
`needs_review`). Цена: опечатка кода не ловится схемой — ловится тестами пакета.
|
||||
@ -951,42 +973,163 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
33. **S-10. `needs_review` полей шапки** — массив `header.needs_review_fields` в контракте и `jsonb` в
|
||||
`financial_statement_meta`; сейчас туда попадает только `report_date`: сомнения в ИНН и организации — `info` по
|
||||
дизайну, `needs_review` у них дал бы `needs_check` вопреки «НПЗ ОФР без ИНН остаётся `done`». Цена: экран подсвечивает
|
||||
ИНН и организацию по `issues` с `field`, а не по `needs_review`.
|
||||
ИНН и организацию по `issues` с `field`, а не по `needs_review`. **Утверждено владельцем 18.10.2026 (Р-22)**; формулировка одна во всех документах:
|
||||
`needs_review_fields` — поля, которые держат документ в `needs_check`; сырой текст и уверенность всех полей шапки
|
||||
хранятся (`header_source`, S-33).
|
||||
34. **S-11. Значения `period_kind`:** `absolute`, `relative:current`, `relative:prev_year_end`,
|
||||
`relative:prev_prev_year_end`, `relative:prev_period`, `none`; «За аналогичный период» — эффективная дата − 1 год
|
||||
(29.02 → 28.02). Цена: новая относительная подпись — новое значение (MINOR).
|
||||
35. **S-12. Ожидаемые даты неуверенной подписи колонки** — эффективная дата, она же − 1 и − 2 года **и 31 декабря
|
||||
предыдущего и предшествующего ему года**. Основание: у квартального ББ колонки «на 31.12 прошлого года», и набор
|
||||
дизайна (дата, − 1 год, − 2 года) давал бы `error` на каждой такой форме. Цена: чуть шире окно подтверждения.
|
||||
дизайна (дата, − 1 год, − 2 года) давал бы `error` на каждой такой форме. Цена: чуть шире окно подтверждения. **Утверждено владельцем 18.10.2026 (Р-22).**
|
||||
36. **S-13. Строка нумерации** — не меньше трёх непустых ячеек, все целые, подряд с 1 слева направо. Цена: нумерация с
|
||||
пропуском не распознаётся (строка уйдёт в данные со статусом).
|
||||
пропуском не распознаётся (строка уйдёт в данные со статусом). **Утверждено владельцем 18.10.2026 (Р-22) с оговоркой:** двойники внутри детектора и допуск
|
||||
одного пропуска — уточнено S-40.
|
||||
37. **S-14. Признак повторной шапки** — подпись кода в строке или ≥ 2 непустых ячеек, совпавших с шапкой первого
|
||||
сегмента. Цена: шапка без «Код» и с другими подписями не начнёт сегмент.
|
||||
сегмента. Цена: шапка без «Код» и с другими подписями не начнёт сегмент. **Утверждено владельцем 18.10.2026 (Р-22).**
|
||||
38. **S-15. `ref` и `row_index`:** `row_index` с 0; Excel `ref` строки — адрес ячейки столбца кодов (без столбца —
|
||||
первой непустой ячейки); разлепленные `row:<i>#<k>` с k от 1; у разлепленных строк `line_code_text` и `text` —
|
||||
k-е части. Цена: нулевая.
|
||||
k-е части. Цена: нулевая. Оговорка Р-22: имя листа в Excel-`ref` экранируется по правилу Excel (кавычки, удвоенный апостроф),
|
||||
примеры контракта исправлены.
|
||||
39. **S-16. Уверенность кода с заменой двойника** — не выше 99 (так «неуверенный» виден из одного поля). Цена: нулевая.
|
||||
40. **S-17. Формат PATCH `lines`** — один элемент = одно действие (`line_code` | `line_status: "detail"` | `confirmed`);
|
||||
коды 422 `INVALID_LINE_CODE`, `LINE_CODE_REQUIRED`, `INVALID_LINE_PATCH`, `UNKNOWN_LINE`, `UNKNOWN_COLUMN`; правка суммы
|
||||
по `line_code` при наличии строки `ok` — 422 `UNKNOWN_CELL`. Цена: имена кодов меняются до реализации без последствий.
|
||||
по `line_code` при наличии строки `ok` — 422 `UNKNOWN_CELL`. Цена: имена кодов меняются до реализации без последствий. Оговорка Р-22: доопределено по K-23 (S-47); 409 `LINE_EXISTS` переименован в `LINE_CODE_TAKEN`
|
||||
(S-43).
|
||||
41. **S-18. Детали 409:** `LINES_UNRESOLVED` — `detail.lines[] {line_id, row_index, line_code_text}`; `DUPLICATE_FORM` —
|
||||
`detail.document_id`. Цена: нулевая.
|
||||
42. **S-19. Имена DBML v5.19:** `document.file_ordinal` (с 0, уникален в файле), `financial_statement_meta.needs_review_fields`
|
||||
(`jsonb`), `document_issue.line_ids` (`jsonb` — сверка ORM↔DBML не знает массивов), enum'ы `statement_line_status`,
|
||||
`statement_form_variant`, `document_issue_kind`, `issue_severity`, `issue_source`; индекс `(meta_id, line_code)` остаётся
|
||||
неуникальным; у строки `is_confirmed` без `confirmed_by/at` (строго по дизайну, кто — в аудите). Новые колонки — последними
|
||||
в таблицах (как их добавит миграция). Цена: нулевая до миграции.
|
||||
в таблицах (как их добавит миграция). Цена: нулевая до миграции. Оговорка Р-22: у `document_issue` добавлены `codes` и `column_index` (S-31, S-35); висячий
|
||||
`line_id` удалённой шаблонной строки убирается из `line_ids`.
|
||||
43. **S-20. `related_group.crm_id` уникален** (как у ГК и ЮЛ), повтор — 409 `CRM_ID_EXISTS`. Цена: две группы с одним ID
|
||||
CRM завести нельзя; так и должно быть.
|
||||
CRM завести нельзя; так и должно быть. **Утверждено владельцем 18.10.2026 (Р-22).**
|
||||
44. **S-21. Значения по умолчанию при приёме 1.x:** `ref = null`, `needs_review` строки `false`, `period_kind` — `absolute`
|
||||
при дате иначе `none`, `form_variant = null`, строки-предупреждения — `LEGACY_WARNING` `info`. Цена: на окне выкладки
|
||||
документы 1.x без новых сигналов качества.
|
||||
документы 1.x без новых сигналов качества. Оговорка Р-22: по K-2 / K-21 — S-36.
|
||||
45. **S-22. Статус документа ОСВ** — `done`, когда контрольные суммы §1.4 листа сошлись, иначе `needs_check` (прежнее
|
||||
правило конверта, применённое к листу). Цена: нулевая — конверт ведёт себя как сейчас.
|
||||
правило конверта, применённое к листу). Цена: нулевая — конверт ведёт себя как сейчас. **Утверждено владельцем 18.10.2026 (Р-22)**; формулировка «контрольные суммы §1.4 листа сошлись»
|
||||
теперь одна в контракте, §11, спеках `recognition-contract` и `tabular-recognition-service`.
|
||||
46. **S-23. `service.statements_version` обязателен**, если в результате есть `statementDocument` (схема проверяет). Цена:
|
||||
результат только с ОСВ может его не нести.
|
||||
47. **S-24. Редакция перечня кодов в сервисах** — `fsbu4` при эффективной дате после 2024-12-31 (то же правило, что
|
||||
`edition` ядра, OQ-145). Цена: досрочный ФСБУ 4/2023 — проверяются обе через «длиннейшую подпоследовательность».
|
||||
`edition` ядра, OQ-145). Цена: досрочный ФСБУ 4/2023 — проверяются обе через «длиннейшую подпоследовательность». Уточнено S-26: проверяются всегда обе, дата решает только ничью.
|
||||
48. **S-25. Квартал периода (условие `RECOGNIZED`):** дата не конец квартала → `NEEDS_CHECK` (как D-6), ручное
|
||||
подтверждение — 409 `PERIOD_REQUIRED`. Основание: оговорка дизайна «иначе документ `RECOGNIZED`, но не подтверждаемый»
|
||||
противоречит автоподтверждению `RECOGNIZED`. Цена: нулевая — поведение как сейчас.
|
||||
противоречит автоподтверждению `RECOGNIZED`. Цена: нулевая — поведение как сейчас. **Утверждено владельцем 18.10.2026 (Р-22).**
|
||||
|
||||
### Решения при доработке спеки по ревью K-1…K-25 (18.10.2026)
|
||||
|
||||
Решения агента при закрытии находок `review-2026-10-18-spec.md` там, где ни ревьюер, ни владелец не задали значение,
|
||||
или где агент отступил от предложения ревьюера. Маркер `S-n` продолжает нумерацию; все обратимы правкой спеки, словаря
|
||||
или DBML до реализации.
|
||||
|
||||
49. **S-26. Редакции перечня кодов — всегда обе** (K-5): для неуверенного кода считаются наибольшие
|
||||
подпоследовательности по `66n` и `fsbu4`, берётся длиннейшая, эффективная дата решает только ничью. Основание:
|
||||
Межениновская за 3 кв. 2025 сдана на бланке 66н — выбор только по дате (S-24) не подтвердил бы её 1120; формула
|
||||
дизайна «редакция — где длиннее, при равенстве — по дате» иначе не имеет смысла. Цена: на ББ с кодами, общими для
|
||||
обеих редакций, выбор безразличен; неверный выбор возможен только при равной длине — тогда решает дата, как в S-24.
|
||||
50. **S-27. Перечень кодов — данные в docs** (K-5): источник — `contracts/statement-code-orders.yaml` (обращение «как
|
||||
контракт»: общие данные сервисов и ядра), в пакете — байтовая копия `rules/statement_code_orders.yaml`, идентичность
|
||||
проверяет `sync-statements --check`; форма файла — `scripts/validate_contracts.py`, для этого в `make venv` docs
|
||||
добавлен PyYAML. Цена: ещё одна зависимость docs-проверок (`make venv` нужно перезапустить); перенос источника в
|
||||
`arm-statements` — правкой одного требования.
|
||||
51. **S-28. Состав перечня — только подтверждённые образцом коды** (K-5): шаблоны банка + коды печатных форм 1С и сканов
|
||||
из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; раздел III НКО
|
||||
(1310 «Паевой фонд» … 1370, 1300 — aep_3) — законный повтор `66n`; клиентский 1231 — в `66n` (Унагранде 2024).
|
||||
Коды без образца (1120 у `fsbu4`, 2421 / 2430 / 2450 у `66n`, упрощённые перечни) — OQ-168, OQ-166. Все 12
|
||||
последовательностей кодов из `fixtures` (Excel и OCR) укладываются в свою редакцию целиком. Цена: на скане с
|
||||
неподтверждённым кодом и неуверенностью ниже 100 — `needs_review` строки; у Excel и уверенного кода — только `info`.
|
||||
52. **S-29. Окно перехода контракта** (K-8): схемы 1.2 / 1.0 лежат в docs как `*.v1.schema.json` (байтовые копии,
|
||||
сервисы сверяются с ними без правки копий), примеры — `examples/v1/`; тест идентичности сервиса выбирает файл по
|
||||
`$id` копии (`…/v1` → `*.v1.schema.json`) — это шаг 0 выкладки в tabular-parser и scan-gateway до слияния docs.
|
||||
Отступление от предложения ревьюера «тест сверяет с MAJOR, который сервис пишет»: признак — `$id` копии, а не
|
||||
настройка сервиса, чтобы правило работало без знания кода сервиса. Цена: маленький PR в двух сервисах до слияния
|
||||
docs; без него их `make check` красный на окне.
|
||||
53. **S-30. Механика `sync-statements`** (K-9): копия — `api/arm_statements/` (импорт как `src`), тесты пакета в
|
||||
`testpaths` хоста, `mypy src arm_statements`; маркер — файл `api/arm_statements/VERSION` с полным хэшем; `--check`:
|
||||
`0` синхронно, `10` отстала (зелёный с предупреждением — иначе любой посторонний PR в сервис требовал бы
|
||||
подтягивать чужие изменения пакета), `20` прямая правка / чужая версия / расхождение перечня кодов / код словаря ядра
|
||||
вне перечня, `2` нет checkout или аргумент. Без checkout `arm-statements` `check-full` красный (код 2): это проверка
|
||||
перед PR, а не `make check`. Цена: отставшая копия проходит `check-full`; видна в описании PR.
|
||||
54. **S-31. `column_index` — номер столбца сетки кандидата** (K-6): у OCR — `column_index` ИС как есть (разреженный), у
|
||||
Excel — номер столбца листа первого сегмента с 0; у `document_issue` — `column_id` для колонки формы и
|
||||
`column_index` для столбца вне колонок; `line_refs` — только строки `lines[]`. Отступление от варианта «порядковый
|
||||
номер колонки значений»: иначе пропущенная колонка (`COLUMN_SKIPPED`) не имела бы номера. Примеры контракта
|
||||
приведены к правилу (Excel — 3, 4; OCR — 2). Цена: номера не с 0 и не подряд — экран и ядро не должны считать их
|
||||
позициями.
|
||||
55. **S-32. `TABLE_PARSE_ERROR` и `SHEET_OTHER` — предупреждения файла** (K-7): с `locator`; документа нет;
|
||||
`TABLE_PARSE_ERROR` (`error`) блокирует `RECOGNIZED` всех документов файла, как `JSON_FAILED`. Цена: один упавший
|
||||
разбор таблицы держит весь файл на ручном подтверждении.
|
||||
56. **S-33. Шапка: `header_source jsonb` + прежние `*_original_value`** (K-3): сырой текст и уверенность всех десяти
|
||||
полей — одной колонкой `{поле: {text, confidence}}`; `inn_original_value`, `report_date_original_value`,
|
||||
`units_original_value` заполняются как прежде, `inn_confidence` остаётся. Отступление от варианта «колонки на каждое
|
||||
поле»: 20 колонок ради подсказки на экране. Цена: поиск по сырому тексту поля шапки в SQL — через `jsonb`.
|
||||
57. **S-34. `organization_name` в PATCH шапки** (K-12): непустая строка до 1000 символов, `organization_name_modified`,
|
||||
`organization_name_original_value` (новые колонки DBML), пересчёт `counterparty_check`, аудит; снимает
|
||||
`NO_ORGANIZATION_NO_INN`. Выбран вариант «добавить», а не «убрать из правила снятия»: иначе форма без шапки
|
||||
снималась бы только перезагрузкой. Цена: правка наименования влияет на сверку контрагента по алиасам.
|
||||
58. **S-35. Коды нарушений в `document_issue`** (K-13): `relation` → `RELATION_FAILED` / `error`, `soft` →
|
||||
`RELATION_SOFT` / `info`, колонка `codes jsonb`, `line_ids` — все строки `ok` кодов. Цена: нулевая.
|
||||
59. **S-36. Формы без `checks`** (K-2, K-21): у результата 1.x и у форм до миграции ядро один раз считает §7 копией
|
||||
пакета (`form_variant = null` — полный набор) и пишет `document_issue` (`source = core`): при приёме 1.x и **в
|
||||
ревизии миграции** (ревизия импортирует копию `arm_statements`). D-6 «проверки сошлись» для 1.x — из этого расчёта;
|
||||
`GET` и `confirm` читают нарушения только из `document_issue`. Предупреждения маппера 1.x по документу —
|
||||
`MAPPING_WARNING` `info`; строки `metrics.warnings` 1.x — `LEGACY_WARNING` в `recognition_job.metrics` (уровень
|
||||
файла, не `document_issue`); `STATEMENTS_VERSION_MISMATCH` при `statements_version = null` не пишется. Цена: ревизия
|
||||
миграции зависит от кода пакета на момент миграции (старая ревизия при смене пакета считает по новым правилам — для
|
||||
разовой миграции приемлемо); отказ от миграционного расчёта дал бы потерю нарушений у существующих форм (K-2).
|
||||
60. **S-37. Признак строки правки — `row_index IS NULL`** (K-1): маппер 1.x и 2.0 всегда пишет `row_index`; миграция
|
||||
ставит `NULL` строкам с `line_code_original_value IS NULL`; `line_code_original_value` — код из источника, правками
|
||||
не меняется. Отступление от дизайна («смена кода — прежний в `line_code_original_value`»): при первой смене это то
|
||||
же значение, а при повторной правке исходный код источника не теряется. Цена: «предыдущий код» после двух правок —
|
||||
только в аудите.
|
||||
61. **S-38. Функция статуса и «содержимое» строки** (K-14): вход — данные без типов источника (`layout_found`, у ядра —
|
||||
есть строка из источника; `has_organization_or_inn`; `period_known`; статусы и `needs_review`; число жёстких
|
||||
нарушений; `accepted`; уровни предупреждений). Содержимое строки — цифра в ячейке кода, сумма в значениях или
|
||||
непустая ячейка значений с уверенностью ниже 100; строка `unreadable` без значений («44 30\nI |» aep_6) — строка
|
||||
данных: документ `needs_check`, подтверждение не блокирует. Цена: такая строка требует от аналитика «Это
|
||||
расшифровка» или кода, хотя сумм в ней нет.
|
||||
62. **S-39. Столбец кодов ищется всегда, и у `DISCLOSURE`** (K-10): `type_hint = DISCLOSURE` при кодах ББ/ОФР → тип по
|
||||
кодам + `TYPE_CONFLICT`; у `DISCLOSURE` шаги 3–7 не выполняются, `columns` / `lines` — пустые массивы. Цена: лишний
|
||||
проход поиска столбца по пояснениям.
|
||||
63. **S-40. Детектор нумерации** (S-13 с оговоркой Р-22, K-15): ≥ 3 непустых ячеек, каждая после замены двойников —
|
||||
целое; возрастают на 1, начинаются с 1 или 2, допускается один пропуск. Цена: настоящая строка данных из трёх
|
||||
маленьких целых подряд («1 | 2 | 3» в суммах) станет нумерацией — на формах ББ/ОФР не встречалась.
|
||||
64. **S-41. `LINES_UNRESOLVED` — сумма или текст** (K-16): блокирует строка `unreadable` / `missing`, у которой хотя бы
|
||||
в одной ячейке `amount` или `string_value` не `null`. Цена: строка с мусором вместо суммы требует разбора.
|
||||
65. **S-42. Подтверждение неуверенной даты отчёта** (K-17): PATCH `header.report_date` тем же значением у поля из
|
||||
`needs_review_fields` снимает его и `REPORT_DATE_UNCERTAIN`; у бланка 66н только с относительными подписями это
|
||||
единственный путь — признано в §11.9. Цена: аналитик должен явно подтвердить дату на каждом таком бланке.
|
||||
66. **S-43. Имена** (K-18): 409 `LINE_EXISTS` → `LINE_CODE_TAKEN` (имя занято сообщением 422 `UNKNOWN_CELL` в коде
|
||||
ядра); `period_end` колонки → `period_date` во всех документах (как в контракте и DBML); порядок `lines` в API —
|
||||
«по возрастанию кода, как сейчас», «как на бумаге» — раскладка экрана; `DISCLOSURE` — пустые массивы. Цена: нулевая.
|
||||
67. **S-44. Откат миграции** (K-19): гарантирован на пустой и совместимой с v5.18 БД; на несовместимых данных
|
||||
`downgrade` останавливается с ошибкой и ничего не удаляет. Цена: откат после появления данных v2.0 — только
|
||||
восстановлением из резервной копии.
|
||||
68. **S-45. «Того же периода»** (K-20) — та же пара `period_year`, `period_quarter` (как автозамена §9.5 п. 3); формы
|
||||
без квартала не считаются (их и так держит `PERIOD_REQUIRED`). Цена: нулевая.
|
||||
69. **S-46. Единицы и ОКЕИ** (K-22): словарь `statement_units.yaml` в пакете (написания и коды ОКЕИ 383 / 384 / 385);
|
||||
единицы по тексту, иначе по ОКЕИ; `okei` (+ `_text`, `_confidence`) — в `header` контракта v2.0 и `headerField`.
|
||||
Цена: ОКЕИ, противоречащий тексту единиц, не ловится (текст главнее).
|
||||
70. **S-47. Пробелы PATCH** (K-23, S-17): `confirmed: false` снимает подтверждение ячейки и кода; `line_status` принимает
|
||||
только `"detail"` (`"ok"` — 422 `LINE_CODE_REQUIRED`); обратного `detail` → `unreadable` / `missing` нет (исправляется
|
||||
назначением кода, исходное — перезагрузкой); `columns[].period_date = null` — 422 валидации; **акцепт сбрасывает
|
||||
правка данных** (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (`confirmed`) — нет. Последнее
|
||||
сужает правило Среза 3b «акцепт сбрасывается любой правкой формы» — вынесено владельцу (отчёт O-n). Цена: если
|
||||
владелец считает подтверждение правкой — одна строка спеки.
|
||||
71. **S-48. Страховка `done`** (K-24): `done` из результата при строке `unreadable` / `missing` со значением,
|
||||
`needs_review`, `relations_failed` или `error`-предупреждении документа → `NEEDS_CHECK` и `RESULT_STATUS_INCONSISTENT`
|
||||
`info`. Цена: проверка на каждом маппинге — линейная по строкам.
|
||||
72. **S-49. Словарь имён ядра** (Р-20, S-4): повтор кода в редакции разрешён — все имена кода синонимы; шаблонная строка
|
||||
и `reference_name` берут первое; «итого по разделу» — только равенство. Цена: справочное имя законного повтора на
|
||||
экране — первое имя кода, файловое — рядом.
|
||||
73. **S-50. Толкование Р-21:** мягкое соотношение, сошедшееся в пределах ±1, покрывает значения любого своего кода; у
|
||||
кода в несошедшемся жёстком соотношении подсветка остаётся (правило «все ячейки несошедшегося жёсткого»), поэтому
|
||||
на практике Р-21 действует на строки без жёстких соотношений, как сформулировал владелец. Цена: нулевая.
|
||||
74. **S-51. Ожидания на реальных сканах признаны в §11.9 и proposal** (K-4): большинство из шести OCR-документов
|
||||
останутся `needs_check`; формулировка гайда фронта смягчена («может стать `RECOGNIZED`»). Цена: нулевая.
|
||||
75. **S-52. Манифест tabular-parser** (K-25): в спеке `tabular-recognition-service` перечислены записи, получающие
|
||||
`1600 = 1700` (страницы 2 баланса ×3), и записи, чьи статусы `done` меняться не должны. Цена: нулевая.
|
||||
|
||||
@ -30,7 +30,10 @@
|
||||
предупреждения — структуры `{code, severity, message, line_refs, column_index, field}` вместо строк.
|
||||
- **Контракт `recognition-result.schema.json` v2.0** (MAJOR, Р-9) и конверт `recognition-result-message.schema.json`
|
||||
v2.0 (структурные `metrics.warnings`); запрос `recognition-request` остаётся v1.0. Путь ОСВ в tabular-parser
|
||||
меняется только в части структуры предупреждений и `document.status`.
|
||||
меняется только в части структуры предупреждений и `document.status`. На окне перехода схемы 1.x лежат рядом
|
||||
(`*.v1.schema.json`, примеры `examples/v1/`), тест идентичности сервиса сверяет копию со схемой своего MAJOR.
|
||||
- **Упорядоченные перечни кодов форм** — данные `contracts/statement-code-orders.yaml` (66н / ФСБУ 4, полная /
|
||||
упрощённая, законные повторы кода — Р-20); в пакет `arm_statements` — байтовой копией.
|
||||
- **arm-core:** DBML v5.19 и одна миграция (статусы и порядок строк, `needs_review`, `is_confirmed`, `source_text`,
|
||||
`period_kind`, `form_variant`, таблица `document_issue`, снятие уникальности `(meta_id, line_code)` и — для ББ/ОФР —
|
||||
`document (stored_file_id, document_type, osv_account_code)`, `recognition_job.statements_version`,
|
||||
@ -53,6 +56,8 @@
|
||||
|
||||
### Порядок выкладки — «сначала ядро» (обязателен)
|
||||
|
||||
0. tabular-parser и scan-gateway: тест идентичности копии контракта выбирает файл docs по MAJOR копии (`$id` `…/v1` →
|
||||
`*.v1.schema.json`) — до слияния шага 2, иначе их `make check` краснеет на окне перехода.
|
||||
1. `arm-statements` — пакет с тестами; `sync-statements` в docs.
|
||||
2. docs: контракт v2.0, DBML v5.19, правила (этот change).
|
||||
3. arm-core: миграция v5.19, маппер 2.0 рядом с маппером 1.x, приём ack и конвертов 1.x и 2.0, PATCH, экранные поля
|
||||
@ -62,7 +67,12 @@
|
||||
6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change).
|
||||
|
||||
Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs
|
||||
устаревают: каждый сервис обновляет копию в своём PR шага 5, ядро — в PR шага 3.
|
||||
сверяются с `*.v1.schema.json` (шаг 0); каждый сервис обновляет копию до v2.0 в своём PR шага 5, ядро держит копии обоих
|
||||
MAJOR с PR шага 3. Схемы `*.v1.schema.json` и `examples/v1/` удаляются вместе с маппером 1.x (шаг 6).
|
||||
|
||||
**Чего ждать на реальных сканах.** На шести OCR-документах `fixtures/ocr` после перехода большинство форм останутся
|
||||
`needs_check` (строки без кода с суммами, неуверенные значения вне соотношений, неуверенная дата); законные повторы кода
|
||||
и мягкие строки ОФР перестают держать документ (Р-20, Р-21). Доля `done` фиксируется в PR scan-gateway.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@ -93,21 +103,30 @@
|
||||
|
||||
### Modified Capabilities
|
||||
- `recognition-contract`: файл результата и конверт — v2.0; правила перехода (ядро принимает 1.x и 2.0, «сначала
|
||||
ядро»); статус и структурные предупреждения у всех документов, включая ОСВ.
|
||||
ядро»); статус и структурные предупреждения у всех документов, включая ОСВ; тест идентичности копии — по MAJOR.
|
||||
- `quality-checks`: `make check` охватывает копию `arm_statements` (тесты, `mypy`), `make check-full` — `statements-check`
|
||||
(`sync-statements --check`).
|
||||
|
||||
## Impact
|
||||
|
||||
- Новый репозиторий `arm-statements`; скрипт `sync-statements` и цель Makefile в docs (вызывается из `make check-full`
|
||||
потребителей, не из `make check`).
|
||||
потребителей, не из `make check`; коды 0 / 10 / 20 / 2).
|
||||
- docs: `contracts/statement-code-orders.yaml`, схемы `*.v1.schema.json` и `examples/v1/` на окне перехода;
|
||||
`scripts/validate_contracts.py` проверяет оба MAJOR и перечни кодов, `make venv` ставит PyYAML.
|
||||
- tabular-parser: `parser/statements/*` (кроме разбора книги и адаптера шага 1) уходит в общий код; словари
|
||||
`statement_checks.yaml`, `statement_periods.yaml`, `statement_headers.yaml` переезжают в пакет; путь ОСВ — структура
|
||||
предупреждений и `document.status`.
|
||||
предупреждений и `document.status`; манифест `api/tests/statements_manifest.yaml` обновляется: `failed_relations`
|
||||
страниц 2 баланса (`unagrande-co/2024-FY/Б.Б. Лист 2.xls`, `unagrande-group/2024-FY/ББ. Лист 2 (2).xls`,
|
||||
`unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls`) получают `1600 = 1700` («посчитанное» соотношение, пустая цель),
|
||||
статусы `done` не меняются.
|
||||
- scan-gateway: `normalizer/ocr/text|dates|amounts` становятся общими разборами, `layout|rows|header|document` — шагами
|
||||
2–6 общего кода; `api/rules/statement_lines.yaml` удаляется, `statement_labels.yaml` и `ocr_lookalikes.yaml` уходят в
|
||||
словари пакета.
|
||||
- arm-core: миграция v5.19 (`financial_statement_*`, `document`, `document_issue`, `recognition_job`,
|
||||
`related_group`), `result_mapping`, `statement_mapping`, `statements`, `documents`, `auto_confirm`, схемы API формы;
|
||||
`api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён.
|
||||
`related_group`; `document_issue` существующих форм — §7 копией пакета), `result_mapping`, `statement_mapping`,
|
||||
`statements`, `documents`, `auto_confirm`, схемы API формы; признак строки правки — `row_index IS NULL`;
|
||||
`api/rules/statement_checks.yaml` удаляется (копия словаря §7), `statement_lines.yaml` остаётся только словарём имён
|
||||
(несколько имён на код, загрузчик повтор кода не запрещает); PATCH шапки принимает `organization_name`.
|
||||
- arm-web: `widgets/DocumentReview` (экран [41]), типы API.
|
||||
- Решение v5.13 «`needs_review` считается, не хранится» пересматривается (design, Decisions № 17).
|
||||
- Риск окна выкладки: пока сервис шлёт 1.x, документы его конвейера получают статус по прежнему правилу D-6 и поля
|
||||
|
||||
@ -3,16 +3,17 @@
|
||||
### Requirement: Хранение результата формы v2.0 (DBML v5.19)
|
||||
Маппер результата `2.0` SHALL писать документ формы как пришёл, без повторной дедупликации колонок, кодов и значений:
|
||||
каждая строка результата — `financial_statement_line` (`row_index`, `source_ref` = `ref`, `line_status`, `line_code`,
|
||||
`line_code_text`, `line_code_confidence`, `line_name`, `line_name_confidence`, `parent_row_index` — `row_index` строки
|
||||
с `ref = parent_ref`, `needs_review`, `is_confirmed = false`); каждая пара «строка × колонка» — одна
|
||||
`financial_statement_value` (`amount`, `string_value`, `source_text` = `values[].text`, `confidence`, `is_recognized`,
|
||||
`needs_review`; значения, которого нет в результате, — пустая запись с `needs_review = false`); колонки —
|
||||
`financial_statement_column` с `period_kind`, `label_confidence`; шапка — `financial_statement_meta` с `form_variant`
|
||||
и `needs_review_fields`; предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего
|
||||
кода — `recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)`
|
||||
уникален там, где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным
|
||||
`document` с `file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится
|
||||
(Р-10); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
`line_code_original_value` = `line_code` результата, `line_code_text`, `line_code_confidence`, `line_name`,
|
||||
`line_name_confidence`, `parent_row_index` — `row_index` строки с `ref = parent_ref`, `needs_review`, `is_confirmed =
|
||||
false`); каждая пара «строка × колонка» — одна `financial_statement_value` (`amount`, `string_value`, `source_text` =
|
||||
`values[].text`, `confidence`, `is_recognized`, `needs_review`; значения, которого нет в результате, — пустая запись с
|
||||
`needs_review = false`); колонки — `financial_statement_column` с `period_kind`, `label_confidence`; шапка —
|
||||
`financial_statement_meta` с `form_variant`, `needs_review_fields` и `header_source` (раздел «Шапка формы»);
|
||||
предупреждения и нарушения документа — `document_issue` (источник `service`); версия общего кода —
|
||||
`recognition_job.statements_version`. Уникальность `(meta_id, line_code)` снята; `(meta_id, source_ref)` уникален там,
|
||||
где `source_ref` не `null`. Каждый `BALANCE` / `INCOME` результата SHALL становиться отдельным `document` с
|
||||
`file_ordinal` (порядковый номер документа в результате); вторая форма того же типа в файле хранится (Р-10); ОСВ по
|
||||
счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
|
||||
#### Scenario: Повтор кода сохраняется
|
||||
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
|
||||
@ -20,12 +21,43 @@
|
||||
|
||||
#### Scenario: Строка без кода
|
||||
- **WHEN** в результате строка `unreadable` с `line_code_text = "ИЧ,250"` и суммой
|
||||
- **THEN** строка хранится с `line_code = null`, `line_status = unreadable`, `line_code_text = "ИЧ,250"`
|
||||
- **THEN** строка хранится с `line_code = null`, `line_code_original_value = null`, `line_status = unreadable`,
|
||||
`line_code_text = "ИЧ,250"` и `row_index` из результата
|
||||
|
||||
#### Scenario: Два баланса в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать)
|
||||
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
|
||||
|
||||
### Requirement: Признак строки, созданной правкой
|
||||
Строка формы SHALL считаться созданной правкой аналитика (шаблонной, `origin = template`) тогда и только тогда, когда
|
||||
`row_index IS NULL`. Маппер `1.x` и маппер `2.0` SHALL всегда писать `row_index` (у `1.x` — по порядку строк результата с
|
||||
0), поэтому у строки из источника он не `null`. `line_code_original_value` SHALL хранить код строки из результата как
|
||||
пришёл (`null` у `unreadable` / `missing` / `detail` и у строк, созданных правкой) и правками кода MUST NOT меняться.
|
||||
Признак `line_code_original_value IS NULL` для `origin` MUST NOT использоваться.
|
||||
|
||||
#### Scenario: Строка источника без кода — не шаблонная
|
||||
- **WHEN** у строки `unreadable` из результата `line_code_original_value = null` и `row_index = 7`
|
||||
- **THEN** `origin = source`; правила шаблонных строк (удаление без сумм, 409) к ней не применяются
|
||||
|
||||
#### Scenario: Назначение одного кода двум строкам источника
|
||||
- **WHEN** аналитик назначил код 1250 строке `unreadable` (`row_index = 7`, без сумм), а затем тот же код — строке
|
||||
`missing` (`row_index = 2`) с суммой
|
||||
- **THEN** обе строки `ok` с кодом 1250, ни одна не удалена, 409 нет (повтор кода законен, Р-4)
|
||||
|
||||
### Requirement: Шапка формы — сырой текст и уверенность
|
||||
Маппер `2.0` SHALL писать в `financial_statement_meta.header_source` объект `{"<поле>": {"text": <поле>_text,
|
||||
"confidence": <поле>_confidence}}` для всех полей шапки результата (`organization_name`, `inn`, `report_date`, `units`,
|
||||
`okud`, `okpo`, `okopf`, `okfs`, `okei`, `address`); разобранные значения — в одноимённые колонки; `*_original_value` —
|
||||
как прежде: `inn_original_value = inn`, `report_date_original_value = report_date_text`, `units_original_value =
|
||||
units_text`; `organization_name_original_value = organization_name`, `inn_confidence = inn_confidence`.
|
||||
`header_source` и `*_original_value` правками MUST NOT меняться. Маппер `1.x` заполняет `header_source` из полей шапки
|
||||
1.x. `StatementOut.header.source` SHALL отдавать `header_source`.
|
||||
|
||||
#### Scenario: Подсказка у поля шапки
|
||||
- **WHEN** в результате `organization_name_text = "ООО «Ромашкa»"`, `organization_name_confidence = 67`
|
||||
- **THEN** `header_source.organization_name = {"text": "ООО «Ромашкa»", "confidence": 67}`; экран показывает их в подсказке
|
||||
у поля
|
||||
|
||||
### Requirement: Агрегат формы по коду
|
||||
Все чтения формы «по коду» SHALL идти через одну чистую функцию «агрегат формы по коду» без хранения (DECISION-5):
|
||||
для кода и колонки — сумма `amount` строк `ok` с этим кодом (`null`, если ни одной непустой), список `line_id`,
|
||||
@ -38,17 +70,24 @@
|
||||
- **THEN** межформенная проверка берёт 1370 = 800
|
||||
|
||||
### Requirement: Условие `RECOGNIZED` при приёме 2.0
|
||||
При приёме `2.0` статус документа формы SHALL быть `RECOGNIZED` ⇔ `document.status = done` ∧ в `metrics.warnings` файла
|
||||
нет предупреждений уровня `error` ∧ `counterparty_check ≠ MISMATCH` ∧ период — конец квартала (`period_year` и
|
||||
`period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом (`line_code_confidence < 100`) распознанное
|
||||
наименование совпало со справочным по коду ∧ в файле нет второй формы того же типа и периода; иначе `NEEDS_CHECK`.
|
||||
Статус конверта и `needs_check` соседнего документа в условие MUST NOT входить (Р-19). Проверки §7, `needs_review` и
|
||||
предупреждения сервиса при загрузке MUST NOT пересчитываться. Совпадение наименований: обе строки приводятся к нижнему
|
||||
регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел, пробелы схлопываются; совпало, если строки равны, или более
|
||||
короткая (не короче 5 символов) входит в более длинную, или отношение сходства `difflib.SequenceMatcher` ≥ 0,8; с
|
||||
любым справочным именем кода в любой редакции словаря ядра. Кода нет в словаре имён или наименование пусто — не
|
||||
совпало. Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка).
|
||||
`RECOGNIZED` SHALL, как и прежде, автоподтверждаться (OQ-157).
|
||||
При приёме `2.0` статус документа формы SHALL быть `RECOGNIZED` ⇔ `document.status = done` и он согласован с
|
||||
содержимым документа ∧ в `metrics.warnings` файла нет предупреждений уровня `error` ∧ `counterparty_check ≠ MISMATCH` ∧
|
||||
период — конец квартала (`period_year` и `period_quarter` определены) ∧ у каждой строки `ok` с неуверенным кодом
|
||||
(`line_code_confidence < 100`) распознанное наименование совпало со справочным по коду ∧ в файле нет второй формы того
|
||||
же типа и периода (та же пара `period_year`, `period_quarter`; формы без квартала не считаются); иначе `NEEDS_CHECK`.
|
||||
**Согласованность:** `done` противоречит содержимому, если есть строка `unreadable` / `missing` со значением (`amount` или
|
||||
`string_value` не `null`), строка, значение или поле шапки с `needs_review`, `checks.relations_failed` или предупреждение
|
||||
документа уровня `error`; тогда ядро считает документ `needs_check` и пишет `document_issue`
|
||||
`RESULT_STATUS_INCONSISTENT` (`info`, источник `core`). Статус конверта и `needs_check` соседнего документа в условие
|
||||
MUST NOT входить (Р-19). Проверки §7, `needs_review` и предупреждения сервиса при загрузке MUST NOT пересчитываться.
|
||||
|
||||
**Совпадение наименований:** обе строки приводятся к нижнему регистру, `ё` → `е`, всё, кроме букв и цифр, — пробел,
|
||||
пробелы схлопываются; совпало, если строки равны, или более короткая (не короче 5 символов) входит в более длинную, или
|
||||
отношение сходства `difflib.SequenceMatcher` ≥ 0,8; сравнение — с **любым** справочным именем кода (у кода может быть
|
||||
несколько имён — синонимы и законные повторы, Р-20) в любой редакции словаря ядра. Если хотя бы одно из имён начинается с
|
||||
«итого по разделу», совпадение — только равенство. Кода нет в словаре имён или наименование пусто — не совпало.
|
||||
Несовпадение — `document_issue` `NAME_MISMATCH` (`info`, источник `core`, строка). `RECOGNIZED` SHALL, как и прежде,
|
||||
автоподтверждаться (OQ-157).
|
||||
|
||||
#### Scenario: Ячейка соседнего документа
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check`, предупреждений файла уровня `error` нет, ИНН совпал, период —
|
||||
@ -56,7 +95,7 @@
|
||||
- **THEN** ББ `RECOGNIZED` и автоподтверждается, ОФР — `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Потерянный json
|
||||
- **WHEN** все документы `done`, но в `metrics.warnings` файла `JSON_FAILED` уровня `error`
|
||||
- **WHEN** все документы `done`, но в `metrics.warnings` файла `JSON_FAILED` (или `TABLE_PARSE_ERROR`) уровня `error`
|
||||
- **THEN** все документы формы файла `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Подмена неуверенного кода на соседа
|
||||
@ -64,6 +103,20 @@
|
||||
- **THEN** наименование не совпало со справочным «Результаты исследований и разработок», документ `NEEDS_CHECK`,
|
||||
`NAME_MISMATCH` у строки
|
||||
|
||||
#### Scenario: Законный повтор с другим именем
|
||||
- **WHEN** строка 1220 (уверенность 50) с наименованием «НДС, начисленный по отгрузке», а в словаре имён `fsbu4` у 1220
|
||||
есть это имя вторым
|
||||
- **THEN** наименование совпало, `NAME_MISMATCH` нет
|
||||
|
||||
#### Scenario: Подмена итога раздела
|
||||
- **WHEN** строка с кодом 1200 (уверенность 50) и наименованием «Итого по разделу I»
|
||||
- **THEN** наименование не совпало со справочным «Итого по разделу II» (для «итого по разделу» — только равенство),
|
||||
`NAME_MISMATCH`
|
||||
|
||||
#### Scenario: Сервис ошибся со статусом
|
||||
- **WHEN** документ пришёл `done`, но в нём строка `unreadable` с суммой
|
||||
- **THEN** документ `NEEDS_CHECK`, `RESULT_STATUS_INCONSISTENT` у документа; автоподтверждения нет
|
||||
|
||||
#### Scenario: Период не конец квартала
|
||||
- **WHEN** документ `done` с датой отчёта 15.08.2025
|
||||
- **THEN** квартал не определён, документ `NEEDS_CHECK` (как в D-6) и не автоподтверждается; ручное подтверждение —
|
||||
@ -71,16 +124,21 @@
|
||||
|
||||
### Requirement: Предупреждения и нарушения документа — `document_issue`
|
||||
Ядро SHALL хранить предупреждения и нарушения документа в `document_issue`: вид (`warning` | `relation` | `soft`), код,
|
||||
уровень (`error` | `info`), сообщение, список `line_id`, `column_id`, поле шапки, источник (`service` | `core`), для
|
||||
нарушений — текст соотношения, `expected`, `actual`. При загрузке пишутся предупреждения и `checks` результата
|
||||
(источник `service`) и проверки ядра уровня `info` (источник `core`): `CODE_NOT_IN_DICTIONARY` (кода нет в словаре
|
||||
имён), `CODE_NOT_IN_EDITION` (код не из перечня редакции по эффективной дате — по копии перечня `arm_statements`),
|
||||
`NAME_MISMATCH`. При перезагрузке файла записи документа удаляются вместе с ним. При пересчёте после правки записи
|
||||
`relation` и `soft` заменяются целиком независимо от источника; записи `warning` источника `service` сохраняются,
|
||||
кроме снимаемых правкой: правка `header.report_date` снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`,
|
||||
`REPORT_DATE_UNCERTAIN`; правка `header.inn` или `organization_name` — `NO_ORGANIZATION_NO_INN`; правка
|
||||
`columns[].period_date` — `COLUMN_WITHOUT_DATE` и `COLUMN_DATE_UNCERTAIN` этой колонки; прочие `error` сервиса снимаются
|
||||
только перезагрузкой файла.
|
||||
уровень (`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`), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`. При перезагрузке файла записи документа
|
||||
удаляются вместе с ним. При пересчёте после правки записи `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
|
||||
@ -90,107 +148,192 @@
|
||||
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
|
||||
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
|
||||
|
||||
#### Scenario: Пропущенная колонка
|
||||
- **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет
|
||||
- **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа
|
||||
|
||||
### Requirement: Документы без `checks` — результаты 1.x и формы до миграции
|
||||
Для формы без `checks` контракта 2.0 — принятой по результату `1.x` и существовавшей до миграции v5.19 — ядро SHALL
|
||||
**один раз** посчитать §7 копией `arm_statements` по текущим суммам (`form_variant = null` — полный набор) и записать
|
||||
нарушения в `document_issue` (источник `core`, виды `relation` / `soft`, как в требовании о `document_issue`): у `1.x` —
|
||||
при приёме результата, у существующих форм — в миграции v5.19. Условие D-6 «проверки сошлись» для `1.x` SHALL браться из
|
||||
этого расчёта. Предупреждения маппера `1.x` по документу (повтор кода, значение вне колонок) — `document_issue`
|
||||
`MAPPING_WARNING` (`info`, источник `core`); строки `metrics.warnings` результата `1.x` — в
|
||||
`recognition_job.metrics.warnings` структурами `{code: "LEGACY_WARNING", severity: "info", message}`. После правки такая
|
||||
форма пересчитывается общим кодом, как форма 2.0; `MAPPING_WARNING` и `LEGACY_WARNING` статус не держат.
|
||||
|
||||
#### Scenario: Мигрированная форма с нарушением
|
||||
- **WHEN** до миграции форма `NEEDS_CHECK` с несошедшимся `1600 = 1700` без акцепта; миграция v5.19 выполнена
|
||||
- **THEN** у формы запись `document_issue` `relation` `RELATION_FAILED` с `codes = ["1600", "1700"]`; экран показывает
|
||||
нарушение; `POST …/confirm` — 409 `CHECKS_NOT_PASSED`
|
||||
|
||||
#### Scenario: 1.x-документ после PATCH
|
||||
- **WHEN** форма принята по результату `1.2`, аналитик правит сумму 1230 так, что `1200 = 1210 + … + 1260` сходится
|
||||
- **THEN** ядро пересчитывает §7 и статус общим кодом, запись `relation` по 1200 заменена (её больше нет), статус
|
||||
пересчитан; `STATEMENTS_VERSION_MISMATCH` не пишется (у задания 1.x версии нет)
|
||||
|
||||
### Requirement: Пересчёт после правки
|
||||
После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review` и статус тем же общим кодом
|
||||
`arm_statements` по сохранённым данным (конвенция знака — по `source_text`), с правилами: `is_modified` или
|
||||
`is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed` строки ⇒ `needs_review`
|
||||
строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят.
|
||||
Отличие `statements_version` задания от версии копии ядра — `document_issue` `STATEMENTS_VERSION_MISMATCH` (`info`).
|
||||
Один PATCH — один пересчёт и одна запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт сбрасывается любой правкой, как и
|
||||
прежде.
|
||||
`arm_statements` по сохранённым данным (конвенция знака — по `source_text`; `form_variant = null` — полный набор), с
|
||||
правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или `is_confirmed`
|
||||
строки ⇒ `needs_review` строки = `false`; акцепт ⇒ жёсткие нарушения не учитываются; правленые ячейки в голосование
|
||||
знака не входят. Отличие `statements_version` задания от версии копии ядра — `document_issue`
|
||||
`STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Один PATCH — один пересчёт и одна
|
||||
запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты
|
||||
колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) акцепт MUST NOT сбрасывать.
|
||||
|
||||
#### Scenario: Подтверждение ячейки без правки
|
||||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||||
- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит
|
||||
- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит; акцепт
|
||||
проверок, если был, сохраняется
|
||||
|
||||
### Requirement: PATCH формы — строки, колонки, подтверждение ячеек
|
||||
`PATCH /documents/{id}/statement` SHALL принимать помимо `header` и `values` (сумма):
|
||||
- `values[] {line_id, column_id, confirmed: true}` — подтвердить ячейку без изменения значения;
|
||||
- `columns[] {column_id, period_date}` — дата колонке (пересчитывает `is_current` и период документа по правилу §9.5
|
||||
п. 2); правка `header.report_date` (в том числе `null`) пересчитывает `period_end` колонок с `period_kind =
|
||||
relative:*` общим кодом (`null` даёт `null`);
|
||||
#### Scenario: Снятие подтверждения
|
||||
- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением
|
||||
- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит
|
||||
|
||||
### Requirement: PATCH формы — строки, колонки, подтверждение, наименование организации
|
||||
`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); правка `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}` — подтвердить код.
|
||||
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить код или снять подтверждение.
|
||||
|
||||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||||
отметкой; `ok` → `ok` сменой кода (прежний — в `line_code_original_value`); `ok` с неуверенным кодом — подтверждением.
|
||||
Назначенный или подтверждённый код: `line_code_confidence = 100`, `line_code_modified = true`, `needs_review`
|
||||
строки = `false`; у `detail` `parent_row_index` пересчитывается как ближайшая строка `ok` выше по `row_index`. Код —
|
||||
4–5 цифр (иначе 422 `INVALID_LINE_CODE`), может совпадать с существующим; незнакомый словарю — `info`. Шаблонная строка
|
||||
с тем же кодом, созданная правкой аналитика с суммами, — 409 `LINE_EXISTS` (сначала очистить её суммы или выбрать
|
||||
другой код); без сумм — удаляется, её место занимает назначенная. `line_status = ok` без кода — 422
|
||||
`LINE_CODE_REQUIRED`; в одном элементе больше одного действия или ни одного — 422 `INVALID_LINE_PATCH`; строка не этого
|
||||
документа — 422 `UNKNOWN_LINE`; колонка не этого документа — 422 `UNKNOWN_COLUMN`. Правка суммы по `line_code` (для
|
||||
достроенной строки) при наличии строки `ok` с этим кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления строк
|
||||
нет. Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`. Аудит
|
||||
`RECOGNIZED_DATA_EDIT` с `before/after` по коду, статусу, родителю и подтверждениям. Ограничения состояния — как
|
||||
прежде (`DOCUMENT_READONLY`, `FILE_SUPERSEDED`, `DOCUMENT_NOT_STATEMENT`).
|
||||
отметкой; `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` не меняется; у `detail` `parent_row_index` пересчитывается
|
||||
как ближайшая строка `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` (для достроенной строки)
|
||||
при наличии строки `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`).
|
||||
|
||||
#### Scenario: Назначение кода строке-мусору (AURORA-1462)
|
||||
- **WHEN** аналитик назначает строке `unreadable` «ИЧ,250» код 1250
|
||||
- **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`; §7 и статус пересчитаны
|
||||
`line_code_original_value` остаётся `null`, `row_index` остаётся 2 (`origin = source`); §7 и статус пересчитаны
|
||||
|
||||
#### Scenario: Отметка расшифровкой
|
||||
- **WHEN** аналитик отмечает строку `missing` с суммой как расшифровку
|
||||
- **THEN** `line_status = detail`, строка выходит из сумм и не блокирует подтверждение
|
||||
|
||||
#### Scenario: Код занят шаблонной строкой с суммами
|
||||
- **WHEN** аналитик назначает строке `unreadable` код 1250, а строка 1250 создана правкой аналитика и имеет сумму
|
||||
- **THEN** 409 `LINE_EXISTS`, ничего не изменено
|
||||
- **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: Статус ok без кода
|
||||
- **WHEN** приходит `lines: [{line_id, line_status: "ok"}]`
|
||||
- **THEN** 422 `LINE_CODE_REQUIRED`
|
||||
|
||||
### Requirement: Подтверждение формы — порядок 409
|
||||
`POST /documents/{id}/confirm` для формы ББ/ОФР SHALL проверять в порядке: `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` →
|
||||
`DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing` с непустым
|
||||
`amount`; `detail.lines[] {line_id, row_index, line_code_text}`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле
|
||||
уже подтверждена другая форма того же типа и периода; `detail.document_id`) → `CHECKS_NOT_PASSED` (жёсткие нарушения
|
||||
без акцепта). Между файлами повтор формы решает автозамена §9.5 п. 3.
|
||||
`DOCUMENT_NOT_CONFIRMABLE` → `PERIOD_REQUIRED` → `LINES_UNRESOLVED` (есть строки `unreadable` / `missing`, у которых хотя
|
||||
бы в одной ячейке `amount` или `string_value` не `null`; `detail.lines[] {line_id, row_index, line_code_text}`; строки без
|
||||
значений не мешают) → `DUPLICATE_FORM` (в файле уже подтверждена другая форма того же типа, `period_year` и
|
||||
`period_quarter`; `detail.document_id`) → `CHECKS_NOT_PASSED` (записи `document_issue` вида `relation` без акцепта).
|
||||
Между файлами повтор формы решает автозамена §9.5 п. 3.
|
||||
|
||||
#### 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: Словари ядра и достройка шаблонных строк
|
||||
Копия словаря §7 ядра (`api/rules/statement_checks.yaml`) SHALL удаляться: проверки ядро считает копией пакета
|
||||
`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименование по
|
||||
редакциям); порядок и состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды
|
||||
словаря имён есть в перечне. Шаблонные строки достраиваются по словарю имён; код, у которого есть хотя бы одна строка
|
||||
`ok` из файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча.
|
||||
`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименования по
|
||||
редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в
|
||||
редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и
|
||||
состав редакций — из копии перечня кодов пакета; `sync-statements --check` сверяет, что все коды словаря имён есть в
|
||||
перечне. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из
|
||||
файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча.
|
||||
|
||||
#### Scenario: Код есть только у строки unreadable
|
||||
- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250»
|
||||
- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом
|
||||
|
||||
#### Scenario: Два имени у кода
|
||||
- **WHEN** в редакции `fsbu4` словаря имён код 1520 записан дважды — «Кредиторская задолженность» и «Обязательство по
|
||||
аренде»
|
||||
- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность»
|
||||
|
||||
### Requirement: Ответ `GET …/statement` по v2.0
|
||||
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields` и сырые тексты полей; у
|
||||
колонок `period_kind`, `label_confidence`; у строк `id`, `origin`, `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`, `field`; `checks` — нарушения с `codes` и `line_ids`. Порядок `lines` — по коду «как на бумаге» (Р-14):
|
||||
внутри строк `ok` по возрастанию кода, одинаковые коды по `row_index`; строки `unreadable` / `missing` — после
|
||||
ближайшей предыдущей по `row_index` строки, размещённой по коду, без такой — в начале; `detail` — после родителя.
|
||||
`StatementOut` SHALL отдавать: у документа `form_variant`; у шапки `needs_review_fields`, `source` (`header_source`) и
|
||||
`organization_name_modified`; у колонок `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`. Порядок
|
||||
`lines` — по возрастанию кода, как сейчас (`schemas/statements.py`), одинаковые коды — по `row_index`; строки
|
||||
`unreadable` / `missing` — после ближайшей предыдущей по `row_index` строки, размещённой по коду, без такой — в начале;
|
||||
`detail` — после родителя. Разделы и размещение итогов на экране («как на бумаге», Р-14) строит экран.
|
||||
|
||||
#### Scenario: Документ, принятый по 1.x
|
||||
- **WHEN** форма загружена до перехода сервиса (результат 1.2)
|
||||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, у значений `text = string_value`,
|
||||
`issues` содержат прежние предупреждения маппинга, экран работает
|
||||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, `origin = source`, у значений `text =
|
||||
string_value`, `issues` содержат нарушения §7, посчитанные ядром, и `MAPPING_WARNING`; экран работает
|
||||
|
||||
### Requirement: Миграция DBML v5.19
|
||||
Одна миграция Alembic SHALL приводить схему к DBML v5.19 и откатываться; заполнение существующих данных:
|
||||
`row_index` строк — по возрастанию `line_code` внутри формы с 0, `line_status = ok`, `needs_review` строк — `false`,
|
||||
`needs_review` значений — по прежнему `values_ok`, `source_text = string_value`, `is_confirmed = false`,
|
||||
`period_kind` — `absolute` при `period_date`, иначе `none`, `document.file_ordinal` — по `(created_at, id)` внутри
|
||||
файла с 0; `related_group.crm_id` — `null`. Сверка `check_dbml_vs_orm.py` — против `data-model-v5.19.dbml`.
|
||||
Одна миграция Alembic SHALL приводить схему к DBML v5.19; заполнение существующих данных: `row_index` — `NULL` у строк с
|
||||
`line_code_original_value IS NULL` (созданных правкой), у остальных — по возрастанию `line_code` внутри формы с 0;
|
||||
`line_status = ok`, `needs_review` строк — `false`, `needs_review` значений — по прежнему `values_ok`, `source_text =
|
||||
string_value`, `is_confirmed = false`, `period_kind` — `absolute` при `period_date`, иначе `none`, `form_variant =
|
||||
null`, `header_source` — из `*_original_value` и `inn_confidence`, `organization_name_original_value =
|
||||
organization_name`, `document.file_ordinal` — по `(created_at, id)` внутри файла с 0, `related_group.crm_id` — `null`;
|
||||
`document_issue` каждой формы — нарушения §7, посчитанные копией `arm_statements` (требование «Документы без `checks`»).
|
||||
Откат (`downgrade`) SHALL быть гарантирован на пустой БД и на данных, совместимых с v5.18 (нет двух строк с одним кодом в
|
||||
форме, строк с `line_code = null`, двух форм одного типа в файле); на несовместимых данных `downgrade` SHALL
|
||||
останавливаться с ошибкой, называющей причину, и MUST NOT удалять данные молча. Сверка `check_dbml_vs_orm.py` — против
|
||||
`data-model-v5.19.dbml`.
|
||||
|
||||
#### Scenario: Цикл миграций
|
||||
- **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД
|
||||
- **THEN** все три шага проходят, сверка ORM с DBML v5.19 — без расхождений
|
||||
|
||||
#### Scenario: Строка правки до миграции
|
||||
- **WHEN** до миграции у формы есть строка 1250, созданная правкой (`line_code_original_value = null`), с суммой
|
||||
- **THEN** после миграции у неё `row_index = null`, `origin = template`; строки источника пронумерованы с 0
|
||||
|
||||
#### Scenario: Откат на данных v2.0
|
||||
- **WHEN** в БД есть форма с двумя строками 2410, выполняется `downgrade` ниже v5.19
|
||||
- **THEN** откат останавливается с ошибкой о повторе кода; данные не изменены
|
||||
|
||||
### Requirement: `crm_id` ГСК/ГСЗ
|
||||
`related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и
|
||||
`PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409
|
||||
|
||||
@ -19,9 +19,11 @@
|
||||
|
||||
### Requirement: Подсветка и предупреждения у места
|
||||
Экран SHALL подсвечивать ячейки только по `needs_review` (в подсказке — `text` и уверенность), строки — по
|
||||
`needs_review` строки, поля шапки — по `needs_review_fields`. Предупреждения и нарушения (`issues`, `checks`) SHALL
|
||||
показываться у места: у строк (`line_ids`), ячеек (`line_ids` + `column_id`), полей шапки (`field`) или у документа;
|
||||
уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized = false` напрямую нет.
|
||||
`needs_review` строки, поля шапки — по `needs_review_fields` (в подсказке у поля — `header.source.<поле>`: сырой текст и
|
||||
уверенность). Предупреждения и нарушения (`issues`, `checks`) SHALL показываться у места: у строк (`line_ids`), ячеек
|
||||
(`line_ids` + `column_id`), полей шапки (`field`) или у документа (в том числе запись с `column_index` без `column_id` —
|
||||
столбец вне колонок формы); уровень `error` отличается от `info`. Подсветки по `confidence < 100` и `is_recognized =
|
||||
false` напрямую нет.
|
||||
|
||||
#### Scenario: Догадка подтверждена проверкой
|
||||
- **WHEN** значение `is_recognized = false`, `needs_review = false`
|
||||
@ -31,11 +33,13 @@
|
||||
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
|
||||
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
|
||||
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате. «Сохранить» отправляет один `PATCH …/statement`
|
||||
(`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате; снятие подтверждения ячейки и кода (`confirmed:
|
||||
false`); правка наименования организации в шапке; подтверждение даты отчёта из `needs_review_fields` без изменения (тем
|
||||
же значением). «Сохранить» отправляет один `PATCH …/statement` (`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
|
||||
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
|
||||
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
|
||||
`LINE_EXISTS` — у поля кода.
|
||||
`LINE_CODE_TAKEN` — у поля кода. Кнопка «Подтвердить данные» учитывает строки `unreadable` / `missing` с суммой или
|
||||
нераспознанным текстом в ячейках.
|
||||
|
||||
#### Scenario: Назначить код
|
||||
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: `make check` — быстрые проверки без Docker
|
||||
Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`,
|
||||
`ruff format --check .`, `mypy src` (в сервисе с копией общего кода `arm_statements` — `mypy src arm_statements`, в
|
||||
`arm-core` — ещё `scripts`), `pytest -m "not integration"` (включая тесты копии `arm_statements/tests/`), тест
|
||||
идентичности вендоренного контракта с `../docs/contracts` (на окне перехода между `MAJOR` — со схемой того `MAJOR`,
|
||||
который сервис пишет). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать
|
||||
запущенного Docker, PostgreSQL или NATS. Свежесть копии `arm_statements` относительно репозитория `arm-statements` в
|
||||
`make check` MUST NOT проверяться (соседний репозиторий в `make check` не читается).
|
||||
|
||||
#### Scenario: Файл не отформатирован
|
||||
- **WHEN** в репозитории есть файл, не отформатированный `ruff format`
|
||||
- **THEN** `make check` завершается с кодом 1 на шаге `ruff format --check`, следующие шаги не выполняются
|
||||
|
||||
#### Scenario: Чистый репозиторий
|
||||
- **WHEN** все проверки проходят
|
||||
- **THEN** `make check` завершается с кодом 0 за время, не превышающее нескольких минут, без обращения к сети
|
||||
|
||||
#### Scenario: Тесты копии общего кода
|
||||
- **WHEN** в `tabular-parser` лежит копия `api/arm_statements/` с тестами пакета
|
||||
- **THEN** `make check` гоняет их вместе с тестами сервиса, `mypy` проверяет и копию
|
||||
|
||||
### Requirement: `make check-full` — полная проверка против compose
|
||||
Каждый сервисный репозиторий SHALL предоставлять цель `make check-full`: `make check` плюс `pytest -m integration` против
|
||||
инфраструктуры `docs/dev/docker-compose.yml` (PostgreSQL 16, NATS, MinIO, для `scan-gateway` — эмулятор). Для `arm-core`
|
||||
цель SHALL дополнительно выполнять «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой
|
||||
БД и `python scripts/check_dbml_vs_orm.py` против текущей версии модели (`../docs/model/data-model-v<текущая>.dbml`, путь — переменная `DBML` Makefile). В сервисе с копией общего
|
||||
кода `arm_statements` цель SHALL выполнять `statements-check` — `../docs/scripts/sync-statements.sh --check` по этому
|
||||
хосту (спека `statement-normalization`, требование «`sync-statements` — копии пакета в потребителях»): коды `0` и `10`
|
||||
— шаг зелёный (`10` — с предупреждением «копия отстала»), `20` и `2` — красный. `make check-full` SHALL быть
|
||||
выполнен перед созданием PR, его итог (и предупреждение `statements-check`, если было) вписывается в описание PR.
|
||||
|
||||
#### Scenario: Расхождение ORM и DBML
|
||||
- **WHEN** в ORM добавлена колонка, которой нет в DBML
|
||||
- **THEN** шаг `check_dbml_vs_orm` падает с перечнем расхождений, `make check-full` завершается с кодом 1
|
||||
|
||||
#### Scenario: Миграция без отката
|
||||
- **WHEN** `downgrade` ревизии не удаляет созданный ею объект
|
||||
- **THEN** шаг «upgrade → downgrade → upgrade» падает
|
||||
|
||||
#### Scenario: Прямая правка копии общего кода
|
||||
- **WHEN** в `scan-gateway` поправлен файл `api/arm_statements/steps/…`, а в `arm-statements` такого изменения нет
|
||||
- **THEN** `statements-check` завершается с кодом 20 и называет файл, `make check-full` красный
|
||||
@ -30,26 +30,32 @@ v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `f
|
||||
Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v2.0: один файл (`stored_file`)
|
||||
→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки
|
||||
`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`.
|
||||
Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы,
|
||||
лист «прочее»).
|
||||
`service.statements_version` — коммит копии общего кода `arm_statements`, которым посчитан результат (обязателен при
|
||||
наличии `statementDocument`). `metrics.warnings[]` — предупреждения уровня файла (структура `warning` с `locator`).
|
||||
|
||||
Каждый документ (и `osvDocument`, и `statementDocument`) SHALL иметь `status` (`done` | `needs_check`) и `warnings[]` —
|
||||
структуры `{code, severity, message, line_refs?, column_index?, field?}`; у `osvDocument` все предупреждения — `info`,
|
||||
`status = done`, когда ведомость разобрана. Остальное в `osvDocument` — как в v1.2 (`extra`, `period_start`,
|
||||
`period_end`, `is_from_year_start`).
|
||||
структуры `{code, severity, message, line_refs?, column_index?, field?}` (`line_refs` — только `ref` строк из `lines[]`);
|
||||
у `osvDocument` все предупреждения — `info`, `status = done`, когда контрольные суммы §1.4 листа сошлись, иначе
|
||||
`needs_check`. Остальное в `osvDocument` — как в v1.2: у строки `rows[]` необязательное поле `extra` — объект «заголовок
|
||||
колонки → строка или null» для колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»); у
|
||||
шапки — `period_start`, `period_end`, `is_from_year_start`.
|
||||
|
||||
`statementDocument` SHALL иметь `form_variant` (`full` | `simplified` | `null`); `header` — поля `organization_name`,
|
||||
`inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address` и для каждого `<поле>_text` и
|
||||
`<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index`, `label`, `label_confidence`,
|
||||
`inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `okei`, `address` и для каждого `<поле>_text` и
|
||||
`<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index` (номер столбца в сетке кандидата: у OCR
|
||||
— `column_index` ИС, у Excel — номер столбца листа с 0), `label`, `label_confidence`,
|
||||
`period_date`, `period_kind` (`absolute` | `relative:current` | `relative:prev_year_end` |
|
||||
`relative:prev_prev_year_end` | `relative:prev_period` | `none`); `lines[]` — `row_index`, `ref`, `line_status` (`ok` |
|
||||
`relative:prev_prev_year_end` | `relative:prev_period` | `none`); `lines[]` — `row_index`, `ref` (у Excel `<лист>!<A1>`,
|
||||
имя листа в кавычках по правилу Excel), `line_status` (`ok` |
|
||||
`unreadable` | `missing` | `detail`), `line_code` (строка 4–5 цифр или `null`; `null` ⇔ статус не `ok`),
|
||||
`line_code_text`, `line_code_confidence`, `line_name` (строка или `null`), `line_name_confidence`, `parent_ref` (у
|
||||
`detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`,
|
||||
`is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у
|
||||
документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected,
|
||||
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`. `ref`
|
||||
уникален в документе.
|
||||
actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` | `OTHER`; у документа
|
||||
без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе.
|
||||
|
||||
#### Scenario: Результат валидируется примерами
|
||||
- **WHEN** CI сервиса запускает тесты контракта
|
||||
@ -64,7 +70,7 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
- **WHEN** tabular-parser разобрал книгу с балансом на двух листах, ОФР со строкой без кода под 2110 и пояснениями
|
||||
(`examples/result-statement-excel.json`)
|
||||
- **THEN** файл проходит схему v2.0: `BALANCE` с `locator` из двух листов, `status`, `checks` и мягким нарушением;
|
||||
`INCOME` со строкой `detail` (`line_code = null`, `parent_ref`); `DISCLOSURE` без `columns`/`lines` и без `checks`
|
||||
`INCOME` со строкой `detail` (`line_code = null`, `parent_ref`); `DISCLOSURE` с пустыми `columns`/`lines` и без `checks`
|
||||
|
||||
#### Scenario: Форма из OCR с сомнительными значениями
|
||||
- **WHEN** scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка
|
||||
@ -87,7 +93,10 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
Сервис SHALL отвергать запрос с неизвестным `MAJOR` (`error.code = SCHEMA_MISMATCH`); ядро SHALL принимать результат
|
||||
с известным `MAJOR` и любым известным или новым `MINOR`. Запрос `recognition-request` остаётся v1.0; файл результата и
|
||||
конверт — v2.0 (`MAJOR` поднят: `line_code` стал nullable, `metrics.warnings` и `document.warnings` — структуры, удалён
|
||||
`checks.duplicate_line_codes`, изменён смысл `is_recognized` у OCR).
|
||||
`checks.duplicate_line_codes`, изменён смысл `is_recognized` у OCR). На окне перехода схемы прежнего `MAJOR` SHALL лежать
|
||||
в `docs/contracts/` рядом с текущими как `recognition-result.v1.schema.json` и `recognition-result-message.v1.schema.json`
|
||||
(байтовые копии 1.2 и 1.0), их примеры — в `docs/contracts/examples/v1/`; `scripts/validate_contracts.py` проверяет оба
|
||||
набора. Они удаляются тем же изменением, что удаляет маппер `1.x` ядра.
|
||||
|
||||
#### Scenario: Несовместимая версия запроса
|
||||
- **WHEN** сервис, поддерживающий запрос `1.x`, получает запрос `schema_version = "2.0"`
|
||||
@ -103,21 +112,46 @@ actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `
|
||||
- **THEN** результат не проходит проверку схемы, файл получает `ERROR` с причиной в `metrics.mapping_error`, повтор не
|
||||
выполняется (как для любого результата не по схеме)
|
||||
|
||||
### Requirement: Копия контракта в сервисе идентична источнику
|
||||
Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check`
|
||||
их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).
|
||||
На окне перехода между `MAJOR` тест идентичности SHALL сверять копию со схемой того `MAJOR`, который сервис пишет (или,
|
||||
у ядра, читает): копия с `$id`, оканчивающимся на `/v1`, — с `docs/contracts/<имя>.v1.schema.json`, иначе — с основным
|
||||
файлом `docs/contracts/<имя>.schema.json`; ядро держит копии обоих `MAJOR`. Правило теста вводится в `tabular-parser` и
|
||||
`scan-gateway` до слияния контракта v2.0 в docs, иначе их `make check` упадёт на окне перехода.
|
||||
|
||||
#### Scenario: Копия устарела
|
||||
- **WHEN** в `docs/contracts/` изменена схема, а копия в сервисе — нет
|
||||
- **THEN** тест идентичности в `make check` сервиса падает с указанием имени файла
|
||||
|
||||
#### Scenario: Сервис ещё пишет 1.x после слияния v2.0 в docs
|
||||
- **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, а копия в `tabular-parser` — v1.2 (`$id` `…/v1`)
|
||||
- **THEN** тест идентичности сверяет копию с `recognition-result.v1.schema.json` и проходит; посторонний PR в сервис на
|
||||
окне перехода не краснеет
|
||||
|
||||
#### Scenario: Сервис перешёл на 2.0
|
||||
- **WHEN** `scan-gateway` в PR шага 5 обновил копию до v2.0 (`$id` `…/v2`)
|
||||
- **THEN** тест сверяет её с основным файлом docs
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Переход на контракт v2.0 — «сначала ядро»
|
||||
Ядро SHALL до перехода любого сервиса принимать конверты (включая ack `processing`) и файлы результата и `1.x`, и
|
||||
`2.0`, с двумя мапперами файла результата. При приёме `1.x` статус документа формы SHALL считаться по прежнему правилу
|
||||
D-6, а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате, `ref =
|
||||
null`, `text = string_value`, `needs_review` значения — по прежнему `values_ok` (`is_recognized` не `false` и
|
||||
`confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при `period_date`, иначе `none`,
|
||||
`form_variant = null`, предупреждения-строки — `warning` `{code: "LEGACY_WARNING", severity: "info", message}`.
|
||||
Сервисы SHALL переходить на `2.0` только после выкладки ядра, принимающего `2.0`. Маппер `1.x` удаляется после
|
||||
перехода обоих сервисов отдельным изменением.
|
||||
D-6, где «проверки §7 сошлись» — по расчёту ядра копией `arm_statements` (полный набор), записанному в `document_issue`,
|
||||
а поля v2.0 заполняться по умолчанию: `line_status = ok`, `row_index` — по порядку строк в результате с 0, `ref =
|
||||
null`, `line_code_original_value = line_code`, `text = string_value`, `needs_review` значения — по прежнему `values_ok`
|
||||
(`is_recognized` не `false` и `confidence` 100 или пуста), `needs_review` строки — `false`, `period_kind` — `absolute` при
|
||||
`period_date`, иначе `none`, `form_variant = null`; строки `metrics.warnings` — в `recognition_job.metrics.warnings`
|
||||
структурами `{code: "LEGACY_WARNING", severity: "info", message}`; предупреждения маппера по документу —
|
||||
`document_issue` `MAPPING_WARNING` (`info`); `statements_version` задания — `null`. Сервисы SHALL переходить на `2.0`
|
||||
только после выкладки ядра, принимающего `2.0`. Маппер `1.x` и схемы `*.v1.schema.json` удаляются после перехода обоих
|
||||
сервисов отдельным изменением.
|
||||
|
||||
#### Scenario: Сервис ещё на v1.2
|
||||
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
|
||||
- **THEN** ядро принимает его маппером `1.x`, статус документа — по D-6, файл не `ERROR`
|
||||
- **THEN** ядро принимает его маппером `1.x`, обе строки кода — по прежнему правилу «остаётся первая» с
|
||||
`MAPPING_WARNING`, нарушения §7 — в `document_issue` от ядра, статус документа — по D-6, файл не `ERROR`
|
||||
|
||||
#### Scenario: Ack нового сервиса
|
||||
- **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"`
|
||||
|
||||
@ -18,9 +18,14 @@ file:<имя json>;table:<i>`. `value` у OCR-ячеек всегда `null`.
|
||||
### Requirement: Ошибки набора json
|
||||
Ошибочный json набора (`parse_error`, непустой `error.code`, корень не объект) при хотя бы одном успешном соседе SHALL
|
||||
давать предупреждение файла `JSON_FAILED` (`error`, `locator = file:<имя json>`, в `message` — код и сообщение ИС);
|
||||
все json ошибочны — конверт `error` с кодом первого. Исключение при разборе таблицы — предупреждение `TABLE_PARSE_ERROR`
|
||||
(`error`) документа-кандидата, остальные таблицы разбираются; ни одной таблицы не разобрано — `error
|
||||
RECOGNITION_FAILED`.
|
||||
все json ошибочны — конверт `error` с кодом первого. Исключение при разборе таблицы — предупреждение **файла**
|
||||
`TABLE_PARSE_ERROR` (`error`, `locator = file:<имя json>;table:<i>`; документа у таблицы нет), остальные таблицы
|
||||
разбираются; ни одной таблицы не разобрано — `error RECOGNITION_FAILED`.
|
||||
|
||||
#### Scenario: Таблица упала при разборе
|
||||
- **WHEN** разбор `table:1` json `aep_3.json` бросил исключение, `table:0` разобрана
|
||||
- **THEN** документ `BALANCE` из `table:0`; в `metrics.warnings` — `TABLE_PARSE_ERROR` уровня `error` с `locator =
|
||||
"file:aep_3.json;table:1"`; конверт `needs_check`; ядро не автоподтверждает документы файла
|
||||
|
||||
#### Scenario: Потерянный ОФР (решение № 28 среза 3a)
|
||||
- **WHEN** `<job_id>.json` успешен (ББ), а `<job_id>_ofr.json` вернул `INVALID_DOCUMENT`
|
||||
|
||||
@ -5,38 +5,80 @@
|
||||
своими тестами и конфигом линтеров. Пакет SHALL содержать: типы шага 1 (`FormCandidate`, `Cell`; сериализуемые в
|
||||
json dataclass-ы), шаги 2–8, общие разборы текста (двойники, даты, суммы, коды), словари — проверок §7
|
||||
`statement_checks.yaml` (полный и упрощённый наборы), периодов `statement_periods.yaml`, заголовков форм и ОКУД,
|
||||
подписей колонок («Код», «Наименование», исключения), букв-двойников, упорядоченные перечни кодов форм по редакциям
|
||||
(`66n`, `fsbu4`) и вариантам (`full`, `simplified`) без наименований, диапазоны кодов — и загрузчик словарей. Словаря
|
||||
подписей колонок («Код», «Наименование», исключения), букв-двойников, единиц измерения `statement_units.yaml` (написания
|
||||
и коды ОКЕИ), упорядоченные перечни кодов форм по редакциям (`66n`, `fsbu4`) и вариантам (`full`, `simplified`) без
|
||||
наименований — байтовая копия `docs/contracts/statement-code-orders.yaml` как `rules/statement_code_orders.yaml`,
|
||||
диапазоны кодов — и загрузчик словарей. Словаря
|
||||
«код → наименование» в пакете MUST NOT быть (Р-3). Пакет SHALL владеть своими типами входа и выхода; конвертацию
|
||||
выхода в модель контракта делает хост. Golden-тесты пакета — внутри пакета на синтетических и обезличенных данных;
|
||||
тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают.
|
||||
|
||||
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров, подключённым
|
||||
к `pytest` и `ruff` хоста как include, и импортироваться по имени `arm_statements`. Копию SHALL раскладывать и
|
||||
проверять скрипт `sync-statements` репозитория docs (по образцу `sync-web.sh`): `--check` возвращает код 20, если
|
||||
копия отличается от источника или правилась напрямую; скрипт вызывается из `make check-full` потребителя и MUST NOT
|
||||
вызываться из `make check` и пайплайна платформы. Маркер коммита источника копии SHALL попадать в результат как
|
||||
`service.statements_version`.
|
||||
В tabular-parser, scan-gateway и arm-core пакет SHALL лежать копией вместе с тестами и конфигом линтеров и
|
||||
импортироваться по имени `arm_statements`; раскладку, проверку свежести и маркер версии задаёт требование
|
||||
«`sync-statements` — копии пакета в потребителях».
|
||||
|
||||
#### Scenario: Наименований в пакете нет
|
||||
- **WHEN** ревьюер ищет в пакете соответствие «код → наименование»
|
||||
- **THEN** его нет: в перечне кодов только коды, наименования строк — словарь ядра
|
||||
|
||||
### Requirement: `sync-statements` — копии пакета в потребителях
|
||||
Копия пакета в хосте (`arm-core`, `tabular-parser`, `scan-gateway`) SHALL лежать в `api/arm_statements/` (рядом с
|
||||
`api/src/`, импорт `import arm_statements`) вместе с тестами пакета (`api/arm_statements/tests/`) и его конфигом
|
||||
линтеров; `ruff` и `pytest` хоста SHALL охватывать копию (тесты пакета — в `testpaths`), `mypy` хоста — запускаться и по
|
||||
ней (`mypy src arm_statements`, у `arm-core` — `mypy src scripts arm_statements`). Маркер версии — файл
|
||||
`api/arm_statements/VERSION` с полным хэшем коммита репозитория `arm-statements`, из которого разложена копия; его пишет
|
||||
только скрипт, сервис читает при старте и пишет в результат как `service.statements_version`. Прямые правки копии MUST
|
||||
NOT делаться.
|
||||
|
||||
Копию SHALL раскладывать и проверять скрипт `docs/scripts/sync-statements.sh` (по образцу `sync-web.sh`; цель `make -C
|
||||
docs sync-statements`). Источник — локальный checkout `arm-statements` рядом с workspace или `--source <dir>`, ссылка —
|
||||
`origin/main` или `--ref`; хост — `--host <имя>` или `--host-dir <dir>`. Без `--check` скрипт раскладывает копию коммита
|
||||
`--ref` в новый worktree хоста на ветке фичи, пишет `VERSION` и готовит issue и PR (как `sync-web.sh`). `--check`
|
||||
SHALL возвращать: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, и это коммит `--ref`; `10` — копия
|
||||
равна дереву коммита из `VERSION`, но в `--ref` есть более новый (копия отстала); `20` — копия отличается от дерева
|
||||
коммита из `VERSION`, `VERSION` нет в истории источника, копия перечня кодов в пакете отличается от
|
||||
`docs/contracts/statement-code-orders.yaml` или (только у `arm-core`) код словаря имён `api/rules/statement_lines.yaml`
|
||||
отсутствует в перечне своей редакции; `2` — нет checkout источника или хоста, неизвестный аргумент. Файлы и коды
|
||||
расхождения SHALL печататься.
|
||||
|
||||
Цель `statements-check` хоста (`sync-statements.sh --check --host-dir .`, docs — `../docs` или `ARM_KA_DOCS`) SHALL
|
||||
входить в `make check-full` (коды `0` и `10` — зелёная, `10` с предупреждением «копия отстала», вписывается в описание PR;
|
||||
`20` и `2` — красная) и MUST NOT входить в `make check` и пайплайн платформы.
|
||||
|
||||
#### Scenario: Прямая правка копии
|
||||
- **WHEN** в `tabular-parser` изменён файл копии пакета, а источник в `arm-statements` — нет
|
||||
- **THEN** `sync-statements --check` завершается с кодом 20 и называет файл; `make check` сервиса этого не проверяет
|
||||
- **THEN** `sync-statements --check` завершается с кодом 20 и называет файл; `make check-full` красный; `make check`
|
||||
сервиса этого не проверяет
|
||||
|
||||
#### Scenario: Копия отстала
|
||||
- **WHEN** копия в `scan-gateway` равна коммиту `abc1234`, а в `arm-statements` `origin/main` уже `def5678`
|
||||
- **THEN** `--check` завершается с кодом 10, `make check-full` зелёный с предупреждением «копия отстала»
|
||||
|
||||
#### Scenario: Нет checkout источника
|
||||
- **WHEN** рядом нет `arm-statements` и `--source` не задан
|
||||
- **THEN** `--check` завершается с кодом 2 и подсказкой, `make check-full` красный
|
||||
|
||||
#### Scenario: Код словаря ядра вне перечня
|
||||
- **WHEN** в `arm-core/api/rules/statement_lines.yaml` редакции `fsbu4` есть код 1120, а в перечне `fsbu4` его нет
|
||||
- **THEN** `--check` в `arm-core` завершается с кодом 20 и называет код и редакцию
|
||||
|
||||
#### Scenario: Версия общего кода в результате
|
||||
- **WHEN** сервис собирает результат копией пакета, разложенной из коммита `abc1234` репозитория `arm-statements`
|
||||
- **WHEN** сервис собирает результат копией пакета, у которой в `VERSION` коммит `abc1234` репозитория `arm-statements`
|
||||
- **THEN** в результате `service.statements_version = "abc1234"`
|
||||
|
||||
### Requirement: Шаг 1 — кандидат в форму
|
||||
Каждый адаптер шага 1 SHALL выдавать на файл список `FormCandidate`, и только с ним работают шаги 2–8. Поля
|
||||
кандидата: `source` (`ocr` | `excel`); `locator` (`file:<имя json>;table:<i>` у OCR, `sheet:<A>[,<B>…]` у Excel);
|
||||
`type_hint` (`BALANCE` | `INCOME` | `DISCLOSURE` | `null`); `evidence` (ОКУД и заголовок формы как `Cell`); `header`
|
||||
(поля `organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `address` — каждое `Cell`
|
||||
или `null`); `rows` — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
|
||||
(поля `organization_name`, `inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `okei`, `address` — каждое
|
||||
`Cell` или `null`); `rows` — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
|
||||
`null`; `segment_breaks` — номера строк сетки, с которых начинается новый лист Excel (у OCR пусто); `warnings`.
|
||||
|
||||
`Cell` SHALL быть одной для шапки и таблицы: `text` — как показывает источник; `value` — число или дата, если
|
||||
источник их знает (у OCR всегда `null`; у Excel — значение по числовому формату ячейки, tp#42); `confidence` 0–100
|
||||
(у Excel всегда 100); `ref` — позиция в источнике (OCR — `row:<row_index ИС>`, Excel — `<лист>!<A1>`).
|
||||
(у Excel всегда 100); `ref` — позиция в источнике (OCR — `row:<row_index ИС>`, Excel — `<лист>!<A1>` по правилу Excel:
|
||||
имя листа в одинарных кавычках, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры, апостроф внутри
|
||||
имени удваивается).
|
||||
|
||||
`type_hint` SHALL вычисляться одним правилом у обоих адаптеров: ОКУД по словарю (0710001 → `BALANCE`, 0710002 →
|
||||
`INCOME`, прочие 0710xxx → `DISCLOSURE`) → иначе заголовок по словарю заголовков (у OCR заголовок — `label` таблицы
|
||||
@ -47,6 +89,10 @@ json dataclass-ы), шаги 2–8, общие разборы текста (дв
|
||||
- **THEN** у `Cell` `text = "106 311"`, `value = 106311`, `confidence = 100`, `ref = "Баланс!D27"` (лист и адрес
|
||||
ячейки)
|
||||
|
||||
#### Scenario: Имя листа с пробелом
|
||||
- **WHEN** код строки стоит в ячейке C14 листа «Бухгалтерский баланс. Лист 1»
|
||||
- **THEN** `ref = "'Бухгалтерский баланс. Лист 1'!C14"`
|
||||
|
||||
#### Scenario: OCR-ячейка
|
||||
- **WHEN** в таблице ИС ячейка строки `row_index = 8` содержит `"[190 :"` с уверенностью 25
|
||||
- **THEN** у `Cell` `text = "[190 :"`, `value = null`, `confidence = 25`, `ref = "row:8"`
|
||||
@ -58,13 +104,18 @@ json dataclass-ы), шаги 2–8, общие разборы текста (дв
|
||||
### Requirement: Шаг 1 — шапка кандидата
|
||||
У каждого кандидата SHALL быть своя шапка. Текст каждого поля разбирается общим кодом по `Cell.text`: ИНН — только
|
||||
цифры, без замены двойников, длина 10 или 12, иначе `null`; дата отчёта — словарём периодов (тот же, что для подписей
|
||||
колонок, шаг 2); единицы — словарём единиц. Сырой текст и уверенность каждого поля SHALL доезжать до контракта
|
||||
(`<поле>_text`, `<поле>_confidence`).
|
||||
колонок, шаг 2); единицы — словарём единиц `statement_units.yaml` по тексту, а если по тексту не найдены — по коду ОКЕИ
|
||||
тем же словарём (384 → «тыс. руб.», 383 → «руб.», 385 → «млн руб.»); ОКЕИ — только цифры. Сырой текст и уверенность
|
||||
каждого поля SHALL доезжать до контракта (`<поле>_text`, `<поле>_confidence`).
|
||||
|
||||
#### Scenario: ИНН с мусором клеток
|
||||
- **WHEN** поле ИНН шапки OCR — `"3|1 21 2|"` с цифрами ИНН между вертикальными чертами
|
||||
- **THEN** ИНН собирается только из цифр, `|` единицей не считается; `inn_text` хранит исходный текст
|
||||
|
||||
#### Scenario: Единицы только кодом ОКЕИ
|
||||
- **WHEN** в шапке Excel-формы нет текста единиц, а в блоке «Коды» стоит ОКЕИ 384
|
||||
- **THEN** `units = "тыс. руб."`, `okei = "384"`, предупреждения `UNITS_NOT_FOUND` нет
|
||||
|
||||
### Requirement: Очистка OCR-текста — первая стадия типовых разборов
|
||||
Очистка OCR SHALL выполняться не отдельным шагом, а первой стадией разборов даты, подписи колонки, кода строки,
|
||||
разлепления склеенной строки и суммы — для любой ячейки без `value` (у Excel — только текстовые ячейки). Буквы-
|
||||
@ -157,17 +208,34 @@ json dataclass-ы), шаги 2–8, общие разборы текста (дв
|
||||
### Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
|
||||
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
|
||||
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
|
||||
двухстрочный заголовок склеивается. Строка нумерации — строка, где не меньше трёх непустых ячеек, каждая — целое число,
|
||||
и числа идут подряд, начиная с 1, слева направо — SHALL получать роль `numbering` и в данные не попадать. Роль
|
||||
двухстрочный заголовок склеивается. Строка нумерации SHALL получать роль `numbering` и в данные не попадать; строка —
|
||||
нумерация, если в ней не меньше трёх непустых ячеек, каждая после замены букв-двойников — целое число, и числа слева
|
||||
направо возрастают на 1, начинаются с 1 или 2 и допускают не больше одного пропуска. Роль
|
||||
`title` — только строки выше найденной строки заголовка; в сегменте без заголовка роли `title` нет. Заголовка нет ни у
|
||||
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
|
||||
`label = ""`, `period_end = null`, `period_kind = none`, одно предупреждение `HEADER_ROW_NOT_FOUND` (`error`).
|
||||
`label = ""`, `period_date = null`, `period_kind = none`, одно предупреждение `HEADER_ROW_NOT_FOUND` (`error`).
|
||||
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
|
||||
|
||||
#### Scenario: Нумерация в скане (AURORA-1517)
|
||||
- **WHEN** под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
|
||||
- **THEN** роль `numbering`, строки данных из неё нет
|
||||
|
||||
#### Scenario: Нумерация с двойником
|
||||
- **WHEN** строка нумерации скана распознана как «1 | 2 | З | 4»
|
||||
- **THEN** «З» читается как 3, роль `numbering`; строки `unreadable` с «суммами» 3 и 4 нет
|
||||
|
||||
#### Scenario: Нумерация без первой ячейки
|
||||
- **WHEN** ИС потеряла первую ячейку нумерации: «2 | 3 | 4 | 5»
|
||||
- **THEN** роль `numbering` (начало с 2 допускается)
|
||||
|
||||
#### Scenario: Нумерация с пропуском
|
||||
- **WHEN** строка «1 | 2 | 4 | 5» (тройка не распознана)
|
||||
- **THEN** роль `numbering` (один пропуск допускается); «1 | 3 | 5» — не нумерация
|
||||
|
||||
#### Scenario: Нумерация в Excel со «2» в столбце кодов
|
||||
- **WHEN** в форме Межениновской под шапкой строка «1 | 2 | 3 | 4 | 5», где «2» стоит в столбце кодов
|
||||
- **THEN** роль `numbering`; «2» кодом не считается
|
||||
|
||||
#### Scenario: Листы 2–5 ББ без шапки
|
||||
- **WHEN** у первого листа ББ заголовок есть, у листов 2–5 — нет
|
||||
- **THEN** листы 2–5 берут колонки первого, предупреждения о заголовке нет, документ может быть `done`
|
||||
@ -175,7 +243,7 @@ json dataclass-ы), шаги 2–8, общие разборы текста (дв
|
||||
### Requirement: Шаг 2 — подпись и дата колонки
|
||||
Подпись колонки SHALL разбираться одним словарём периодов `statement_periods.yaml`, объединяющим словарь
|
||||
tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dates.py::test_parses_real_forms` scan-gateway —
|
||||
обязательные тесты словаря. Результат — `period_end` и `period_kind`:
|
||||
обязательные тесты словаря. Результат — `period_date` (конец периода колонки) и `period_kind`:
|
||||
- абсолютная подпись («На 31 марта 2025 г.», «31.12.2024», «30/09/25», «30092025», «За 9 месяцев 2025 г.», «с
|
||||
01.01.2025 по 31.03.2025», «2024», хвост «0:00:00», клетки «2|0|2|4|») → `absolute`;
|
||||
- относительная: «На отчётную дату», «За отчётный период» → `relative:current` (= эффективная дата); «На 31 декабря
|
||||
@ -185,11 +253,11 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
- подписи нет → `none`.
|
||||
|
||||
Эффективная дата документа SHALL быть датой отчёта из шапки, иначе самой поздней абсолютной датой колонок; нет ни
|
||||
той, ни другой → у относительных колонок `period_end = null`, `period_kind` остаётся относительным.
|
||||
той, ни другой → у относительных колонок `period_date = null`, `period_kind` остаётся относительным.
|
||||
|
||||
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
|
||||
предупреждением `COLUMN_SKIPPED` (`info`). Подпись, не разобранная ни как период, ни как исключение, при числах в
|
||||
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, `period_end = null`, `period_kind = none` и
|
||||
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, `period_date = null`, `period_kind = none` и
|
||||
предупреждением `COLUMN_WITHOUT_DATE` (`error`, `column_index`); без чисел — пропуск. Подпись OCR с уверенностью ниже
|
||||
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
|
||||
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
|
||||
@ -198,18 +266,33 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
#### Scenario: Бланк 66н без впечатанных дат
|
||||
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
|
||||
предшествующего предыдущему», дата отчёта шапки 30.09.2025
|
||||
- **THEN** `period_end` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`,
|
||||
- **THEN** `period_date` колонок 2025-09-30, 2024-12-31, 2023-12-31, `period_kind` — `relative:current`,
|
||||
`relative:prev_year_end`, `relative:prev_prev_year_end`; предупреждений о дате колонок нет
|
||||
|
||||
#### Scenario: Мусорная подпись над числами
|
||||
- **WHEN** подпись колонки — `"| | г"` с уверенностью 17, а в колонке суммы
|
||||
- **THEN** колонка остаётся, `period_end = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
|
||||
- **THEN** колонка остаётся, `period_date = null`, `period_kind = none`, предупреждение `COLUMN_WITHOUT_DATE` уровня
|
||||
`error` с `column_index`, документ `needs_check`
|
||||
|
||||
#### Scenario: Колонка «Изменение»
|
||||
- **WHEN** правее периодов колонка «Изменение, %»
|
||||
- **THEN** колонка пропускается, предупреждение `COLUMN_SKIPPED` уровня `info`
|
||||
|
||||
### Requirement: Шаг 2 — номер колонки
|
||||
`column_index` колонки значений (`columns[]`, `values[]`, предупреждения) SHALL быть номером столбца в сетке кандидата
|
||||
шага 1: у OCR — `column_index` ИС как есть (не обязательно подряд), у Excel — номер столбца листа первого сегмента с 0.
|
||||
Номер уникален в документе; колонки сегментов без шапки сопоставляются с колонками первого сегмента по порядку и несут их
|
||||
номера. Предупреждение о столбце, которого нет среди `columns[]` (`COLUMN_SKIPPED`), SHALL нести номер этого столбца в
|
||||
сетке.
|
||||
|
||||
#### Scenario: Разреженные колонки ИС
|
||||
- **WHEN** в таблице ИС код стоит в колонке 1, суммы — в колонках 2, 4, 5 (колонки 3 нет)
|
||||
- **THEN** `columns[].column_index` — 2, 4, 5, у значений — те же номера
|
||||
|
||||
#### Scenario: Пропущенная колонка Excel
|
||||
- **WHEN** на листе Excel коды в столбце C, периоды в D и E, «Изменение, %» в F
|
||||
- **THEN** `columns[].column_index` — 3 и 4, предупреждение `COLUMN_SKIPPED` с `column_index = 5`
|
||||
|
||||
### Requirement: Шаг 2 — столбец наименования
|
||||
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке
|
||||
заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки —
|
||||
@ -221,10 +304,11 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
- **THEN** наименование берётся из столбца текстов статей
|
||||
|
||||
### Requirement: Шаг 3 — строки данных и их статусы
|
||||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: код, число или
|
||||
непустой текст в колонке значений с уверенностью ниже 100. У строки — `row_index` (порядковый номер в документе с 0,
|
||||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: в ячейке кода есть
|
||||
хотя бы одна цифра (разобранный код или нет), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`), или
|
||||
непуста с уверенностью ниже 100. Строка только с текстом наименования — не строка данных. У строки — `row_index` (порядковый номер в документе с 0,
|
||||
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>` по правилу шага 1, без столбца кодов — первой непустой
|
||||
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
|
||||
Статусы:
|
||||
- `ok` — код разобран;
|
||||
@ -241,6 +325,11 @@ tabular-parser и шаблоны `dates.py` scan-gateway; строки `test_dat
|
||||
- **WHEN** ячейка кода — `"ИЧ,250"`, в колонках суммы
|
||||
- **THEN** строка `unreadable` с `line_code_text = "ИЧ,250"`, суммами и `ref`; строка не пропадает
|
||||
|
||||
#### Scenario: Мусор в ячейке кода без значений
|
||||
- **WHEN** в ББ aep_6 ячейка кода — `"44 30\nI |"`, ячейки значений пусты
|
||||
- **THEN** строка `unreadable` без значений: документ `needs_check` (не все строки `ok` / `detail`), подтверждение она не
|
||||
блокирует (`LINES_UNRESOLVED` — только строки со значениями)
|
||||
|
||||
#### Scenario: Расшифровка «в том числе»
|
||||
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
|
||||
- **THEN** строка `detail`, `parent_ref` = `ref` строки 1150
|
||||
@ -304,7 +393,7 @@ k-я из непустых частей, если их тоже N, иначе ц
|
||||
- **WHEN** текст `"4,200"`
|
||||
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
|
||||
|
||||
#### Scenario: Длинное тире (OQ-148)
|
||||
#### Scenario: Длинное тире (OQ-148, закрыт решением владельца 18.10.2026)
|
||||
- **WHEN** ячейка OCR — `"—"` с уверенностью 100
|
||||
- **THEN** `amount = null`, `string_value = null`, `is_recognized = true`
|
||||
|
||||
@ -324,8 +413,8 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code`
|
||||
`null` у `unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от
|
||||
источника, у кода с заменой двойника — не выше 99; `line_name` — распознанный текст наименования как есть, с
|
||||
уверенностью источника. Сервисы MUST NOT подставлять наименование из справочника и MUST NOT срезать уверенность кода
|
||||
по справочнику (Р-3). Редакция (`66n` / `fsbu4`) в сервисах выбирается по эффективной дате (`fsbu4` при дате после
|
||||
2024-12-31) только для перечня кодов; без даты проверяются обе редакции.
|
||||
по справочнику (Р-3). Редакция (`66n` / `fsbu4`) в сервисах нужна только для перечня кодов (шаг 8): проверяются обе
|
||||
редакции, эффективная дата (`fsbu4` при дате после 2024-12-31) разрешает только равенство.
|
||||
|
||||
#### Scenario: Наименование из OCR (AURORA-1522)
|
||||
- **WHEN** у строки 1230 OCR-наименование «Дебиторская задолжен-ность»
|
||||
@ -335,11 +424,13 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code`
|
||||
Шаг 6 SHALL собирать `StatementDocument {document_type, form_variant, locator, header, columns, lines}`. Тип —
|
||||
большинство кодов строк `ok` в отрезке ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений → `DISCLOSURE`;
|
||||
совпало с `type_hint` или `type_hint = null` — тип по кодам; противоречит — тип по кодам и предупреждение
|
||||
`TYPE_CONFLICT` (`error`); кодов нет — `type_hint`. Без столбца кодов: `type_hint` есть → документ без строк
|
||||
(`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND` уровня `error`); `type_hint` нет → у `ocr`
|
||||
таблица ИС с содержимым — предупреждение файла `TABLE_LOST` (`error`), у `excel` — лист «прочее», `SHEET_OTHER`
|
||||
(`info`). `DISCLOSURE` по `type_hint` — шаги 2–7 не выполняются; `DISCLOSURE` по кодам — строки и колонки
|
||||
отбрасываются, проверки не считаются, документ `done`.
|
||||
`TYPE_CONFLICT` (`error`); кодов нет — `type_hint`. Поиск столбца кодов (шаг 2) SHALL выполняться всегда, в том числе
|
||||
при `type_hint = DISCLOSURE`: если большинство кодов в отрезке ББ или ОФР, тип — по кодам и `TYPE_CONFLICT`. Без столбца
|
||||
кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с `CODE_COLUMN_NOT_FOUND`
|
||||
уровня `error`); `type_hint` нет → у `ocr` таблица ИС с содержимым — предупреждение **файла** `TABLE_LOST` (`error`,
|
||||
`locator` таблицы), у `excel` — лист «прочее», предупреждение **файла** `SHEET_OTHER` (`info`, `locator = sheet:<имя>`);
|
||||
документа в обоих случаях нет. У `DISCLOSURE` (по `type_hint` без кодов форм ББ/ОФР или по кодам прочих форм) шаги 3–7
|
||||
не выполняются: `columns` и `lines` — пустые массивы, `checks` нет, документ `done`.
|
||||
|
||||
`form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||||
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
|
||||
@ -350,6 +441,16 @@ line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code`
|
||||
- **WHEN** в ББ есть 1150, 1210, 1600, 1310, 1700 и нет 1100 и 1200
|
||||
- **THEN** `form_variant = simplified`; шаг 7 и перечень кодов берут упрощённый набор
|
||||
|
||||
#### Scenario: «Пояснения» с кодами баланса
|
||||
- **WHEN** ИС подписала таблицу «Пояснения к бухгалтерскому балансу» (`type_hint = DISCLOSURE`), а в столбце кодов —
|
||||
1110…1700
|
||||
- **THEN** столбец кодов найден, `document_type = BALANCE`, предупреждение `TYPE_CONFLICT` уровня `error`; данные не
|
||||
выброшены
|
||||
|
||||
#### Scenario: Лист «прочее»
|
||||
- **WHEN** в книге Excel лист «Примечания» без заголовка формы, ОКУД и кодов
|
||||
- **THEN** документа нет; в `metrics.warnings` — `SHEET_OTHER` уровня `info` с `locator = "sheet:Примечания"`
|
||||
|
||||
#### Scenario: Заголовок спорит с кодами
|
||||
- **WHEN** `type_hint = BALANCE`, а коды строк — 2110, 2120, 2100, 2200
|
||||
- **THEN** `document_type = INCOME`, предупреждение `TYPE_CONFLICT` уровня `error`
|
||||
@ -367,7 +468,9 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
соотношения — в `soft_relations_failed`;
|
||||
- члены `abs_terms` (2120, 2210, 2220, 2330, 2350, 2410) сравниваются и как записаны, и по модулю; такое соотношение
|
||||
подтверждает величину, но не знак (шаг 8);
|
||||
- набор соотношений — по `form_variant` (полный или упрощённый);
|
||||
- набор соотношений — по `form_variant` (полный или упрощённый); `form_variant = null` — полный. В упрощённом наборе
|
||||
соотношения ББ жёсткие, а `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` ОФР — мягкое, как `2400` полного набора
|
||||
(знак 2410, OQ-12);
|
||||
- родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из
|
||||
первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
|
||||
|
||||
@ -387,6 +490,11 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
- **WHEN** две строки 2410 с суммами 100 и 649
|
||||
- **THEN** в соотношениях код 2410 равен 749
|
||||
|
||||
#### Scenario: Упрощённый ОФР с доходом по налогу
|
||||
- **WHEN** у упрощённого ОФР 2410 записан со знаком дохода, и `2400 = 2110 − 2120 − 2330 + 2340 − 2350 − 2410` по модулю
|
||||
не сходится
|
||||
- **THEN** нарушение в `soft_relations_failed`, документ из-за него не `needs_check`
|
||||
|
||||
#### Scenario: Округление итогов
|
||||
- **WHEN** `1600 = 1700` расходится на 9,02 при итогах ~17 млрд
|
||||
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
|
||||
@ -395,8 +503,10 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
Сервис SHALL вычислять `needs_review` у каждого значения:
|
||||
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
|
||||
нуждаются;
|
||||
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
|
||||
(в пределах допуска или округления), и `amount` значения не `null`;
|
||||
- значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
|
||||
посчитано и сошлось: жёсткое — в пределах допуска или округления; мягкое — только в пределах допуска ±1, без
|
||||
«округления» (Р-21; так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки — у них жёстких
|
||||
соотношений нет);
|
||||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус,
|
||||
без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`,
|
||||
`is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён;
|
||||
@ -418,19 +528,32 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
- **THEN** `needs_review = true` у 2120 независимо от соотношения
|
||||
|
||||
#### Scenario: Справочная строка без проверки
|
||||
- **WHEN** значение 2510 с уверенностью 80 (строк 25xx нет в жёстких соотношениях)
|
||||
- **WHEN** значение 2900 с уверенностью 80 (строк 29xx нет ни в одном соотношении)
|
||||
- **THEN** `needs_review = true`
|
||||
|
||||
#### Scenario: Чистая прибыль подтверждена мягким соотношением (Р-21)
|
||||
- **WHEN** значение 2400 с уверенностью 66, а `2400 = 2300 − 2410 + 2420` в колонке сошлось с расхождением 0
|
||||
- **THEN** у 2400 `needs_review = false`
|
||||
|
||||
#### Scenario: Мягкое соотношение в пределах округления не подтверждает
|
||||
- **WHEN** значение 2400 с уверенностью 66, а `2400 = 2300 − 2410 + 2420` расходится на 9 при итогах ~17 млрд (в пределах
|
||||
0,001 %, но больше ±1)
|
||||
- **THEN** у 2400 `needs_review = true`; нарушение в `soft_relations_failed`
|
||||
|
||||
### Requirement: Шаг 8 — подтверждение неуверенного кода структурой
|
||||
Код строки SHALL считаться неуверенным, если `line_code_confidence < 100` или при разборе заменён двойник; только к
|
||||
неуверенным кодам применяется структурная проверка (Р-12). Четырёхзначные коды строк `ok` документа образуют
|
||||
последовательность; для каждой проверяемой редакции перечня (по эффективной дате; без даты — обе) берётся наибольшая
|
||||
подпоследовательность, согласованная с порядком бланка (итоги после слагаемых); редакция — с более длинной
|
||||
подпоследовательностью, при равенстве — по эффективной дате; уверенные коды участвуют как подтверждённые якоря;
|
||||
при нескольких наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён,
|
||||
если подтверждён его родитель и подстрока стоит после него. Повтор кода, у которого хотя бы одна копия неуверенна, —
|
||||
`needs_review = true` у неуверенной копии (Р-16). Неподтверждённый неуверенный код — `StatementLine.needs_review = true`.
|
||||
У Excel и чистых OCR-кодов `needs_review` строки по структуре MUST NOT ставиться.
|
||||
неуверенным кодам применяется структурная проверка (Р-12). Данные — упорядоченные перечни кодов по редакциям (`66n`,
|
||||
`fsbu4`) и вариантам (`full`, `simplified`), источник — `docs/contracts/statement-code-orders.yaml`; законные повторы
|
||||
кода (Р-20) — часть перечня (`repeats`: у `fsbu4` 1220, 1520, 2410 печатной формы 1С; у `66n` — раздел III НКО).
|
||||
Четырёхзначные коды строк `ok` документа образуют последовательность; для **обеих** редакций берётся наибольшая
|
||||
подпоследовательность, согласованная с порядком перечня варианта формы (итоги после слагаемых; код с повтором может
|
||||
войти столько раз, сколько он стоит в перечне); редакция — с более длинной подпоследовательностью, при равенстве — по
|
||||
эффективной дате (`fsbu4` после 2024-12-31); уверенные коды участвуют как подтверждённые якоря; при нескольких
|
||||
наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён, если
|
||||
подтверждён его родитель и подстрока стоит после него. Повтор кода, которого нет в перечне редакции (или копий больше,
|
||||
чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review = true` у неуверенной копии (Р-16); повтор из
|
||||
перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `StatementLine.needs_review =
|
||||
true`. У Excel и чистых OCR-кодов `needs_review` строки по структуре MUST NOT ставиться.
|
||||
|
||||
#### Scenario: Неуверенный код на своём месте
|
||||
- **WHEN** код 1230 с уверенностью 50 стоит между 1220 и 1240 (уверенными)
|
||||
@ -440,6 +563,19 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
|
||||
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check`
|
||||
|
||||
#### Scenario: Законный повтор 1220 (Р-20, AURORA-1431)
|
||||
- **WHEN** в ББ печатной формы 1С по ФСБУ 4/2023 стоят подряд 1215, 1220 «НДС по приобретенным ценностям» (уверенность
|
||||
100), 1220 «НДС, начисленный по отгрузке» (уверенность 50), 1230
|
||||
- **THEN** обе строки 1220 подтверждены структурой (повтор есть в перечне `fsbu4`), `needs_review = false`
|
||||
|
||||
#### Scenario: Повтор вне перечня
|
||||
- **WHEN** в ББ код 1230 стоит дважды подряд, у второй копии уверенность 50
|
||||
- **THEN** у второй копии `needs_review = true`, документ `needs_check`
|
||||
|
||||
#### Scenario: Бланк 66н в 2025 году
|
||||
- **WHEN** скан ББ за 3 кв. 2025 на бланке 66н с неуверенным кодом 1120 между 1110 и 1130
|
||||
- **THEN** проверяются обе редакции, подпоследовательность `66n` длиннее, код 1120 подтверждён
|
||||
|
||||
#### Scenario: Нестандартный код Excel
|
||||
- **WHEN** в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
|
||||
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
|
||||
@ -447,8 +583,11 @@ actual}`, где `codes` — коды цели и членов соотноше
|
||||
### Requirement: Шаг 8 — поля шапки
|
||||
Дата отчёта с уверенностью ниже 100 SHALL подтверждаться совпадением с самой поздней абсолютной датой колонок; иначе
|
||||
поле `report_date` попадает в `needs_review_fields` и выдаётся `REPORT_DATE_UNCERTAIN` (`error`, `field =
|
||||
report_date`). ИНН и организация с уверенностью ниже 100 — предупреждения `INN_UNCERTAIN` / `ORGANIZATION_UNCERTAIN`
|
||||
(`info`, с `field`), в `needs_review_fields` не попадают; единицы и прочие поля на статус не влияют.
|
||||
report_date`). У бланка только с относительными подписями колонок абсолютных дат нет, и неуверенная дата отчёта
|
||||
структурой не подтверждается никогда: документ `needs_check`, аналитик подтверждает или исправляет дату в ядре (PATCH
|
||||
шапки). `needs_review_fields` — поля, которые держат документ в `needs_check`; ИНН и организация с уверенностью ниже 100
|
||||
— предупреждения `INN_UNCERTAIN` / `ORGANIZATION_UNCERTAIN` (`info`, с `field`), в `needs_review_fields` не попадают
|
||||
(подсветка у поля — по предупреждению); единицы и прочие поля на статус не влияют.
|
||||
|
||||
#### Scenario: Неуверенная, но верная дата
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025
|
||||
@ -458,16 +597,25 @@ report_date`). ИНН и организация с уверенностью ни
|
||||
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
|
||||
|
||||
#### Scenario: Бланк 66н с относительными подписями
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, подписи колонок — «На отчетную дату», «На 31 декабря предыдущего
|
||||
года»
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`; снимается подтверждением даты
|
||||
аналитиком
|
||||
|
||||
### Requirement: Шаг 8 — предупреждения
|
||||
Предупреждение SHALL быть структурой `{code, severity, message, line_refs?, column_index?, field?}`: `severity` —
|
||||
`error` | `info`; `line_refs` — список `ref` строк; `column_index` — колонка; `field` — поле шапки. Место в файле —
|
||||
`locator` документа, а не текст. Предупреждения уровня файла (`metrics.warnings`) дополнительно имеют `locator?`
|
||||
(json набора ИС или лист). Уровни SHALL быть такими:
|
||||
`error` | `info`; `line_refs` — список `ref` строк только из `lines[]` (о строках ролей `title` / `header` /
|
||||
`numbering` — в `message`); `column_index` — номер столбца сетки (требование «Шаг 2 — номер колонки»); `field` — поле
|
||||
шапки. Место в файле — `locator` документа, а не текст. Предупреждения уровня файла (`metrics.warnings`) дополнительно
|
||||
имеют `locator?` (json набора ИС, таблица ИС или лист) и SHALL выдаваться там, где документа нет; предупреждение файла
|
||||
уровня `error` делает конверт `needs_check` и блокирует `RECOGNIZED` всех документов файла в ядре. Уровни SHALL быть
|
||||
такими:
|
||||
|
||||
| Код | Уровень | Когда |
|
||||
|---|---|---|
|
||||
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
|
||||
| `TABLE_PARSE_ERROR` | error | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`) |
|
||||
| `TABLE_PARSE_ERROR` | error (файл) | исключение при разборе таблицы ИС (`BACKEND_PARSING_ERROR`); документа нет |
|
||||
| `TABLE_LOST` | error (файл) | таблица ИС с содержимым без колонки кодов и без `type_hint` |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | у формы с `type_hint` `BALANCE`/`INCOME` нет столбца кодов |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
|
||||
@ -491,12 +639,17 @@ report_date`). ИНН и организация с уверенностью ни
|
||||
| `FRACTIONAL_IN_THOUSANDS` | info | дробные при «тыс. руб.» (кроме xlsb без форматов) |
|
||||
| `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED` | info | числовые форматы недоступны или не разобраны |
|
||||
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр |
|
||||
| `SHEET_OTHER` | info | лист Excel «прочее» без `type_hint` и кодов |
|
||||
| `SHEET_OTHER` | info (файл) | лист Excel «прочее» без `type_hint` и кодов; документа нет |
|
||||
| `ROW_SPLIT` | info | разлеплена склеенная строка |
|
||||
|
||||
Предупреждения пути ОСВ — уровня `info`, код с префиксом `OSV_`. Новый код предупреждения вводится словарём пакета и
|
||||
таблицей §11 `05-domain-rules.md`; `code` в контракте — строка `^[A-Z][A-Z0-9_]*$`, не перечисление.
|
||||
|
||||
#### Scenario: Исключение при разборе таблицы ИС
|
||||
- **WHEN** разбор второй таблицы json упал с исключением, первая разобрана
|
||||
- **THEN** документ первой таблицы со своим статусом; в `metrics.warnings` — `TABLE_PARSE_ERROR` уровня `error` с
|
||||
`locator = "file:<имя json>;table:1"`; конверт `needs_check`
|
||||
|
||||
#### Scenario: НПЗ ОФР без ИНН
|
||||
- **WHEN** в шапке ОФР есть организация и нет ИНН
|
||||
- **THEN** `INN_NOT_FOUND` уровня `info`, документ может быть `done`
|
||||
@ -511,8 +664,13 @@ report_date`). ИНН и организация с уверенностью ни
|
||||
период определён (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет
|
||||
значений, строк и полей шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет
|
||||
предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и
|
||||
проверках. Функция SHALL принимать на вход и подтверждения и акцепт аналитика (у ядра после правок): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения. Статус конверта: `done` ⇔
|
||||
проверках. Вход функции SHALL быть одинаковым у сервиса и ядра и не зависеть от источника: `document_type`,
|
||||
`layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть строка из источника,
|
||||
`row_index` не `null`), `has_organization_or_inn`, `period_known`, строки `{line_status, needs_review}`, значения
|
||||
`{needs_review}`, число жёстких нарушений вне допуска и округления, `accepted`, уровни предупреждений документа (у ядра —
|
||||
записи `document_issue` вида `warning`). `needs_review` на входе уже учитывает правки и подтверждения аналитика
|
||||
(правленое или подтверждённое значение и строка — `false`); `accepted` — жёсткие нарушения не учитываются. Статус ОСВ —
|
||||
`done`, когда контрольные суммы §1.4 листа сошлись. Статус конверта: `done` ⇔
|
||||
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
|
||||
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
|
||||
со справочником в `done` MUST NOT входить.
|
||||
@ -525,6 +683,11 @@ report_date`). ИНН и организация с уверенностью ни
|
||||
- **WHEN** в форме есть строка `unreadable` с суммой
|
||||
- **THEN** документ `needs_check`
|
||||
|
||||
#### Scenario: Ядро считает статус той же функцией
|
||||
- **WHEN** ядро после PATCH вызывает функцию статуса по строкам БД формы, у которой все строки `ok`, правленая ячейка
|
||||
была `needs_review`, нарушений и предупреждений `error` нет
|
||||
- **THEN** функция даёт `done` — тот же результат, что дал бы сервис по тем же данным с подтверждённой ячейкой
|
||||
|
||||
#### Scenario: Соседний документ не тянет файл
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
|
||||
- **THEN** конверт `needs_check`, у ББ `document.status = done`
|
||||
|
||||
@ -7,10 +7,13 @@ tabular-parser SHALL оставить у себя чтение книги (ли
|
||||
выполняться копией пакета `arm_statements`. Адаптер SHALL: выделять блоки форм на листе по заголовку или ОКУД (без
|
||||
кодов между ними блоки сливаются, заголовок после кодов — новая форма); собирать шапку каждого блока в его границах
|
||||
(организация, ИНН без ИНН аудитора, ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес) как `Cell` с `confidence = 100`;
|
||||
заполнять у каждой ячейки `text` по числовому формату (tp#42) и `value`, `ref = <лист>!<A1>`; дописывать
|
||||
заполнять у каждой ячейки `text` по числовому формату (tp#42) и `value`, `ref = <лист>!<A1>` (имя листа в одинарных
|
||||
кавычках по правилу Excel, если в нём есть что-то кроме букв, цифр и `_`); дописывать
|
||||
листы-продолжения в сетку открытой формы до раскладки (маркер «Форма … с.2» или позиция листа; проверки типа,
|
||||
диапазона кодов и, при трёх и более кодах, числа колонок — как `join_blocker`), записывая границы листов в
|
||||
`segment_breaks`. Несовпадение формата с `file_type` — предупреждение файла `FILE_TYPE_MISMATCH` (`info`).
|
||||
`segment_breaks`. Несовпадение формата с `file_type` — предупреждение файла `FILE_TYPE_MISMATCH` (`info`). Лист без
|
||||
признаков ОСВ и формы, без `type_hint` и кодов — предупреждение файла `SHEET_OTHER` (`info`, `locator = sheet:<имя>`),
|
||||
документа нет.
|
||||
|
||||
#### Scenario: Книга ББ + ОФР
|
||||
- **WHEN** книга содержит лист «Баланс» (ОКУД 0710001), лист «Баланс (2)» с маркером «Форма 0710001 с.2» и лист «ОФР»
|
||||
@ -19,7 +22,7 @@ tabular-parser SHALL оставить у себя чтение книги (ли
|
||||
|
||||
#### Scenario: Ссылки ячеек
|
||||
- **WHEN** код строки стоит в ячейке C27 листа «Баланс»
|
||||
- **THEN** `ref` строки — `Баланс!C27`
|
||||
- **THEN** `ref` строки — `Баланс!C27`; у листа «Бухгалтерский баланс. Лист 1» — `'Бухгалтерский баланс. Лист 1'!C27`
|
||||
|
||||
### Requirement: Строки без кода и неизвестные колонки в Excel
|
||||
tabular-parser SHALL отдавать строки без кода с содержимым со статусами `detail` / `missing` / `unreadable` (Р-8; ранее
|
||||
@ -30,9 +33,23 @@ tabular-parser SHALL отдавать строки без кода с содер
|
||||
- **WHEN** в ОФР ФСБУ 4/2023 строка «Реализация иного имущества…» без кода с суммами стоит под строкой 2340
|
||||
- **THEN** в результате строка `detail` с `parent_ref` строки 2340; ранее она отбрасывалась со счётчиком
|
||||
|
||||
### Requirement: Манифест ожиданий на реальных формах
|
||||
Манифест `tabular-parser/api/tests/statements_manifest.yaml` SHALL быть обновлён в PR перехода на 2.0 по правилу
|
||||
«посчитанного» соотношения §7 (пустая цель при непустых членах — нарушение): `failed_relations` страниц 2 баланса без
|
||||
1600 получают `["1600 = 1700"]` — `unagrande-co/2024-FY/Б.Б. Лист 2.xls`, `unagrande-group/2024-FY/ББ. Лист 2 (2).xls`,
|
||||
`unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` (было `[]`, причина «страница 2 без шапки» остаётся); страницы 1 «только
|
||||
актив» (`Б.Б Лист 1.xls`, `ББ. Лист 1 .xls`, `ББ. Лист 1 3кв.25.xls`) сохраняют `["1600 = 1700"]` (теперь как «непустая
|
||||
цель при пустых членах»). Статусы `done` (Межениновская ББ и ОФР, НПЗ ББ и ОФР, ОФР «Лист 1» Унагранде ×2) MUST NOT
|
||||
меняться: их строки без кода уходят в `detail`, строки шапки выше заголовка — в `title`. Остальные записи манифеста
|
||||
пересчитываются прогоном и правятся только с объяснением в описании PR.
|
||||
|
||||
#### Scenario: Страница 2 баланса
|
||||
- **WHEN** разбирается `unagrande-group/2025-Q3/ББ. Лист 2 3кв.25.xls` (есть 1700, нет 1600)
|
||||
- **THEN** документ `needs_check`, в `checks.relations_failed` — `1600 = 1700` с пустой целью
|
||||
|
||||
### Requirement: Путь ОСВ в контракте v2.0
|
||||
У `osvDocument` tabular-parser SHALL выдавать `status` (`done`, когда ведомость разобрана, иначе `needs_check` — по
|
||||
прежним условиям §1.6 листа) и `warnings[]` — прежние предупреждения листа структурами с `severity = info` и кодом с
|
||||
У `osvDocument` tabular-parser SHALL выдавать `status` (`done`, когда контрольные суммы §1.4 листа сошлись, иначе
|
||||
`needs_check` — прежние условия §1.6, применённые к листу) и `warnings[]` — прежние предупреждения листа структурами с `severity = info` и кодом с
|
||||
префиксом `OSV_`; строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Остальной разбор ОСВ не меняется.
|
||||
|
||||
#### Scenario: Предупреждение ОСВ
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12.
|
||||
"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12 и упорядоченных перечней кодов форм.
|
||||
|
||||
Запуск из любого cwd: python scripts/validate_contracts.py
|
||||
Зависимости: стандартная библиотека + jsonschema.
|
||||
Зависимости: стандартная библиотека + jsonschema + PyYAML (`make venv`).
|
||||
Код возврата: 0 — всё валидно, 1 — любая ошибка.
|
||||
|
||||
Окно перехода на MAJOR 2 (change unified-statement-normalization): схемы прежнего MAJOR лежат рядом как
|
||||
`*.v1.schema.json`, их примеры — в `examples/v1/`; удаляются после перехода обоих сервисов (маппер 1.x ядра — тоже).
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
@ -21,8 +24,16 @@ SCHEMAS = {
|
||||
"request": CONTRACTS / "recognition-request.schema.json",
|
||||
"message": CONTRACTS / "recognition-result-message.schema.json",
|
||||
"result": CONTRACTS / "recognition-result.schema.json",
|
||||
"message.v1": CONTRACTS / "recognition-result-message.v1.schema.json",
|
||||
"result.v1": CONTRACTS / "recognition-result.v1.schema.json",
|
||||
}
|
||||
|
||||
# Каталог примеров → суффикс вида схемы: examples/ — текущий MAJOR, examples/v1/ — прежний (окно перехода).
|
||||
EXAMPLE_DIRS = [(EXAMPLES, ""), (EXAMPLES / "v1", ".v1")]
|
||||
|
||||
CODE_ORDERS = CONTRACTS / "statement-code-orders.yaml"
|
||||
CODE_RE = re.compile(r"^\d{4,5}$")
|
||||
|
||||
# Порядок важен: result-message-* проверяется раньше result-*.
|
||||
PATTERNS = [
|
||||
("request-", "request"),
|
||||
@ -75,9 +86,11 @@ def main():
|
||||
print(f"OK схема {path.name}")
|
||||
|
||||
counts = {kind: 0 for kind in SCHEMAS}
|
||||
for example in sorted(EXAMPLES.glob("*.json")):
|
||||
kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None)
|
||||
if kind is None:
|
||||
examples = [(example, suffix) for folder, suffix in EXAMPLE_DIRS for example in sorted(folder.glob("*.json"))]
|
||||
for example, suffix in examples:
|
||||
base = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None)
|
||||
kind = None if base is None else (base if base == "request" else base + suffix)
|
||||
if kind is None or kind not in SCHEMAS:
|
||||
print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)")
|
||||
failed = True
|
||||
continue
|
||||
@ -107,10 +120,67 @@ def main():
|
||||
print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}")
|
||||
failed = True
|
||||
|
||||
failed = check_code_orders() or failed
|
||||
total = sum(counts.values())
|
||||
print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def code_order_errors(data):
|
||||
"""Структура перечня: editions[id] → forms[BALANCE|INCOME] → variants[full|simplified] → список кодов."""
|
||||
errors = []
|
||||
if not isinstance(data, dict) or not isinstance(data.get("editions"), dict):
|
||||
return ["нет словаря editions"]
|
||||
for edition in ("66n", "fsbu4"):
|
||||
forms = data["editions"].get(edition)
|
||||
if not isinstance(forms, dict):
|
||||
errors.append(f"нет редакции {edition}")
|
||||
continue
|
||||
for form, first in (("BALANCE", "1"), ("INCOME", "2")):
|
||||
variants = forms.get(form)
|
||||
if not isinstance(variants, dict):
|
||||
errors.append(f"{edition}: нет формы {form}")
|
||||
continue
|
||||
for variant in ("full", "simplified"):
|
||||
codes = variants.get(variant)
|
||||
where = f"{edition}/{form}/{variant}"
|
||||
if not isinstance(codes, list) or not codes:
|
||||
errors.append(f"{where}: пустой или не список")
|
||||
continue
|
||||
for code in codes:
|
||||
if not isinstance(code, str) or not CODE_RE.fullmatch(code) or not code.startswith(first):
|
||||
errors.append(f"{where}: код {code!r} — не строка из 4 цифр формы")
|
||||
repeats = {c for i, c in enumerate(codes) if c in codes[:i]}
|
||||
allowed = set(data.get("repeats", {}).get(edition, {}).get(form, []))
|
||||
if repeats - allowed:
|
||||
errors.append(f"{where}: повтор кода вне перечня repeats: {sorted(repeats - allowed)}")
|
||||
return errors
|
||||
|
||||
|
||||
def check_code_orders():
|
||||
"""Упорядоченные перечни кодов форм (05-domain-rules.md §11.9): yaml разбирается и устроен как описано."""
|
||||
if not CODE_ORDERS.is_file():
|
||||
print(f"FAIL {CODE_ORDERS.name}: файл не найден")
|
||||
return True
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print(f"FAIL {CODE_ORDERS.name}: нужен PyYAML (make venv)")
|
||||
return True
|
||||
try:
|
||||
data = yaml.safe_load(CODE_ORDERS.read_text(encoding="utf-8"))
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"FAIL {CODE_ORDERS.name}: не YAML: {str(exc).splitlines()[0]}")
|
||||
return True
|
||||
errors = code_order_errors(data)
|
||||
if errors:
|
||||
print(f"FAIL {CODE_ORDERS.name}")
|
||||
for error in errors:
|
||||
print(f" {error}")
|
||||
return True
|
||||
print(f"OK {CODE_ORDERS.name}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user