Compare commits
5 Commits
43b4b808ae
...
3e0b69038e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e0b69038e | ||
|
|
ecc789667f | ||
|
|
56a0ff4527 | ||
|
|
9b1a4ccbfd | ||
|
|
90ea429642 |
2
Makefile
@ -5,7 +5,7 @@ VENV := .venv
|
||||
PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
|
||||
CHANGE ?= slice-0-skeleton
|
||||
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
|
||||
DBML ?= model/data-model-v5.18.dbml
|
||||
DBML ?= model/data-model-v5.19.dbml
|
||||
|
||||
.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ git remote add origin <url-arm-core> && git push -u origin main
|
||||
|
||||
| Слой | Где | Статус | Правило для агента |
|
||||
|---|---|---|---|
|
||||
| Модель данных | `docs/model/data-model-v5.18.dbml` | стабильный, версионируется | единственный источник имён и структур |
|
||||
| Модель данных | `docs/model/data-model-v5.19.dbml` | стабильный, версионируется | единственный источник имён и структур |
|
||||
| Правила домена | `docs/context/05-domain-rules.md` | курируемый — пишет человек/агент по итогам ревью | **единственный вход алгоритмов в код**; из ТЗ напрямую не кодируем |
|
||||
| Открытые вопросы | `docs/context/07-open-questions.md` | живой реестр | каждое допущение в коде — `ASSUMPTION(OQ-nn)` |
|
||||
| Экспорт ТЗ | `docs/source/tz/` | снимок на дату из `docs/source/tz/_export.md` | читать для контекста; расхождение с §05 → вопрос, не код |
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.18)
|
||||
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.19)
|
||||
|
||||
## Компоненты (= репозитории, см. `00-workspace-layout.md`)
|
||||
|
||||
@ -31,8 +31,9 @@
|
||||
лимиты, чтение, дедлайн, вид каждого листа): листы ОСВ идут в конвейер `osv.py`, листы форм — в конвейер форм
|
||||
`statements/` (блоки форм, колонки, шапка, строки, соотношения §7, склейка продолжений); документы результата — сначала
|
||||
ОСВ, затем формы. Контракт результата —
|
||||
`docs/contracts/recognition-result.schema.json` (контракт v1.2: `DISCLOSURE` в `document_type`, `statementDocument.checks`,
|
||||
границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания
|
||||
`docs/contracts/recognition-result.schema.json` (контракт v2.0 — статусы строк, `needs_review`, `document.status` и
|
||||
структурные предупреждения, change `unified-statement-normalization`; до перехода сервис пишет v1.2: `DISCLOSURE` в
|
||||
`document_type`, `statementDocument.checks`, границы периода ОСВ `period_start/period_end/is_from_year_start`). Правила распознавания
|
||||
заголовков/группировок — `rules/*.yaml`.
|
||||
- **scan-gateway** — адаптер ИС Сканирование: положить файл в горячую папку `Input/<job_id>.<ext>` (расширение —
|
||||
суффикс `original_name` в нижнем регистре; файл пишется сразу в `Input` с перезаписью существующего, без временного
|
||||
@ -163,6 +164,18 @@ advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскак
|
||||
- **Нагрузка и транзакции.** Переход версии после загрузки выполняется в отдельной короткой транзакции
|
||||
(`session_transaction`), сбой не отменяет принятые файлы (R-10). Чтение (`GET …/osv`, сводка, экспорт) блокировок не берёт.
|
||||
|
||||
### Общий код нормализации ББ/ОФР — `arm-statements` (change `unified-statement-normalization`, целевое)
|
||||
|
||||
Шаги 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`). Сервисы держат только адаптер
|
||||
шага 1 (json ИС / лист Excel → `FormCandidate`); ядро считает тем же кодом после правок аналитика и не держит своих
|
||||
копий словаря §7. Словарь «код → наименование» — только в ядре. Правила — `05-domain-rules.md` §11; маркер версии
|
||||
копии — `service.statements_version` результата → `recognition_job.statements_version`. Порядок выкладки — «сначала
|
||||
ядро» (ядро принимает 1.x и 2.0). Пункты ниже описывают поведение до перехода (Срез 3b).
|
||||
|
||||
### Формы ББ/ОФР, проверки §7 и API экрана проверки (Срез 3b)
|
||||
|
||||
Формы (`statementDocument` контракта v1.2) приходят от обоих сервисов распознавания в одном формате; ядро — единственное
|
||||
@ -236,7 +249,7 @@ api/src/
|
||||
# машины статусов, policy — pydantic-модели на входе и выходе
|
||||
services/ # use-cases: оркестрация domain + repositories + outbox + audit_event
|
||||
repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка)
|
||||
models/ # ORM 1:1 с DBML v5.18; имена таблиц/колонок/enum — как в DBML
|
||||
models/ # ORM 1:1 с DBML v5.19; имена таблиц/колонок/enum — как в DBML
|
||||
schemas/ # pydantic DTO API /api/v1
|
||||
core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент),
|
||||
# logger, exception_handlers
|
||||
@ -326,7 +339,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
|
||||
`version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`.
|
||||
- Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное
|
||||
`<rev>_<slug>.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`.
|
||||
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.18.
|
||||
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.19.
|
||||
|
||||
### Инфраструктурные клиенты
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# 04. Как читать модель данных v5.18
|
||||
# 04. Как читать модель данных v5.19
|
||||
|
||||
Файлы: `docs/model/data-model-v5.18.dbml` (источник истины), `docs/model/entities-v5.18.md` (смысл таблиц; предыдущие
|
||||
версии `data-model-v5.13…v5.17.dbml` и `entities-v5.13…v5.17.md` остаются как история),
|
||||
`docs/model/diagrams/part01…09.{svg,png}` (диаграммы v5.18 по частям из таблицы ниже; перерисовываются из DBML командой
|
||||
Файлы: `docs/model/data-model-v5.19.dbml` (источник истины), `docs/model/entities-v5.19.md` (смысл таблиц; предыдущие
|
||||
версии `data-model-v5.13…v5.18.dbml` и `entities-v5.13…v5.18.md` остаются как история),
|
||||
`docs/model/diagrams/part01…09.{svg,png}` (диаграммы v5.19 по частям из таблицы ниже; перерисовываются из DBML командой
|
||||
`make diagrams` в корне репозитория docs — скрипт `scripts/gen_model_diagrams.py`, нужен npx; при новой версии DBML
|
||||
обновить `DBML` в Makefile, а новую таблицу — добавить в раскладку частей скрипта и в эту таблицу).
|
||||
|
||||
@ -15,11 +15,17 @@
|
||||
| 03 Бухгалтерские справочники | `chart_of_account`, `elimination_rule`, `elimination_rule_account`, `required_document_rule` | план счетов (seed из 1С), правила элиминации Прил. 3/4 (версионируются), чек-лист по подходу |
|
||||
| 04 Файлы и распознавание | `s3_object`, `stored_file`, `document`, `recognition_job` | физический файл → задание → логические документы |
|
||||
| 05 Разобранные ОСВ | `osv_entry` | строка ОСВ с шестью суммами, сырым контрагентом и резолвом |
|
||||
| 06 Формы ББ/ОФР | `financial_statement_meta`, `_column`, `_line`, `_value` | шапка, колонки-даты, строки с кодами, значения с confidence |
|
||||
| 06 Формы ББ/ОФР | `financial_statement_meta`, `_column`, `_line`, `_value`, `document_issue` | шапка, колонки-даты, строки со статусами, значения с confidence и `needs_review`, предупреждения и нарушения §7 |
|
||||
| 07 Консолидация | `consolidation`, `consolidation_version`, `_status_history`, `_lock`, `_member`, `_document` | логическая консолидация (ГК × период) и её версии с периметром |
|
||||
| 08 Корректировки и КФО | `correction`, `goodwill`, `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `client_kfo_line` | ручные решения и результат |
|
||||
| 09 Комментарии, аудит, ЕХ | `comment`, `audit_event`, `ex_snapshot`, `ex_snapshot_member`, `ex_snapshot_item` | полиморфные комментарии, append-only журнал, замороженные срезы |
|
||||
|
||||
**v5.19:** формы ББ/ОФР по контракту v2.0 (change `unified-statement-normalization`, `05-domain-rules.md` §11): строки
|
||||
формы хранятся все — с кодом (повтор кода законен, уникальность `(meta_id, line_code)` снята) и без (`line_status`
|
||||
`unreadable` / `missing` / `detail`); `needs_review` значения и строки **хранится** (пересматривает v5.13); предупреждения и
|
||||
нарушения §7 — `document_issue`; две формы одного типа в файле — разные `document` (`file_ordinal`); чтения «по коду» —
|
||||
агрегат формы по коду (сумма строк `ok`, не хранится). Плюс `related_group.crm_id`.
|
||||
|
||||
**v5.18:** `stored_file.consolidation_id` — в какую консолидацию (период ГК) загружен файл; файлы, загруженные в консолидацию, — основа рабочего стола консоли, общего для её версий (полный состав стола, с файлами документов, привязанных к версиям, — `05-domain-rules.md` §9.5 п. 11). Что использует версия — `consolidation_document`.
|
||||
|
||||
## Соглашения
|
||||
@ -60,9 +66,9 @@
|
||||
## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ)
|
||||
|
||||
v5.14 имена таблиц и полей не менял (добавлены `related_group_member.kind` и два значения `audit_action`), v5.15 — тоже
|
||||
(добавлено значение `osv_data_type.NU`), v5.16 — тоже (колонка `osv_entry.counterparty_match_kind`), v5.17 — тоже (колонка `legal_entity.kpp`), v5.18 — тоже (колонка `stored_file.consolidation_id`), поэтому таблица справедлива и для v5.14…v5.18.
|
||||
(добавлено значение `osv_data_type.NU`), v5.16 — тоже (колонка `osv_entry.counterparty_match_kind`), v5.17 — тоже (колонка `legal_entity.kpp`), v5.18 — тоже (колонка `stored_file.consolidation_id`), v5.19 — тоже (колонки форм, `document_issue`, `related_group.crm_id`), поэтому таблица справедлива и для v5.14…v5.19.
|
||||
|
||||
| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 / v5.18 |
|
||||
| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 / v5.18 / v5.19 |
|
||||
|---|---|
|
||||
| `stored_file.period_year / period_quarter / document_type / osv_account` | `document.period_year / period_quarter / document_type / osv_account_code / is_cumulative` |
|
||||
| `stored_file.status` «Загружен / Обработка / Требуется проверка / Ошибка» | техника: `stored_file.status` UPLOADED/PROCESSING/PROCESSED/ERROR/SUPERSEDED; проверка: `document.status` RECOGNIZED/NEEDS_CHECK/CONFIRMED/ERROR |
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# 05. Правила предметной области (выжимка из ТЗ, имена — v5.18)
|
||||
# 05. Правила предметной области (выжимка из ТЗ, имена — v5.19)
|
||||
|
||||
Каждый раздел: источник в ТЗ → правило → где в модели. Где ТЗ молчит или противоречит — пометка **[открыто]** и
|
||||
ссылка на `07-open-questions.md`.
|
||||
@ -293,10 +293,15 @@ result-osv-needs-check.json`.
|
||||
|
||||
Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по
|
||||
`file_type` запроса; несовпадение — предупреждение. Листы без признаков ОСВ и форм пропускаются с
|
||||
предупреждением (`sheet=<имя> skipped: нет признаков ОСВ или формы`; листы форм теперь разбираются, §12). Другие предупреждения:
|
||||
предупреждением (`sheet=<имя> skipped: нет признаков ОСВ или формы`; листы форм теперь разбираются, §11.2). Другие предупреждения:
|
||||
`sheet=<имя> hierarchy=<стратегия>` (всегда), свёрнутое сальдо, нераспознанное число (с номером строки), не найденное
|
||||
поле шапки, «нет строк данных». xlsb не содержит outline level — каскад начинается с отступа (предупреждение
|
||||
«xlsb: outline level недоступен»).
|
||||
**С контракта v2.0** (change `unified-statement-normalization`, §11.9) у каждого `osvDocument` есть `status` (`done`, когда
|
||||
контрольные суммы §1.4 листа сошлись, иначе `needs_check` — те же условия, что выше, но по листу) и `warnings[]` —
|
||||
перечисленные предупреждения листа структурами `{code: "OSV_…", severity: "info", message}`; строки `sheet=<имя> …` в
|
||||
`metrics.warnings` больше не пишутся (предупреждения уровня книги — `metrics.warnings` структурами, например
|
||||
`FILE_TYPE_MISMATCH`). Разбор ОСВ не меняется.
|
||||
|
||||
Лимиты: `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с; кооперативный — проверка между листами,
|
||||
в циклах по листу и между стратегиями; чтение книги библиотекой он не прерывает). Поэтому до библиотеки чтения действуют
|
||||
@ -506,38 +511,60 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
Межформенная: `1370(отчётная дата) − 1370(начало года) = 2400(ОФР за период)` — при расхождении КА может акцептовать
|
||||
с комментарием (`financial_statement_meta.checks_accepted_*`).
|
||||
|
||||
**Парсер tabular-parser (§12 п. 7)** считает **жёсткими** (нарушение → `needs_check`) соотношения ББ — `1100`, `1200`
|
||||
(+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700` — и ОФР — `2100`, `2200`,
|
||||
`2300`; **мягкими** (предупреждение) — `2400 = 2300 − 2410 + 2420` и варианты ФСБУ 4/2023 (`2400 = 2300 + 2410 + 2460`,
|
||||
`2410 = 2411 + 2412`), `2500 = 2400 + 2510 + 2520 + 2530` и «сумма пятизначных подстрок = родитель»
|
||||
(`ASSUMPTION(OQ-12)`), последняя — по колонке и только если хотя бы одна подстрока родителя в ней непуста (`amount` не `null`; ноль — значение): без детализации соотношение не проверяется, ни нарушения, ни предупреждения (AURORA-1527, решение владельца 16.10.2026). Допуск ±1 в единицах формы (расхождение до 0,001 % наибольшего числа соотношения — округление,
|
||||
мягкое, OQ-138); соотношения считаются по отображаемым значениям (п. 5 §12), расходы — двойной расчёт знаков (`abs_terms`). Межформенная проверка
|
||||
`1370 − 1370(начало года) = 2400` парсером не выполняется. Это сигнал о структуре и данных файла, а не замена проверок
|
||||
ядра: ядро (Срез 3) пересчитывает §7 и ведёт акцепт (`financial_statement_meta.checks_accepted_*`).
|
||||
**Самопроверка в сервисах (общий код, §11.8; change `unified-statement-normalization`, контракт v2.0).** Оба сервиса
|
||||
(tabular-parser и scan-gateway) считают §7 одним кодом `arm_statements` по одному словарю `statement_checks.yaml` и кладут
|
||||
результат в `documents[].checks` (`relations_failed`, `soft_relations_failed`; у нарушения — `column_index`, текст
|
||||
соотношения, `codes` — коды цели и членов, `expected`, `actual`). Ядро хранит `checks` как пришли (`document_issue`) и
|
||||
пересчитывает тем же кодом только после правок аналитика; своей копии словаря у ядра нет. Правила счёта:
|
||||
|
||||
**Ядро (Срез 3b, реализовано).** Ядро считает §7 само, для обоих источников (Excel и OCR), по **текущим** суммам формы
|
||||
(`financial_statement_value.amount` после правок аналитика); результат не хранится, считается на лету (`GET
|
||||
/documents/{id}/statement`, подтверждение). Правила — словарь `arm-core/api/rules/statement_checks.yaml`: **копия**
|
||||
словаря tabular-parser (те же жёсткие и мягкие соотношения, допуск ±1 в единицах формы, округление 0,001 %, `abs_terms`,
|
||||
мягкая «сумма пятизначных подстрок = родитель», по колонке — только если хотя бы одна подстрока родителя в ней непуста: `amount` не `null`, ноль — значение; пустая детализация не проверяется, AURORA-1527); тест идентичности копий между репозиториями не ставится, расхождение —
|
||||
известный риск (`03-architecture.md`). Соотношение считается по каждой колонке и только если строка-цель (левая часть) есть в форме;
|
||||
отсутствующий код и пустая сумма — 0 (ненулевые слагаемые при пустой цели дают жёсткое нарушение). Результат в ответе API: `failed` (жёсткие), `soft_failed` (мягкие, в
|
||||
том числе «в пределах округления»), `cross_form`, `accepted`.
|
||||
- **Жёсткие** (нарушение → `needs_check`): ББ — `1100`, `1200` (+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`,
|
||||
`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).
|
||||
- **Значение кода** в колонке — сумма по всем строкам `ok` с этим кодом (повтор кода — законный случай, Р-4); строки
|
||||
`detail`, `unreadable`, `missing` в соотношения не входят; нарушение относится ко всем строкам его кодов.
|
||||
`duplicate_line_codes` не выдаётся.
|
||||
- **Посчитанное соотношение.** Соотношение считается, если непуста цель или хотя бы один член (есть строка `ok` с кодом
|
||||
и непустой суммой); пусто всё — пропускается. Пустая цель при непустых членах — нарушение («БАЛАНС» без кода ушёл в
|
||||
`detail` — 1600 пуст, `1600 = 1100 + 1200` подсвечивает родителя); непустая цель при пустых членах — тоже нарушение
|
||||
(лист только с активом: `1600 = 1700` при отсутствующем 1700 — `needs_check`). До v2.0 соотношение с отсутствующей
|
||||
целью пропускалось у обоих сервисов и в ядре — поведение изменено; посчитанное соотношение из одних `null` ничего не
|
||||
подтверждает.
|
||||
- **Допуск и округление:** жёсткое соотношение не сошлось, если расхождение больше ±1 в единицах формы **и** больше
|
||||
0,001 % наибольшего по модулю числа соотношения — только тогда оно в `relations_failed`; расхождение в пределах
|
||||
округления (OQ-138) и мягкие соотношения — в `soft_relations_failed`, это `info`, на статус и подсветку не влияют.
|
||||
Соотношения считаются по отображаемым значениям (§11.2, числовой формат Excel).
|
||||
- **Члены по модулю (`abs_terms`: 2120, 2210, 2220, 2330, 2350, 2410).** Правая часть считается дважды — как записано и
|
||||
с этими членами по модулю (1С выгружает расходы со знаком минус, 66н — в скобках). Такое соотношение сходится при
|
||||
любом знаке члена и потому **подтверждает величину, но не знак**: знак сомнительной ячейки подтверждается конвенцией
|
||||
колонки (§11.9).
|
||||
- **Упрощённый набор.** Набор выбирается по `form_variant` документа (§11.7): у упрощённой формы нет подытогов 1100 /
|
||||
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`. Без
|
||||
упрощённого набора полный дал бы нарушения «пустой цели» на каждой упрощённой форме.
|
||||
- **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
|
||||
из первых четырёх цифр (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code`.
|
||||
- **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже).
|
||||
|
||||
**Ядро.** При загрузке результата 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(начало
|
||||
года) = 2400` — **мягкая**, считается ядром по агрегату формы по коду (§9.5 п. 2) и не блокирует подтверждение: пара —
|
||||
другой документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый
|
||||
предпочтительнее, далее самый поздний `created_at`); считается, только если найдены пара и обе колонки. Результат в
|
||||
ответе API: `failed` (жёсткие), `soft_failed` (мягкие, в том числе «в пределах округления»), `cross_form`, `accepted`;
|
||||
у нарушения — `codes` и `line_ids`.
|
||||
Показ пятизначных подстрок (AURORA-1527, решение владельца 16.10.2026): ядро отдаёт у каждой строки формы вычисляемое
|
||||
`parent_code` — код четырёхзначного родителя подстроки по тому же правилу, что и проверка «сумма подстрок = родитель»
|
||||
(п. 7 §12; одна функция в ядре, порядок кодов тот же); у остальных строк `null`. Экран показывает подстроки сразу под
|
||||
родителем, пустые подстроки (нет значения ни в одной колонке) свёрнуты за переключателем; правила родителя на фронте нет.
|
||||
Только показ: парсеры и данные не меняются.
|
||||
Межформенная `1370(отчётная дата) − 1370(начало года) = 2400` — **мягкая** и не блокирует подтверждение: пара — другой
|
||||
документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый предпочтительнее, далее
|
||||
самый поздний `created_at`); считается, только если найдены пара и обе колонки (ОФР часто приходит отдельным файлом позже).
|
||||
`parent_code` (правило выше); экран показывает подстроки под родителем, пустые подстроки свёрнуты за переключателем.
|
||||
**Акцепт** — по жёстким нарушениям, с комментарием (1–2000 символов), `POST /documents/{id}/statement/checks/accept`;
|
||||
без нарушений — 409 `NOTHING_TO_ACCEPT`. Акцепт **сбрасывается любой правкой формы** (значения или шапки): данные
|
||||
изменились, проверку нужно смотреть заново. Аудит акцепта — `RECOGNIZED_DATA_EDIT` (отдельного `audit_action` нет, новое
|
||||
значение enum потребовало бы DBML v5.17). Формулы одни для редакций `66n` и `fsbu4` (`ASSUMPTION(OQ-12, OQ-146)`); редакцию
|
||||
ядро определяет по дате периода документа и отдаёт в ответе (`edition`: `66n` при дате ≤ 2024-12-31, `fsbu4` — позже, `null`
|
||||
без даты; решение владельца 07.10.2026 по OQ-145 — редакция в контракт не передаётся, досрочный ФСБУ 4/2023 — новый OQ при
|
||||
первом случае); раздельные словари и формулы по редакциям — вместе с составом `fsbu4` (OQ-146, OQ-150).
|
||||
без нарушений — 409 `NOTHING_TO_ACCEPT`. Акцепт **сбрасывается любой правкой формы**. Аудит акцепта —
|
||||
`RECOGNIZED_DATA_EDIT`. Формулы одни для редакций `66n` и `fsbu4` (`ASSUMPTION(OQ-12, OQ-146)`); редакцию ядро определяет
|
||||
по дате периода документа и отдаёт в ответе (`edition`: `66n` при дате ≤ 2024-12-31, `fsbu4` — позже, `null` без даты;
|
||||
OQ-145); тем же правилом сервисы выбирают редакцию упорядоченного перечня кодов (§11.6).
|
||||
|
||||
**Полный состав формы (arm-core#80, решение владельца 09.10.2026).** Экран проверки показывает все строки формы
|
||||
редакции документа (`edition`): справочник `arm-core/api/rules/statement_lines.yaml` (66н и ФСБУ 4/2023, ББ и ОФР, из
|
||||
@ -546,11 +573,19 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
(`line_code_original_value = NULL` — признак «не из источника») и ячейку как правку аналитика (аудит
|
||||
`RECOGNIZED_DATA_EDIT`). Пустая добавленная строка не влияет на статус, проверки §7 и автоподтверждение; созданная
|
||||
правкой строка участвует в проверках, пока у неё есть хоть одна сумма. Без редакции (нет даты периода) строки не
|
||||
достраиваются. Строки «БАЛАНС» (1600, 1700) на экране выделяются цветом.
|
||||
достраиваются. Строки «БАЛАНС» (1600, 1700) на экране выделяются цветом. **С v2.0** (change
|
||||
`unified-statement-normalization`): справочник ядра — только словарь имён (код → наименование по редакциям); порядок и
|
||||
состав редакций ядро берёт из копии упорядоченного перечня кодов `arm_statements` (`sync-statements --check` сверяет, что
|
||||
все коды словаря имён есть в перечне); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не
|
||||
достраивается; строки `unreadable` / `missing` шаблонной строкой молча не заменяются — показываются отдельно со статусом.
|
||||
Наименование строки из источника хранится как распознано; экран показывает справочное имя по коду, файловое — рядом
|
||||
(Р-3, AURORA-1406 / 1522).
|
||||
|
||||
Когда: после распознавания; после «Сохранить» правок; при «Подтвердить данные». Непройдено → `document.status =
|
||||
NEEDS_CHECK`; пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`.
|
||||
Отрицательные значения в формах — в скобках `(2100)` → `-2100`. Значения с `confidence < 100` подсвечиваются.
|
||||
Когда: в сервисе при распознавании; в ядре после «Сохранить» правок. Непройдено → `document.status = NEEDS_CHECK`;
|
||||
пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`. Отрицательные значения в
|
||||
формах — в скобках `(2100)` → `-2100`. Подсветка ячеек — только по `needs_review` (§11.9): уверенность, `is_recognized` и
|
||||
нарушения §7 сведены в него сервисом; «подтверждение проверкой» — сошедшееся посчитанное жёсткое соотношение снимает
|
||||
подсветку с сомнительной ячейки (Р-7).
|
||||
|
||||
---
|
||||
|
||||
@ -650,6 +685,16 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
|
||||
(`financial_statement_meta` нет), подтверждается без условий п. 9.5.4, а `GET`/`PATCH …/statement` по нему — 409
|
||||
`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`; определены
|
||||
`period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence
|
||||
< 100`) распознанное наименование совпало со справочным по коду (Р-16, правило совпадения — §11.9); в файле нет второй
|
||||
формы того же типа и периода (Р-18). Иначе `NEEDS_CHECK`. Проверки §7, `needs_review` и предупреждения сервиса при
|
||||
загрузке не пересчитываются. После правки или акцепта статус пересчитывается общим кодом (§11.9) с подтверждениями
|
||||
аналитика и теми же условиями ядра; `RECOGNIZED` после правки — только вручную, как и прежде.
|
||||
|
||||
### 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 мин.
|
||||
|
||||
@ -687,6 +732,24 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`NEEDS_CHECK`. Файл только с формами — не ошибка; `ERROR` по результату — только при пустом `documents`. Страницы одной
|
||||
формы из разных файлов не склеиваются (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`,
|
||||
`source_ref` (`ref`, уникален в форме), `line_status`, `line_code` (`null` у `unreadable` / `missing` / `detail`),
|
||||
`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`); версия общего кода —
|
||||
`recognition_job.statements_version`. Каждый `BALANCE` / `INCOME` становится отдельным документом с `file_ordinal`:
|
||||
вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не
|
||||
автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
Все чтения формы «по коду» (межформенная проверка, суммы формы для консолидации и выгрузок, `parent_code`, раскладка
|
||||
экрана, правка по `line_code`, метрики `fields_*`) идут через одну чистую функцию **«агрегат формы по коду»**: сумма
|
||||
`amount` строк `ok` с кодом по колонке, список `line_id`, справочное имя (DECISION-5, не хранится). Строки `detail`,
|
||||
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8. Записи `document_issue` удаляются вместе
|
||||
с документом при перезагрузке файла.
|
||||
3. **Автозамена.** После успешного маппинга другие файлы того же ЮЛ (не `SUPERSEDED`), содержащие документ с теми же
|
||||
`document_type`, `osv_account_code`, `period_year`, `period_quarter`, переводятся в `SUPERSEDED`
|
||||
(`superseded_by_file_id` = новый файл); `version_no` нового = максимум заменённых + 1. Новый файл без контекста (`consolidation_id IS NULL`) наследует `consolidation_id` первого (по `id`) заменённого файла, у которого он задан; файл, загруженный в консолидацию, сохраняет свою (v5.18). Заменяется и файл с `CONFIRMED`
|
||||
@ -710,6 +773,11 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`PERIOD_REQUIRED` (поправьте дату отчёта, п. 9); непройденные неакцептованные жёсткие проверки §7 — 409
|
||||
`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`.
|
||||
Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не допускает).
|
||||
При подтверждении пишется `financial_statement_meta.reviewed_by/at`. Несовпадение контрагента (`MISMATCH`) и низкая
|
||||
уверенность значений подтверждение **не блокируют** (как у ОСВ): защита — статус `NEEDS_CHECK` и подсветка. Мягкие проверки
|
||||
(в том числе межформенная) не блокируют. `DISCLOSURE`/`OTHER` — без условий. Привязка к версиям и переходы статуса версии —
|
||||
@ -750,6 +818,27 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
без изменений — 200 без аудита. Автозамена при правке не запускается (только при маппинге): правка даты, давшая период
|
||||
другого файла ЮЛ, привязывает документ к версии лишь при подтверждении. Порядок блокировок — как у подтверждения (файл →
|
||||
документ).
|
||||
**С 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`).
|
||||
После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue`
|
||||
видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица
|
||||
§11.9). Расхождение `statements_version` задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH`. Один PATCH —
|
||||
один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям).
|
||||
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
|
||||
объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует
|
||||
видимость); работает и для `SUPERSEDED`; объекта нет в хранилище — 404 `FILE_CONTENT_NOT_FOUND`. Комментарии к документу —
|
||||
@ -912,262 +1001,445 @@ OQ-92 (пустые «Организация»/«Группа компаний»
|
||||
|
||||
---
|
||||
|
||||
## §11. Маппинг JSON ИС Сканирование (FNR01/FNR02) → формы
|
||||
## §11. Нормализация форм ББ/ОФР — общая цепочка (scan-gateway и tabular-parser)
|
||||
|
||||
Формат ответа — **по реальным файлам** `fixtures/ocr/aep_3.json`, `aep_3_ofr.json`, `aep_6.json` (проверено 04.10.2026; ТЗ
|
||||
и первая редакция этого параграфа расходятся с ними — см. OQ-13, OQ-14, OQ-77):
|
||||
**Статус: целевое правило** change `unified-statement-normalization` (RSHB/arm-ka-docs#116; дизайн —
|
||||
`openspec/changes/unified-statement-normalization/design.md`, решения владельца Р-1…Р-19 от 17.10.2026), контракт
|
||||
v2.0. Порядок выкладки — «сначала ядро» (ядро принимает 1.x и 2.0); пока сервис пишет 1.x, он работает по прежним
|
||||
правилам — прежний текст §11, §11.1 и §12 (Срезы 2b, 3a, 3b) — в истории git репозитория docs, коммит `43b4b80`.
|
||||
§11 объединяет прежние §11 (маппинг json ИС), §11.1 (очистка OCR-текста) и §12 (ББ и ОФР из Excel); §12 оставлен
|
||||
заголовком-ссылкой.
|
||||
|
||||
- Каталог ответа `<job_id>/` содержит **несколько json** — по одному на распознанный документ файла (пример: `aep_3.json` —
|
||||
успех, `aep_3_ofr.json` — ошибка по второму документу того же PDF). Шлюз читает все json каталога и склеивает в один ответ
|
||||
(`raw/{job_id}.json`: `{job_id, files[{name, response, parse_error}]}`); ошибка одного файла не отменяет остальные.
|
||||
- Корень файла: `{file_uid, error {code, message}, data_ocr}`. **Успех — `error.code == ""`** (поле `error` не `null`, а
|
||||
`{"code": "", "message": ""}`); ошибка — непустой `code` (`INVALID_DOCUMENT`, `FILE_READ_ERROR`, `INVALID_FORMAT`,
|
||||
`RECOGNITION_FAILED`, `UNKNOWN_ERROR`), `data_ocr` с пустыми полями и `table_structure: []`.
|
||||
- `data_ocr` — **объект** (не список): `label`, `organization`, `inn`, `units`, `date` — каждый `{form_name, confidence}`
|
||||
(в `form_name` шумный OCR-текст; `date.form_name` ненадёжна, ИНН может быть мусором), `table_structure` — **список таблиц**
|
||||
`{label {form_name, confidence}, rows[]}`: `label.form_name` «Бухгалтерский баланс» → BALANCE, «Отчет о финансовых
|
||||
результатах» → INCOME. **Наблюдается:** в одном json может быть **несколько таблиц разных форм** (в `aep_3.json` и
|
||||
`aep_6.json` — баланс и ОФР), каждая со своим `label`; тип документа задаёт `label`. Разбиение одной формы на несколько
|
||||
таблиц (сегменты) в образцах **не встречалось** и не предполагается; если одна форма вдруг придёт двумя таблицами или
|
||||
`label` повторится — это нештатный случай: документ не создаётся молча, а уходит в `NEEDS_CHECK` с причиной (решение —
|
||||
Срез 3, OQ-77). Неизвестный `label` → `OTHER`.
|
||||
- `rows[]`: `row_index`, `cells[] {column_index, value, confidence}`; `column_index` не обязательно подряд (бывают 0, 1, 2, 4, 5),
|
||||
`value` — сырой OCR-текст (мусор в ячейках, переносы строк, скобки для отрицательных, пробелы-разделители тысяч).
|
||||
В `aep_6.json` `rows[0]` — строка-шапка («Наименование показателя», «Код», «На 30 июня 2025 г», …); в `aep_3.json` шапки
|
||||
**нет** — первая строка данных. Бывают строки без кода (расшифровки «в том числе…», OQ-36) и код-строка внутри ячейки
|
||||
наименования («АКТИВ / I. Внеоборотные активы / Гудвил» с кодом 1105).
|
||||
- `confidence` — **числа 0–100** (в ТЗ — доли 0..1; в реальных файлах проценты) → `numeric(5,2)` как есть.
|
||||
Источник требований: ТЗ 6.2 («парсинг ОСВ, бухгалтерского баланса и ОФР»), DOC-037734, DOC-221281, FNR01/FNR02; формат —
|
||||
Приказ 66н / ФСБУ 4/2023, на практике — скан бланка (ИС Сканирование), выгрузка 1С («Регламентированный отчёт»), шаблон
|
||||
банка и вручную набитая таблица.
|
||||
|
||||
Маппинг (Срез 3): таблица `table_structure[i]` с `label` «Бухгалтерский баланс» / «Отчет о финансовых результатах» →
|
||||
`document` (BALANCE/INCOME; `locator = file:<имя json>;table:<i>` — так его формирует scan-gateway для текстового
|
||||
PDF-пути, решение №1 спеки 3a; `pages:…` — только если ИС отдаст номера страниц) + `financial_statement_meta`
|
||||
(ИНН, имя, units, report_date, okud — из `organization/inn/units/date.form_name`, с низкой уверенностью → `NEEDS_CHECK`) +
|
||||
`financial_statement_column` (из шапки `rows[0]`, если она есть; `period_date` распарсить из «На 30 июня 2025 г», `is_current` =
|
||||
первая; без шапки — колонки без дат, документ `NEEDS_CHECK`) + `financial_statement_line` (`cells[1]` код, `cells[0]` имя;
|
||||
только строки с кодом) + `financial_statement_value` (число: убрать пробелы/`-`, скобки → минус; `confidence`; `is_recognized`).
|
||||
Недостающие строки формы ядро достраивает на экране по справочнику редакции (§7, arm-core#80).
|
||||
Сверки: ИНН/имя vs `legal_entity` → `document.counterparty_check`; дата vs период версии → `period_check`.
|
||||
**Принципы.**
|
||||
|
||||
### §11.1 Очистка OCR-текста (перенос из старого ядра, Срез 3)
|
||||
1. **Один выход на шаг.** Код, зависящий от источника, живёт только в шаге 1 (адаптеры «json ИС → кандидат», «лист
|
||||
Excel → кандидат»). Шаги 2–8 — общий код с общими словарями. Единственное место, где общий код смотрит на источник, —
|
||||
разбор запятой и точки в сумме (шаг 4).
|
||||
2. **Ничего не выбрасывать молча.** Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
|
||||
статусом, а не превращается в счётчик; положение в источнике (`ref`), порядок строк (`row_index`) и сырой текст
|
||||
ячейки (`text`) доезжают до базы и экрана. Подтвердить документ, не разобрав строки с суммами без кода, нельзя (Р-13).
|
||||
3. **Качество — структурно и с привязкой к месту:** уверенность ячейки (от источника), `is_recognized` (разбор числа),
|
||||
статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность, `is_recognized` и §7
|
||||
сервис сводит в один признак `needs_review` у значения, строки и поля шапки и из них выводит статус документа. Ядро
|
||||
при загрузке ничего из этого не пересчитывает.
|
||||
4. **Один словарь — одно место:** проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, подписи колонок,
|
||||
упорядоченные перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде
|
||||
сервисов; соответствие «код → наименование» — только в ядре (Р-3).
|
||||
5. **Очистка OCR — не отдельный шаг**, а первая стадия типовых разборов текста там, где уже известен смысл ячейки (дата,
|
||||
подпись колонки, код, разлепление, сумма).
|
||||
|
||||
Источник — модуль старого ядра `DKB_ARMKA` `api/src/services/ofr_result_conversion/` (`utils.py`, `date_utils.py`,
|
||||
`consts.py`; в локальном `reference-old-arm` тот же код лежит в `api/src/core/PDFWorker/`). Решение владельца
|
||||
06.10.2026: модуль **проверен на реальном стенде ИС Сканирование, его код можно переносить** в нормализатор `scan-gateway`.
|
||||
Это **единственное исключение** из правила «доменный код из `reference-old-arm` не переносить» (CLAUDE.md workspace).
|
||||
Переносятся чистые функции очистки и разбора; обвязка старого ядра (`json_to_db_rows`, dataclass'ы `Meta/DbRow/DbValue`,
|
||||
прямая запись в `financial_statement_*`) не переносится — результат ложится в `statementDocument` контракта v1.2
|
||||
(`string_value`, `confidence`, `is_recognized`, `period_date`, `period_text`, `line_code_confidence`). Перенос — с тестами
|
||||
на `fixtures/ocr/aep_*.json`; изменения правил ниже — только через этот параграф.
|
||||
**Где код.** Шаги 2–8, общие разборы и словари — пакет `arm_statements/` репозитория `arm-statements` (Р-1); в
|
||||
tabular-parser, scan-gateway и arm-core он лежит копией (раскладывает и проверяет `sync-statements` в docs, прямые правки
|
||||
копии запрещены, маркер версии — `service.statements_version`). Golden-тесты пакета — на синтетических и обезличенных
|
||||
данных внутри пакета; тесты на `fixtures/` остаются в сервисах. Изменения правил ниже — только через этот параграф и
|
||||
словари пакета.
|
||||
|
||||
1. **Буквы-двойники рядом с цифрами** (`date_utils._normalize`, `_LOOKALIKES`): `О/о/O/o → 0`, `З/з → 3`,
|
||||
`l/I/|/і → 1`, `б → 6`, `S/s → 5`, `Ч → 4` — заменяются **только если символ стоит вплотную к цифре**, в несколько
|
||||
проходов («2ОО5» → «2005»); слова («Общество») не портятся. Если первая строка значения состоит только из цифр,
|
||||
пробелов, `|`, точек и двоеточий — `|` считается разделителем клеток формы («2|0|2|4|» → «2 0 2 4»), а не единицей.
|
||||
**Применяется только к датам** — дате отчёта (шапка) и подписям колонок значений; коды строк очищаются п. 6
|
||||
(`clean_value`), суммы — п. 7 (`parse_amount`), ИНН — только цифры (п. 8). Замена двойников внутри кодов и сумм на
|
||||
стенде не проверялась, поэтому не делается (решение №22 спеки 3a): код с латинской `O` вместо нуля не распознаётся
|
||||
как целое и остаётся только в `raw/`.
|
||||
2. **Даты из OCR-текста** (`date_utils._parse_date`): сначала текстовые формы по всей строке в порядке приоритета —
|
||||
диапазон «с 01.01.2025 по 30.09.2025» (берётся конец), «30 сентября 2025 г./r./года» (латинская `r` вместо `г`),
|
||||
«I квартал 2026» / «1-й кв.» / «первый квартал», «1 полугодие» / «1 п/г», «9 месяцев 2025», «январь – сентябрь 2025»
|
||||
(последний месяц), «за 2025 г.» / «2025 год» (31.12); затем числовые — три группы цифр («30.09.2025», «2025 03 31»,
|
||||
«30/09/25»), иначе цифры склеиваются: 8 цифр → ДДММГГГГ / ГГГГММДД / ГГГГДДММ (первый валидный вариант), 4 → год
|
||||
(31.12, только 1990–2100), 6 → ДДММГГ; хвост «0:00:00» отбрасывается. Двузначный год → 20YY. Период квартала/полугодия/
|
||||
N месяцев → последний день последнего месяца. Не дата (название организации) → `None`. Разборщик общий с §12 п. 4
|
||||
(Excel), там на вход приходят чистые строки. **Отличие от источника (решение №31 спеки 3a):** текст длиннее 200
|
||||
символов не разбирается (`None`, двойники к нему не применяются) — регулярки квартала и полугодия дают
|
||||
экспоненциальное/кубическое backtracking на длинной строке; настоящая дата занимает десятки символов.
|
||||
3. **Раскладка колонок** (`_build_layout`, `_infer_layout`): шапка — строка с ячейкой, начинающейся на «код» (после
|
||||
нормализации: нижний регистр, всё не-буквенно-цифровое → пробел); наименование — ячейка с «наименование», иначе ближайшая
|
||||
заполненная слева от кода; колонки значений — правее кода, где в тексте есть четыре цифры года или текст начинается на
|
||||
«на » / «за ». Повторная шапка внутри таблицы **перезаписывает** раскладку. **Без шапки** раскладка выводится по данным:
|
||||
код — столбец с наибольшим числом значений из справочника кодов строк; наименование — столбец левее кода с наибольшим
|
||||
числом текстовых ячеек; значения — столбцы правее кода, где суммы не реже мусора (полоса скана) или столбец пуст
|
||||
(период без данных); шапка синтезируется (`Код` / `Наименование показателя`, тексты периодов пустые). Строки до первой
|
||||
настоящей шапки разбираются по выведенной раскладке. Ни шапки, ни столбца кода — таблица пропускается с warning.
|
||||
**Рулинг 06.10.2026 (вместо прежнего «без шапки → колонки без дат, `NEEDS_CHECK`»):** раскладку выводить, как в старом
|
||||
ядре, но документ без шапки всё равно отдавать `needs_check` с причиной в `metrics.warnings` — колонки без `period_date`
|
||||
аналитик подтверждает руками.
|
||||
4. **Классы строк** (`_classify_row`): пустой код → строка пропускается; нечисловой код → заголовок (не данные);
|
||||
наименование «Баланс» → `BALANCE`, начинается с «итого» → `TOTAL`, иначе обычная строка. Берутся только строки с
|
||||
целым кодом (как в §11 выше); расшифровки без кода — OQ-36. Строки, отброшенные классификацией, **не восстанавливаются**
|
||||
(поведение источника), но о них выдаётся предупреждение уровня таблицы (решение №29 спеки 3a): `строк с нечисловым
|
||||
кодом пропущено: N` — код непустой и не целое число (`1150 :`); `строк без кода с суммами пропущено: N` — код пуст, но в
|
||||
колонках значений есть разобранная сумма (в `aep_3.json` «БАЛАНС» и «Выручка» имеют пустой код, мусор `211(1 |` стоит в
|
||||
колонке значения; как в Excel-пути, OQ-141). Строки с пустым кодом без сумм (заголовки групп, «в том числе:»)
|
||||
предупреждения не дают. Любая такая таблица — `needs_check`; восстановление строк по наименованию — OQ-149.
|
||||
**Склеенные строки** (OQ-156, решение владельца 08.10.2026): ИС может вернуть две соседние строки формы одной строкой —
|
||||
в ячейке кода несколько кодов через перевод строки, суммы тоже через перевод строки (ББ РБ и Счастливочной за 1 кв.
|
||||
2025: код `1500\n1700`, наименование `Итого по разделу V\n\nБАЛАНС`, суммы `917 775\n2 371 333`). До классификации
|
||||
такая строка **разлепляется**: если в ячейке кода N ≥ 2 непустых частей, каждая — целое число, и в каждой ячейке
|
||||
колонок значений либо пусто, либо ровно N непустых частей, получается N строк: i-й код, i-я часть каждой ячейки
|
||||
значений (пустая ячейка — пусто во всех), наименование — i-я из непустых частей, если их тоже N, иначе всё
|
||||
наименование у каждой; уверенность — исходной ячейки. Предупреждение уровня таблицы `строк с несколькими кодами
|
||||
разлеплено: N` (документ `needs_check`: разбивку подтверждает аналитик). Число частей не сходится хотя бы в одной
|
||||
ячейке — строка не разлепляется и отбрасывается как нечисловой код (предупреждение выше).
|
||||
**Повтор кода** в таблице: остаётся первая строка, остальные не попадают в документ, выдаётся предупреждение «код N
|
||||
повторяется: оставлена первая строка», документ `needs_check` (как §12 п. 5: в БД `financial_statement_line`
|
||||
уникален по `(meta_id, line_code)`; старое ядро дубли не убирало — решение №15 спеки 3a). Известный недостаток: в
|
||||
`aep_6` (ОФР) код 2410 повторён, первая строка пуста, а отброшенная содержит `(649)` — OQ-152 (сумма отброшенной
|
||||
строки остаётся в `raw/`).
|
||||
5. **Тип таблицы** (`_resolve_table_type`): по первой цифре кодов строк данных — больше `1xxx` → BALANCE, больше `2xxx` →
|
||||
INCOME; это запасной путь, когда `label.form_name` зашумлён; при расхождении с `label` — `needs_check`.
|
||||
6. **Код и наименование строки**: код очищается до букв, цифр, пробела и точки (`_clear_value`: переносы строк и
|
||||
табуляции → пробел, остальное выбрасывается); наименование берётся **из справочника по коду** (`consts.codes_mapping`;
|
||||
у нас — `api/rules/statement_lines.yaml` **scan-gateway** с редакциями форм `66n` / `fsbu4`, см. п. 10, а не
|
||||
`rules/statement_lines.yaml` из §12 — в tabular-parser такого файла нет), а из OCR — только если кода нет в справочнике
|
||||
выбранной редакции (`line_code_confidence < 100`, не выше 99). Код считается распознанным (`line_code_recognized`), если
|
||||
`confidence == 100`, очищенный текст равен сырому и код есть в справочнике.
|
||||
7. **Суммы** (`_parse_amount`): пробелы убираются; **сначала разделитель тысяч** (см. ниже), затем запятая → точка;
|
||||
содержимое скобок — отрицательное; из строки
|
||||
удаляется всё, кроме цифр, `+`, `-`, `.`; если точек больше одной, **удаляются все** (запятая уже стала точкой:
|
||||
`1.234,56` → `123456`, дробная часть не сохраняется, `is_recognized = false`); `Decimal`, иначе `None`. Прочерк, «–», «—», пусто — пустое значение (NULL, не 0). `string_value` — текст ячейки как распознан
|
||||
(`\n`/`\t` → пробел), **только если сумма не разобрана** (`amount = null`) и ячейка не пуста и не прочерк (DBML:
|
||||
«как распознано, если не число»); у разобранных сумм — `null` (решение №16 спеки 3a).
|
||||
**Перенесено дословно (расхождения с желаемым — OQ-147, OQ-148):** закрывающая скобка, распознанная как `}` или `]`
|
||||
(`(7744536}`), минус не ставит — сумма положительная, `is_recognized = false` (решение №12); длинное/среднее тире
|
||||
«—»/«–» при `confidence = 100` даёт `is_recognized = false` (тире вырезается как «лишний» символ), дефис «-» — `true`
|
||||
(решение №20). Скобки сами `is_recognized` не портят: признак считается до постановки минуса (решение №13; в источнике
|
||||
любая сумма в скобках получала `false`).
|
||||
**Разделитель тысяч** (AURORA-1405, scan-gateway#35, решение владельца 15.10.2026; отличие от источника, где «4,200» в
|
||||
PDF означало 4 200, а в прежнем АРМ показывалось как 4,2; в scan-gateway до этого правила ячейка отсекалась решением №30:
|
||||
`amount = null`, текст в `string_value`): если текст ячейки после `strip` и удаления пробелов, с учётом скобок
|
||||
(шаблон применяется к содержимому скобок, только если пара скобок занимает всю строку) и знака, полностью совпадает с `[+-]?[1-9]\d{0,2}(,\d{3})+`, запятые — разделители
|
||||
тысяч и **удаляются до** замены запятой на точку: `4,200` → 4200, `(4,200)` → −4200, `1,234,567` → 1234567, `-4,200` →
|
||||
−4200, `+4,200` → 4200. Удаление таких запятых не считается отбрасыванием символов: `is_recognized` определяется только
|
||||
`confidence == 100`. Остальное без изменений: `4,2` → 4.2, `12,34` → 12.34, `0,125` → 0.125 (далее отсекается решением
|
||||
№30), `1,23` → 1.23, `1 234,56` → 1234.56, `1.234,56` и `1,234.56` → 123456 (`is_recognized = false`); `12,3456`,
|
||||
`4,200,`, `(4,200}`, `(4,200) руб`, `abc(4,200)` и `(4,200)(1,000)` шаблону не соответствуют, разбор прежний.
|
||||
**Точка — разделитель тысяч** (AURORA-1405, scan-gateway#37, решение владельца 16.10.2026: ИС Сканирование местами
|
||||
возвращает «4.200», «837.994»; такие суммы разбирались как 4.200 с тремя знаками и отсекались решением №30 в
|
||||
`amount = null`, текст в `string_value`): то же правило, но шаблон шире (решение владельца 16.10.2026: если после точки ровно три цифры — это разделитель тысяч независимо от числа цифр до неё; если суммы не сойдутся, аналитик увидит это на проверках): `[+-]?[1-9]\d*(\.\d{3})+` — те же условия
|
||||
(весь текст ячейки после `strip` и удаления пробелов или содержимое пары скобок на всю строку), точки удаляются,
|
||||
`is_recognized` определяется только `confidence == 100`: `4.200` → 4200, `837.994` → 837994, `(4.200)` → −4200,
|
||||
`-4.200` → −4200, `1.234.567` → 1234567 (раньше при нескольких точках — `is_recognized = false`), `1234.567` →
|
||||
1234567, `12345.678` → 12345678, `1 234.567` → 1234567, `(1 234.567)` → −1234567. Остальное без изменений: `4.2` → 4.2,
|
||||
`4.20` → 4.20, `12.34` → 12.34, `1234.56` → 1234.56 (дробная часть не из трёх цифр — дробь); `0.125` и `01.234` (первая
|
||||
цифра нулевая — не тысячи) → дробь, при трёх знаках отсекается решением №30; `1.2345` — текст. Правило для запятой
|
||||
этим **не расширяется**: `1234,567` и `1 234,567` по-прежнему дробь 1234.567 (отсекается №30), тысячи — только
|
||||
`\d{0,2}` цифр до первой запятой. Смешанные записи
|
||||
(`1.234,56`, `1,234.56`, `1.234,567`) и «грязные» (`4.200.`, `(4.200}`, `(4.200) руб`, `1.234.56`) шаблону не
|
||||
соответствуют, разбор прежний. Правило действует только на ячейки колонок значений таблицы ББ/ОФР (`parse_amount` не
|
||||
вызывается для дат, кодов строк, ОКЕИ, ИНН); дробь с тремя знаками после точки и так не принималась (`Money`, №30).
|
||||
**Отличия от источника под контракт** (решение №30 спеки 3a): сумма с **более чем двумя** знаками после запятой
|
||||
(например `0,125`, `12.3456`) не укладывается в `Money` контракта (`numeric(18,2)`) — `amount = null`, `string_value` —
|
||||
текст ячейки, `is_recognized = false` (иначе один артефакт OCR валил всю таблицу); `1,234` под это не попадает —
|
||||
разбирается правилом тысяч выше (= 1234);
|
||||
`confidence` ячейки и полей шапки приводится к диапазону 0–100, NaN → 0.
|
||||
**`is_recognized`** значения = `confidence == 100` **и** разбор суммы не потребовал выбрасывать символы (сырой текст без
|
||||
скобок равен очищенному). Для ячеек шапки: `is_recognized` = `confidence == 100` и в тексте не было мусора; если дата
|
||||
разобралась — `period_text` нормализуется в «На ДД.ММ.ГГГГ», `period_date` — результат п. 2. Ячейки под колонкой
|
||||
заголовка нет — пустое значение с `is_recognized = false`, чтобы не сдвигать столбцы.
|
||||
8. **Шапка документа**: ИНН — только цифры из `inn.form_name`, «распознан», если `confidence == 100` и в сыром тексте не
|
||||
было ничего, кроме цифр (предупреждение уровня файла: `ИНН не распознан` — в поле нет ни одной цифры; `ИНН распознан
|
||||
неуверенно` — цифры извлечены, но `confidence` ≠ 100 или убран мусор); единицы — `_clear_value`, тот же критерий; дата отчёта — п. 2, «распознана», если
|
||||
`confidence == 100` и дата разобралась. Нераспознанное → `needs_check` (как в §11).
|
||||
9. **Ошибка разбора одной таблицы** (исключение в разборе `table_structure[i]`) не роняет файл: выдаётся предупреждение
|
||||
уровня таблицы `BACKEND_PARSING_ERROR: <класс исключения>: <сообщение>`, остальные таблицы файла разбираются;
|
||||
хотя бы один документ есть — конверт `needs_check` (статус не бывает `done`, пока есть предупреждение уровня таблицы),
|
||||
ни одной таблицы не разобрано — `error` с кодом `RECOGNITION_FAILED` («ни одной таблицы не разобрано», причины — в
|
||||
`metrics.warnings`). Ошибочный файл набора ответа при успешном соседе (например, `<job_id>_ofr.json` с
|
||||
`INVALID_DOCUMENT`) документы успешных файлов не отменяет: ошибка попадает в `metrics.warnings` строкой
|
||||
`<имя json>: <код ИС>: <сообщение>`, такой файл **понижает статус до `needs_check`** (`done` невозможен, решение №28 спеки 3a: потерянный документ, например ОФР,
|
||||
не должен остаться незамеченным).
|
||||
10. **Редакции форм** (`api/rules/statement_lines.yaml`): `66n` (Приказ Минфина № 66н, отчётность по 2024 год включительно)
|
||||
и `fsbu4` (ФСБУ 4/2023, с 2025 года; **пока пуста** — состав кодов даёт владелец, OQ-150). Редакция документа выбирается
|
||||
**по дате отчёта** шапки, если её нет — по **самой поздней `period_date`** колонок значений, если нет и её — по
|
||||
**числу совпадений кодов** строк со словарями редакций (с предупреждением «редакция определена по кодам»); если и кодов
|
||||
не хватает — редакция **не определена**: имена строк из OCR, предупреждение «редакция не определена», `needs_check`.
|
||||
Имя строки — из словаря выбранной редакции по коду, из OCR — только если кода в нём нет. **Пустая** выбранная
|
||||
редакция → имена строк из OCR, одно предупреждение «редакция `<id>` не заполнена», `needs_check`. Предупреждение «код N из редакции X»
|
||||
(код есть в другой редакции, но не в выбранной) выдаётся только для **непустой** выбранной редакции — иначе каждая
|
||||
строка формы 2025 года давала бы шум (решение №23 спеки 3a). Редакция формы в конверт не передаётся (OQ-145).
|
||||
### §11.1 Источник OCR: ИС Сканирование (адаптер шага 1 в scan-gateway)
|
||||
|
||||
**Формат ответа** — по реальным файлам `fixtures/ocr/*.json` (проверено 04.10.2026; OQ-13, OQ-14, OQ-77):
|
||||
|
||||
- Каталог ответа `<job_id>/` содержит **несколько json** — по одному на распознанный комплект отчётности. Шлюз читает
|
||||
все json каталога и склеивает их в `raw/{job_id}.json` (`{job_id, files[{name, response, parse_error}]}`) **до**
|
||||
нормализации; ошибка одного json не отменяет остальные. Допущение владельца (17.10.2026): несколько периодов в одном
|
||||
файле ИС отдаёт разными json; образца нет — проверить на первом таком файле (OQ-167).
|
||||
- Корень: `{file_uid, error {code, message}, data_ocr}`. **Успех — `error.code == ""`**; ошибка — непустой `code`
|
||||
(`INVALID_DOCUMENT`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `RECOGNITION_FAILED`, `UNKNOWN_ERROR`).
|
||||
- `data_ocr` — объект: поля шапки `label`, `organization`, `inn`, `units`, `date` — каждое `{form_name, confidence}` (сырой
|
||||
OCR-текст; `date.form_name` ненадёжна — у aep_6 и «БО 1 кв 2025» там название организации), **одна шапка на json**;
|
||||
`table_structure[]` — таблицы `{label {form_name, confidence}, rows[{row_index, cells[{column_index, value,
|
||||
confidence}]}]}`; `column_index` не обязательно подряд; `confidence` — проценты 0–100 (NaN → 0). В одном json
|
||||
встречаются ББ и ОФР одного периода под общей шапкой. ИС отдаёт ячейку на каждую позицию строки: пустая — пустой
|
||||
текст с уверенностью 100, непрочитанная — пустой текст с уверенностью 0.
|
||||
|
||||
**Адаптер.** Каждая таблица успешного json — кандидат (`FormCandidate`): `source = ocr`; `locator = file:<имя
|
||||
json>;table:<i>` (как в golden-эталонах); шапка json копируется в каждую свою таблицу (`Cell.text = form_name`,
|
||||
`confidence` ИС); `label.form_name` таблицы — заголовок в `evidence` (тип — правилом §11.2 «тип-подсказка» по словарю
|
||||
заголовков; прежний `statement_labels.yaml` уходит в словарь пакета); сетка строится по `row_index` и `column_index`
|
||||
плотно, пустая позиция — `null`; `Cell.ref = row:<row_index>`, `value = null`.
|
||||
|
||||
**Ошибки набора.** Ошибочный json (`parse_error`, непустой `error.code`, корень не объект) при хотя бы одном успешном
|
||||
соседе — предупреждение файла `JSON_FAILED` (`error`, `locator = file:<имя json>`, в тексте код и сообщение ИС; решение
|
||||
№ 28 среза 3a: потерянный документ, например ОФР, не должен остаться незамеченным); все json ошибочны — конверт `error`
|
||||
с кодом первого. Исключение при разборе таблицы — `TABLE_PARSE_ERROR` (`error`) документа, остальные таблицы
|
||||
разбираются; ни одной таблицы не разобрано — `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/`); решение владельца
|
||||
06.10.2026: проверен на стенде ИС Сканирование, перенос разрешён — **единственное исключение** из правила «доменный код из
|
||||
`reference-old-arm` не переносить». Перенесённые функции теперь — общие разборы пакета (§11.3–§11.6), отличия от
|
||||
источника перечислены там.
|
||||
|
||||
### §11.2 Источник Excel: tabular-parser (адаптер шага 1)
|
||||
|
||||
**Остаётся в сервисе:** лимиты и чтение книги (`PARSE_MAX_BYTES` 50 МБ, `PARSE_TIMEOUT_SECONDS` 120 с, формат по
|
||||
сигнатуре байтов — OLE2 → XLS, zip с `xl/workbook.bin` → XLSB, иной zip → XLSX, иначе `UNSUPPORTED_FORMAT`; несовпадение
|
||||
с `file_type` — `FILE_TYPE_MISMATCH` `info`; лимиты распакованного zip и площади объединённых ячеек; библиотека упала —
|
||||
`UNREADABLE`), классификация листов и путь ОСВ (§1).
|
||||
|
||||
1. **Классификация листов**: лист ОСВ (признак ведомости в первых 30 строках, §1) → форма → продолжение → прочее. Форма —
|
||||
заголовок («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД в любой строке до
|
||||
таблицы; титул «Бухгалтерская отчетность» — не тип. Продолжение — лист без заголовка, но с маркером «Форма … с.2», или
|
||||
с подписью «Код» и периодами правее, или с колонкой кодов, или с одним-двумя кодами сразу после листа формы. Прочее —
|
||||
`SHEET_OTHER` (`info`), если у листа нет ни типа, ни кодов.
|
||||
2. **Блоки форм на листе**: строки с заголовком или ОКУД — кандидаты; без кодов между ними блоки сливаются, заголовок
|
||||
после кодов — новая форма. Шапка **своя у каждого блока** в его границах: организация (после метки «Организация»,
|
||||
иначе текст с признаком ОПФ строкой выше метки), ИНН 10/12 цифр у метки «ИНН» (ИНН аудитора не берётся: строки после
|
||||
метки «аудитор» исключены), ОКПО, ОКОПФ, ОКФС, ОКЕИ, адрес, единицы, дата (подзаголовок формы или тройка «число | месяц
|
||||
| год» у метки «Дата»; для ОФР — конец периода). Шапка другого листа не используется. Найденное поле — `Cell` с
|
||||
`confidence = 100`, не найденное — `null`.
|
||||
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`.
|
||||
4. **Склейка листов-продолжений — до раскладки.** Строки продолжения дописываются в конец сетки открытой формы по
|
||||
маркеру «Форма … с.2» или по позиции листа; граница листа — в `segment_breaks` (всегда граница сегмента раскладки).
|
||||
Тип маркера сверяется с типом формы: расхождение — отдельный кандидат и `CONTINUATION_TYPE_CONFLICT` (`error`). Лист без
|
||||
маркера и шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и более кодах, число колонок
|
||||
сумм совпадает с формой (проверки прежнего `join_blocker`); иначе — отдельный кандидат и `SHEET_NOT_JOINED` (`error`).
|
||||
`locator = sheet:<A>[,<B>…]`. **Страницы в разных файлах не склеиваются** (1С печатает форму постранично): страница 2
|
||||
без первой — документ без шапки, `needs_check` (OQ-140). Две половины формы одного типа и периода в одном файле
|
||||
склеиваются только листом-продолжением; иначе это две формы (Р-15, вопрос — OQ-165).
|
||||
5. **Пятизначные подстроки** (11101, 12301…) — обычные строки. **Шаблоны банка** (коды есть, сумм и шапки нет) —
|
||||
`needs_check` (OQ-142).
|
||||
|
||||
**Тип-подсказка (`type_hint`) — одно правило у обоих источников:** ОКУД по словарю (0710001 → `BALANCE`, 0710002 →
|
||||
`INCOME`, прочие 0710xxx — 0710003 капитал, 0710004 ДДС и др. → `DISCLOSURE`) → иначе заголовок по словарю заголовков
|
||||
(у OCR — `label` таблицы ИС; «Пояснения», «Расшифровка» → `DISCLOSURE`) → иначе `null`. Окончательный тип — по кодам строк
|
||||
(§11.7).
|
||||
|
||||
**Шапка — одно правило разбора** по `Cell.text`: ИНН — только цифры, **без замены двойников** (иначе «3|1 21 2|» из aep_3
|
||||
даёт лишние единицы и `MISMATCH` с ЮЛ), длина 10 или 12, иначе `null`; дата отчёта — словарь периодов §11.3 п. 3;
|
||||
единицы — словарь единиц. Сырой текст и уверенность каждого поля — в контракт (`<поле>_text`, `<поле>_confidence`).
|
||||
|
||||
### §11.3 Шаг 2 — раскладка таблицы
|
||||
|
||||
**Очистка OCR-текста** (общий разбор, применяется к любой ячейке без `value`; у Excel — к текстовым ячейкам, почти
|
||||
всегда без изменений). Буквы-двойники рядом с цифрами `О/о/O/o → 0`, `З/з → 3`, `б → 6`, `S/s → 5`, `I/l/|/і → 1`,
|
||||
`Ч → 4` заменяются **только вплотную к цифре**, в несколько проходов («2ОО5» → «2005»), слова не портятся; если первая
|
||||
строка значения состоит только из цифр, пробелов, `|`, точек и двоеточий — `|` считается разделителем клеток («2|0|2|4|» →
|
||||
«2 0 2 4»). **Применяется к датам, подписям колонок, кодам и суммам** (Р-11; отменяет прежнее «только к датам» и решение
|
||||
№ 22 среза 3a): сумма с заменой — `is_recognized = false`, код с заменой — неуверенный (уверенность не выше 99); в суммах
|
||||
крайние `|` (след границы таблицы: «| : 122477») удаляются, а не заменяются. ИНН двойники не трогают. Текст длиннее 200
|
||||
символов датой не разбирается (решение № 31 среза 3a: backtracking регулярок).
|
||||
|
||||
**Диапазоны кодов** (словарь пакета): ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала 3100–3699, отчёт о
|
||||
движении денежных средств 4100–4499, пояснения 5000–5999, отчёт о целевом использовании средств 6100–6499. «Кодовые
|
||||
диапазоны» для поиска столбца — объединение отрезков (годы 2000–2099 и четырёхзначные суммы вне отрезков кодами не
|
||||
считаются). «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца; им проверяются
|
||||
разлепление строк, слабые продолжения и наследование столбца в сегментах.
|
||||
|
||||
**Разбор кода:** убрать пробелы и переносы → двойники (`|` → 1, только если результат в кодовом диапазоне: «| 140» →
|
||||
1140) → краевая пунктуация («1220 ,» → 1220) → 4–5 цифр (допускается `.0`: `1110.0` из Excel), первые четыре — в кодовом
|
||||
диапазоне. Многострочная ячейка — код, только если каждая её часть код.
|
||||
|
||||
1. **Столбец кодов.** (а) Столбец с подписью из словаря («код», «коды», «код строки», «код показателя» — после
|
||||
нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью доля кодов среди непустых ≥ 0,6 или
|
||||
кодов ≥ 3; несколько кандидатов — с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом.
|
||||
Защита обязательна: в выгрузках 1С под «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267, 12300 — «коды
|
||||
баланса» по первым цифрам), ОКЕИ — один код из девяти, доля 0,11. Документ из двух строк остаётся документом (два из
|
||||
двух, доля 1,0). (б) Подписи нет — столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6. Нет —
|
||||
`CODE_COLUMN_NOT_FOUND`; сопоставление по наименованиям строк не делается (OQ-139).
|
||||
2. **Сегменты.** Новый сегмент открывает строка-шапка (в ней есть подпись кода из словаря, или не менее двух её непустых
|
||||
ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из `segment_breaks`. У
|
||||
сегмента своя раскладка: по его шапке; без шапки — столбец кодов по п. 1, а если сегмент не первый и кодов меньше трёх —
|
||||
ослабленный поиск: столбец с наибольшим числом кодов из диапазона столбца первого сегмента, минимум один (лист «_2» НПЗ
|
||||
с единственной строкой 2400, у которого код в другой колонке). Колонки значений сегмента без шапки — столбцы правее
|
||||
кода, где в строках с кодами числа, сопоставление с первым сегментом по порядку; число не совпало —
|
||||
`SEGMENT_COLUMNS_MISMATCH` (`error`), значения по позиции. Столбец кодов не найден ни так, ни так —
|
||||
`SEGMENT_WITHOUT_CODES` (`error`); строки сегмента не теряются (роль `data`, статусы по §11.4).
|
||||
3. **Дата колонки — один словарь периодов** `statement_periods.yaml`, объединяющий прежний словарь парсера и шаблоны
|
||||
`dates.py` шлюза (порядок приоритета текстовых форм: диапазон «с 01.01.2025 по 30.09.2025» — конец; «30 сентября 2025
|
||||
г./r./года»; «I квартал 2026» / «1-й кв.» / «первый квартал»; «1 полугодие» / «1 п/г»; «9 месяцев 2025»; «январь –
|
||||
сентябрь 2025» — последний месяц; «за 2025 г.» / «2025 год» — 31.12; затем числовые: три группы цифр «30.09.2025»,
|
||||
«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`:
|
||||
- абсолютные подписи → `absolute`;
|
||||
- относительные подписи бланка 66н без впечатанных дат (Р-6): «На отчётную дату», «За отчётный период» →
|
||||
`relative:current` (= эффективная дата); «На 31 декабря предыдущего года» → `relative:prev_year_end` (31.12 года
|
||||
эффективной даты − 1); «На 31 декабря года, предшествующего предыдущему» → `relative:prev_prev_year_end` (31.12 года
|
||||
− 2); «За аналогичный период предыдущего года» → `relative:prev_period` (эффективная дата − 1 год; 29.02 → 28.02);
|
||||
- подписи нет → `none`.
|
||||
|
||||
**Эффективная дата документа** — дата отчёта из шапки, иначе самая поздняя абсолютная дата колонок (та же формула,
|
||||
что период формы в ядре, §9.5 п. 2); нет ни той, ни другой → у относительных колонок `period_end = null`, вид
|
||||
остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в
|
||||
дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря
|
||||
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка).
|
||||
4. **Строка заголовка** — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя
|
||||
над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный
|
||||
заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки
|
||||
заголовка (титульный блок «Дата 30 | 9 | 2025»); в сегменте без заголовка роли `title` нет, строки выше первого кода с
|
||||
содержимым идут в данные. Заголовка нет ни у одного сегмента → колонки значений первого сегмента — столбцы правее кода,
|
||||
где в строках с кодами числа, `label` пустая, `period_end = 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).
|
||||
6. **Подпись без периода** (Р-6): период по словарю (абсолютный или относительный) → колонка с датой; подпись из
|
||||
словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск, `COLUMN_SKIPPED` (`info`);
|
||||
иначе, если в столбце есть числа в строках с кодами (OCR-мусор «| | г» с уверенностью 17, обрезанная или самодельная
|
||||
подпись), — колонка остаётся с подписью как есть, `period_end = null`, `period_kind = none`, `COLUMN_WITHOUT_DATE`
|
||||
(`error`, колонка); аналитик ставит дату через PATCH (§9.5 п. 9); иначе пропуск. Колонка без даты не мешает периоду
|
||||
формы (эффективная дата), но межформенная проверка и гудвилл «на отчётную дату» по ней не считаются, пока дата не
|
||||
проставлена. Уверенность подписи — `label_confidence` (Excel — 100, подписи нет — `null`).
|
||||
7. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с
|
||||
наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца,
|
||||
пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||||
|
||||
### §11.4 Шаг 3 — отбор строк данных и статусы
|
||||
|
||||
Строка данных — строка сетки с ролью `data`, у которой есть содержимое: код, число или непустой текст в колонке значений
|
||||
с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3). У строки: `row_index` (номер в документе с
|
||||
0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||||
ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:**
|
||||
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «изо :», «[190 :»); ячейка
|
||||
без единой цифры («}», «:») считается пустой;
|
||||
- `detail` (Р-8) — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах — родитель
|
||||
продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке», «Товар
|
||||
(41.01)»); родитель — ближайшая строка с кодом выше. Сервисы её отдают, ядро хранит; в проверки §7, агрегаты и `done` она
|
||||
не входит; экран по умолчанию не показывает;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом («Выручка» aep_3, где 2110
|
||||
уехал в ячейку значения); строки роли `title` сюда не попадают.
|
||||
|
||||
Строка без кода и без содержимого (заголовок раздела «АКТИВ», «в том числе:» без сумм) — не строка. **Счётчиков
|
||||
отброшенных строк нет — есть статусы** (отменяет «строк с нечисловым кодом пропущено», «строк без кода с суммами
|
||||
пропущено», OQ-36 / OQ-141; OQ-149 — восстановление строк по наименованию — остаётся открытым). Строка формы с пустой
|
||||
ячейкой кода под другой строкой с кодом попадёт в `detail` и будет скрыта; её значения не войдут в сумму раздела,
|
||||
нарушение §7 подсветит родителя (пустая цель при непустых членах — нарушение), аналитик раскроет расшифровки и назначит
|
||||
код (Р-13) — осознанная цена Р-8.
|
||||
|
||||
**Повтор кода** (Р-4) — законный случай (один код, разные наименования; договорённость ДБД в AURORA-1431, закрывает
|
||||
AURORA-1406): все строки `ok`, порядок как в файле, ни предупреждения, ни `duplicate_line_codes` (отменяет «оставлена
|
||||
первая строка», решение № 15 среза 3a и OQ-152). Для §7 и всего после — сумма по коду (агрегат формы по коду, §9.5 п. 2).
|
||||
Неверно прочитанный код, совпавший с существующим, ловит структурная проверка (§11.9).
|
||||
|
||||
**Разлепление склеенных строк** (OQ-156; Р-17 заменяет решение владельца 08.10.2026 о безусловном `needs_check`) —
|
||||
**до** разбора кода: ячейка кода содержит N ≥ 2 частей через перевод строки, каждая непустая ячейка значений — ровно N
|
||||
частей, и хотя бы одна часть кода разобралась как код диапазона столбца («44 30\nI |» aep_6 не множится: 4430 не в
|
||||
диапазоне ББ). Получается N строк (`ref` `row:<i>#<k>`): k-й код (нечитаемая часть — строка `unreadable`), k-я часть
|
||||
каждой ячейки значений, наименование — k-я из непустых частей, если их тоже N, иначе целиком; `line_code_text` и `text` —
|
||||
k-е части; уверенность — исходной ячейки. Значения разлепленных строк — `is_recognized = false`, нуждаются в покрытии
|
||||
§7 (при сошедшихся соотношениях не подсвечиваются); само разлепление — `ROW_SPLIT` (`info`). Число частей не совпало или
|
||||
ни одна часть не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст,
|
||||
`is_recognized = false`. Пример: ББ РБ и Счастливочной за 1 кв. 2025 — код `1500\n1700`, суммы `917 775\n2 371 333`.
|
||||
У Excel не срабатывает.
|
||||
|
||||
### §11.5 Шаг 4 — значения ячеек
|
||||
|
||||
Выход: `{column_index, amount, string_value, text, confidence, is_recognized}` (+ `needs_review`, §11.9). `text` — всегда
|
||||
сырой текст источника; `string_value` — только когда `amount = null` и текст непуст и не прочерк.
|
||||
|
||||
1. **`value` есть (Excel):** число → `amount` с квантованием до 2 знаков half-up, не больше 16 целых цифр (иначе `amount
|
||||
= null`, `string_value` = текст, `is_recognized = false`, `VALUE_OUT_OF_RANGE` `info`); дата или логическое → `amount
|
||||
= null`, `string_value` = текст, `is_recognized = false`.
|
||||
2. **`value` нет — `parse_amount(text, source)`**, один для обоих: двойники (§11.3) и крайние `|`; пробелы, NBSP и тонкие
|
||||
пробелы; минус «−» (U+2212) как знак; скобки → минус, `}` и `]` принимаются как закрывающая скобка (OQ-147:
|
||||
«(7744536}» → −7744536, `is_recognized = false`); прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
|
||||
`is_recognized = true` (OQ-148). Разделители:
|
||||
- `ocr`: запятая — тысячи, если весь текст (после `strip` и удаления пробелов, с учётом знака и пары скобок на всю
|
||||
строку) — `[+-]?[1-9]\d{0,2}(,\d{3})+` (AURORA-1405, sg#35: `4,200` → 4200, `1,234,567` → 1234567); точка — тысячи
|
||||
при `[+-]?[1-9]\d*(\.\d{3})+` (sg#37: `837.994` → 837994, `1.234.567` → 1234567, `1 234.567` → 1234567); удаление
|
||||
таких разделителей исправлением не считается. Иначе запятая и точка — десятичный разделитель (`4,2` → 4.2, `1 234,56`
|
||||
→ 1234.56); смешанные записи (`1.234,56`, `1,234.56`) — цифры подряд, `is_recognized = false`. **Три и более знаков
|
||||
после десятичного разделителя** (`0,125`, `12.3456`) — `amount = null`, `string_value` = текст (решение № 30 среза
|
||||
3a сохраняется для OCR-текста: такое значение не укладывается в `numeric(18,2)`);
|
||||
- `excel`: запятая и точка — десятичный разделитель («4,200» = 4,20, «1.500» = 1,5; как прежний `to_money`).
|
||||
3. **Пробел внутри числа** допустим только как разделитель тысяч (группы ровно по три цифры); иначе `amount` — «лучшая
|
||||
догадка» (цифры подряд, не длиннее 16 цифр, иначе `null`), `is_recognized = false`, сырой текст — в `text` («42831 1» →
|
||||
428311; AURORA-1511, 1519). Многострочное значение в строке с однострочным кодом → `amount = null`, `string_value` =
|
||||
текст, `is_recognized = false`. Excel-ячейки «число текстом» идут тем же путём.
|
||||
4. **`is_recognized`** — один смысл у обоих: текст разобран как число без выброшенных и исправленных символов (замена
|
||||
двойника, `}` / `]` вместо скобки — исправление). `confidence` — отдельно, от источника (у Excel — 100); прежнее «`is_recognized`
|
||||
= `confidence == 100` и ничего не выброшено» шлюза отменено.
|
||||
5. **Пустая ячейка** (Р-2): позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`,
|
||||
`is_recognized = true`, `confidence` ячейки (позиции нет — 100); собственной подсветки нет. Непрочитанная ячейка ИС
|
||||
(пустой текст, уверенность 0) нуждается в покрытии по §11.9.
|
||||
6. **Единицы:** «тыс. руб.» и ≥ 10 % дробных значений → `FRACTIONAL_IN_THOUSANDS` (`info`, документ остаётся `done`;
|
||||
OQ-138 — единицам шапки верим); то же в книге xlsb без числовых форматов — `XLSB_SCALE_SUSPECT` (`error`: вероятен
|
||||
масштаб ×1000, а §7 сходится при любом масштабе).
|
||||
|
||||
Разбор сумм ОСВ (`to_money` в `classify.py`, §1) этими правилами не затрагивается.
|
||||
|
||||
### §11.6 Шаг 5 — код, наименование, редакция
|
||||
|
||||
Строка: `{row_index, ref, line_status, line_code, line_code_text, line_code_confidence, line_name, line_name_confidence,
|
||||
parent_ref, needs_review, values}`. `line_code` — разобранный код или `null` у `unreadable` / `missing` / `detail`;
|
||||
`line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от источника (у кода с заменой двойника — не выше 99),
|
||||
**без срезания до 99 по справочнику**; `line_name` — распознанный текст наименования как есть, с уверенностью источника
|
||||
(Р-3: имя из справочника шлюза `api/rules/statement_lines.yaml` отменено, файл удаляется; соответствие «код →
|
||||
наименование» — только в ядре, §7 «Полный состав формы»). **Редакция** (`66n` / `fsbu4`) в сервисах нужна только для
|
||||
упорядоченного перечня кодов (§11.9) и выбирается по эффективной дате (`fsbu4` — после 2024-12-31, как `edition` ядра,
|
||||
OQ-145); без даты проверяются обе редакции. Прежний выбор редакции шлюза для имён (по дате, по кодам, «редакция не
|
||||
определена» / «не заполнена» → `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`.
|
||||
2. **Вариант формы** `form_variant` — только по структуре кодов (ОКУД 0710096 — код КНД комплекта, на бланках
|
||||
0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`; ОФР — есть 2400 или
|
||||
2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` / `OTHER` — `null`. Шаг 7 и перечень
|
||||
кодов берут набор варианта. Образец упрощённой формы — в fixtures при первой возможности (OQ-166).
|
||||
3. **`locator`** — пары `ключ:значение` через `;`: `file:<имя json>;table:<i>` / `sheet:<A>[,<B>]`. Шапка документа —
|
||||
поля кандидата (`text` → `<поле>_text`, разобранное → поле, `confidence` → `<поле>_confidence`).
|
||||
|
||||
### §11.8 Шаг 7 — самопроверки §7
|
||||
|
||||
Правила счёта — §7 («Самопроверка в сервисах»): один словарь у обоих сервисов, значение кода — сумма строк `ok`,
|
||||
«посчитанное» соотношение, допуск и округление, `abs_terms` подтверждают величину, но не знак, полный или упрощённый
|
||||
набор по `form_variant`, родитель подстроки. `checks` есть у каждого `BALANCE` / `INCOME` со строками и отсутствует у
|
||||
документа без строк. Проверки по словарю имён и перечню кодов («код не в словаре», «код не из редакции») — в ядре,
|
||||
`info` к строке.
|
||||
|
||||
### §11.9 Шаг 8 — `needs_review`, статус, предупреждения
|
||||
|
||||
**Подтверждение проверкой** (Р-5, Р-7, Р-11, Р-17). Проверка §7 — арбитр, уверенность — подсказка там, где проверки нет.
|
||||
|
||||
- Значение **нуждается в покрытии**, если `confidence < 100` (Р-5: у ИС уверенность дискретная — 100, ~80, 71, 67, 60,
|
||||
50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false`
|
||||
(грязный текст, замена двойников, разлепленная строка). Чистые значения в покрытии не нуждаются.
|
||||
- Значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось (в
|
||||
пределах допуска или округления), и `amount` значения не `null`.
|
||||
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма записи знака (скобки, минус, без знака —
|
||||
по `text`) совпадает с формой у большинства членов `abs_terms` той же колонки с непустым `amount`, `is_recognized =
|
||||
true` и без правки аналитика; не совпадает или сравнивать не с чем — не подтверждён. «(7744536}» подтверждается, если
|
||||
2210 и 2220 той же колонки в скобках; «{38378)» при остальных в скобках — подсвечивается.
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой
|
||||
текст и уверенность остаются в подсказке ячейки.
|
||||
|
||||
Не покрыто проверками и остаётся на уверенности: справочные строки ОФР 25xx / 29xx, детализация без проверки родителя,
|
||||
`DISCLOSURE`, колонки только с мягкими соотношениями. Риск двух взаимно компенсирующих ошибок в одном соотношении
|
||||
принимается. Закрывает AURORA-1524 без ущерба для 1519.
|
||||
|
||||
**Код строки** (Р-5, Р-12, Р-16). Код неуверенный — `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`) у строки. По
|
||||
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя.
|
||||
|
||||
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок;
|
||||
иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал
|
||||
бы неверный период и автоподтверждение. ИНН и организация с уверенностью ниже 100 — `info` (их сверяет ядро: ИНН —
|
||||
`MISMATCH` с ЮЛ, организация — по алиасам), в `needs_review_fields` не попадают; единицы и прочие поля на статус не
|
||||
влияют.
|
||||
|
||||
**Предупреждение** — структура `{code, severity, message, line_refs?, column_index?, field?}`; уровень файла
|
||||
(`metrics.warnings`) — ещё `locator?`. Место — `locator` документа, не текст. Принцип: `error` — только то, что меняет
|
||||
данные или их полноту; `error` → `needs_check`, `info` на статус не влияет.
|
||||
|
||||
| Код | Уровень | Когда | Чем снимается в ядре |
|
||||
|---|---|---|---|
|
||||
| `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка |
|
||||
| `TABLE_PARSE_ERROR` | error | исключение при разборе таблицы ИС | перезагрузка |
|
||||
| `TABLE_LOST` | error, файл | таблица ИС с содержимым без столбца кодов и без типа | перезагрузка |
|
||||
| `CODE_COLUMN_NOT_FOUND` | error | форма `BALANCE` / `INCOME` без столбца кодов | перезагрузка |
|
||||
| `SEGMENT_WITHOUT_CODES` | error | сегмент с содержимым без столбца кодов (в т. ч. лист-продолжение) | перезагрузка |
|
||||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился | перезагрузка |
|
||||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме | перезагрузка |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым | перезагрузка |
|
||||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи | PATCH `columns[].period_date` |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты | PATCH `columns[].period_date` |
|
||||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента | перезагрузка |
|
||||
| `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` |
|
||||
| `XLSB_SCALE_SUSPECT` | error | xlsb без форматов, «тыс. руб.», ≥ 10 % дробных | перезагрузка |
|
||||
| `INN_NOT_FOUND`, `INN_UNCERTAIN` | info | ИНН не найден / неуверен при найденной организации | — |
|
||||
| `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN` | info | организация не найдена / неуверенна при найденном ИНН | — |
|
||||
| `REPORT_DATE_UNPARSED` | info | дата отчёта не разобрана при наличии дат колонок | PATCH `header.report_date` |
|
||||
| `UNITS_NOT_FOUND` | info | единицы не найдены | — |
|
||||
| `FILE_TYPE_MISMATCH` | info, файл | формат файла не совпал с `file_type` | — |
|
||||
| `COLUMN_SKIPPED` | info | подпись из словаря исключений пропущена | — |
|
||||
| `FRACTIONAL_IN_THOUSANDS` | info | дробные при «тыс. руб.» (кроме xlsb без форматов) | — |
|
||||
| `XLSB_NO_FORMATS`, `NUMBER_FORMAT_UNPARSED` | info | числовые форматы недоступны или не разобраны | — |
|
||||
| `VALUE_OUT_OF_RANGE` | info | число Excel длиннее 16 целых цифр | — |
|
||||
| `SHEET_OTHER` | info | лист Excel «прочее» без типа и кодов | — |
|
||||
| `ROW_SPLIT` | info | разлеплена склеенная строка | — |
|
||||
| `OSV_*` | info | предупреждения листа ОСВ (§1) | — |
|
||||
| `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH` | info, ядро | проверки ядра (§9.3, §9.5) | пересчёт |
|
||||
|
||||
Назначение кода и отметка `detail` снимают нарушения, пересчитываемые §7. Новый код предупреждения вводится словарём
|
||||
пакета и строкой этой таблицы; в контракте `code` — строка, не перечисление.
|
||||
|
||||
**Статус документа** — одна функция общего кода: `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`.
|
||||
|
||||
**Статус конверта:** `done` ⇔ все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`;
|
||||
документов нет — `error` (`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser — коды описывают
|
||||
разные причины). Порядок документов tabular-parser — сначала ОСВ в порядке листов, затем формы. Статус конверта в
|
||||
условие `RECOGNIZED` ядра не входит (Р-19).
|
||||
|
||||
### §11.10 Что отменено и заменено этим параграфом
|
||||
|
||||
| Прежнее правило | Теперь |
|
||||
|---|---|
|
||||
| Строки без кода не отдаются и не хранятся (OQ-36, OQ-141; §12 п. 5) | статусы `detail` / `missing` / `unreadable`, хранятся (Р-8); OQ-36 и OQ-141 закрыты |
|
||||
| Двойники — только в датах (§11.1 п. 1, решение № 22 среза 3a) | двойники в датах, подписях, кодах и суммах (Р-11); ИНН — без двойников |
|
||||
| Три и более знаков после разделителя → `amount = null` (решение № 30 среза 3a) | сохраняется для OCR-текста; для числа Excel — квантование до 2 знаков |
|
||||
| Разлепленная строка → документ `needs_check` (решение владельца 08.10.2026 по OQ-156) | значения нуждаются в покрытии §7, `ROW_SPLIT` — `info` (Р-17) |
|
||||
| Повтор кода: остаётся первая строка (решение № 15 среза 3a, §12 п. 5, OQ-152) | все строки `ok`, сумма по коду (Р-4) |
|
||||
| Наименование — из справочника шлюза, уверенность кода ≤ 99 вне справочника (§11.1 п. 6, п. 10) | наименование из источника, уверенность от источника (Р-3) |
|
||||
| Без шапки — синтез шапки и `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` OCR = уверенность 100 ∧ ничего не выброшено; любое предупреждение → `needs_check` | `is_recognized` — только разбор; статус — §11.9 |
|
||||
| `checks` ядро игнорирует и пересчитывает §7 при загрузке | хранит как пришли, пересчитывает только после правок |
|
||||
|
||||
### §11.11 Отложено
|
||||
|
||||
Восстановление строк по наименованию (OQ-149); сопоставление формы без столбца кодов по именам строк (OQ-139); склейка
|
||||
страниц из разных файлов (OQ-140) и половин формы из одного файла (OQ-165, Р-15); приведение единиц сверх числового
|
||||
формата (OQ-138); формулы §7 по редакциям и знаки 2410/2420 (OQ-12, OQ-146); образец упрощённой формы (OQ-166);
|
||||
несколько периодов в одном файле ИС (OQ-167, допущение владельца); переключатель «как в файле» на экране (Р-14).
|
||||
|
||||
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_* + document_issue` для PDF и Excel;
|
||||
экран «Проверка распознавания» показывает статусы строк и предупреждения у места.
|
||||
|
||||
---
|
||||
|
||||
## §12. ББ и ОФР из Excel (tabular-parser, второй тип документа)
|
||||
## §12. ББ и ОФР из Excel — см. §11
|
||||
|
||||
Источник: ТЗ 6.2 («парсинг ОСВ, бухгалтерского баланса и ОФР (эксель файлов)»), DOC-037734, DOC-221281; формат —
|
||||
Приказ 66н / ФСБУ 4/2023, но на практике это и выгрузка из 1С («Регламентированный отчёт»), и шаблон банка
|
||||
(`Копия Рекомендуемые формы расшифровок…`, лист `Отчетность` в `Анализ *.xlsx`), и вручную набитая таблица.
|
||||
|
||||
Принцип тот же, что в §1.0: **структура ищется по содержимому**, результат — тот же `statementDocument` контракта,
|
||||
что и у `scan-gateway`, чтобы ядро маппило ББ/ОФР одинаково независимо от источника (`confidence = 100`,
|
||||
`is_recognized = true` для табличного пути).
|
||||
|
||||
**Статус: реализовано (Срез 2b, tabular-parser feat/14, контракт v1.2; решения владельца 06.10.2026).** Модули —
|
||||
`api/src/parser/statements/`, словари — `api/rules/statement_headers.yaml`, `statement_periods.yaml`,
|
||||
`statement_checks.yaml`; ожидания по реальным файлам — `api/tests/statements_manifest.yaml` (06-test-data §4б).
|
||||
|
||||
1. **Классификация листов**: лист ОСВ → форма → продолжение → прочее. Форма: заголовок («Бухгалтерский баланс»,
|
||||
«Отчет о финансовых результатах», «Отчет о прибылях и убытках») или ОКУД в **любой** строке до таблицы (у НПЗ он стоит
|
||||
под титулом); титул «Бухгалтерская отчетность» — не тип, тип берёт ближайший следующий заголовок. Тип по ОКУД:
|
||||
0710001 → `BALANCE`, 0710002 → `INCOME`, прочие 0710xxx (0710003 капитал, 0710004 ДДС) и пояснения («Пояснения к
|
||||
бухгалтерскому балансу», «Расшифровка») → `DISCLOSURE`; при противоречии ОКУД и заголовка — тип по ОКУД и
|
||||
предупреждение. Две формы на одном листе — по второму заголовку, в книге несколько листов — несколько `document`.
|
||||
Продолжение: лист без заголовка формы, но с маркером «Форма … с.2», либо с подписью «Код» и периодами правее, либо с
|
||||
колонкой кодов. ОСВ и формы в одной книге допустимы.
|
||||
2. **Колонка кодов**: в блоке ≥ 60 % непустых значений — коды (ББ 1100–1799, ОФР 2100–2999, расшифровки 5xxx,
|
||||
пятизначные — по первым четырём цифрам), кодов не меньше `min_codes` = 3; заголовок «Код/Коды» необязателен, числа вида
|
||||
`1110.0` читаются как текст «1110». Колонки нет — документ без строк, `needs_check`, предупреждение «колонка кодов не
|
||||
найдена»; сопоставление по наименованиям строк **не делается** (решение владельца, OQ-139).
|
||||
3. **Колонка наименований** — ближайшая левее кодов с преобладанием текста; «Пояснения» (пустые или ≤ 3 символов) не
|
||||
наименование.
|
||||
4. **Колонки-периоды**: ячейки заголовка правее кодов с распознанным периодом («На 31 декабря 2024 г.», «31.12.2024»,
|
||||
«За 9 месяцев 2025 г.», «За январь - сентябрь 2025 г.», «2024»), по порядку слева направо (`column_index` 0, 1, 2…);
|
||||
`period_date` — дата или конец периода; `is_current` в контракте нет — ядро берёт самую позднюю. Заголовок в две строки
|
||||
(«На» + «20__ г.», перенос внутри ячейки) склеивается; строка нумерации «1 2 3 4 5» пропускается. Колонки без периода
|
||||
(«Изменение», «%») **не читаются** (одно предупреждение на документ): у `statementDocument` поля `extra` нет.
|
||||
5. **Значения**: `Decimal`, скобки → минус, «–»/пусто → `null`, нечисловой текст → `string_value`,
|
||||
`is_recognized = false`. **Значение из Excel — как его показывает числовой формат ячейки** (OQ-138, пересмотрено
|
||||
владельцем 15.10.2026, AURORA-1528): выгрузки 1С хранят рубли с копейками, а в тысячи их переводит формат. Применяются
|
||||
масштабирующие запятые (`#,##0,` → тысячи, `0,` → тысячи), число знаков после точки (не больше 2 — `numeric(18,2)`),
|
||||
секции и условия (`[=0]"-"`, `[=-646532]"(647)"`); литерал условной секции разбирается как текстовая ячейка (скобки →
|
||||
минус, пробелы/NBSP — разделители, «-»/«–»/«—»/пусто → `null`); округление «половина от нуля» (как в Excel); знак — от
|
||||
сырого значения (для числовых секций; у секций-литералов значение берётся из литерала). Условия `[=x]` и значение
|
||||
сравниваются с точностью 15 значащих цифр, как в Excel. Секция выбирается по порядку: при явных условиях ни одна не
|
||||
подошла — формат не разобран, неявные условия у нефинальных секций — по умолчанию по числу секций. `General`/пустой
|
||||
формат — значение как в ячейке. Формат, который не удалось разобрать (`%`, экспонента, дробь, даты, `@`, неизвестные
|
||||
знаки вне кавычек вроде `₽`, больше четырёх секций, условие не по шаблону вроде `[=1E-5]`, литерал не число/пусто/
|
||||
прочерк, результат вне `numeric(18,2)`), — значение как в ячейке и одно предупреждение на документ с перечнем форматов. xlsb: стили не читаются
|
||||
— значения как в файле и одно предупреждение на документ «формат чисел xlsb не читается: значения как в файле». ОСВ
|
||||
правило не затрагивает. Единицы сверх формата не нормализуются: `header.units` — текст шапки; при «тыс.» и ≥ 10 %
|
||||
отображаемых значений с дробной частью — предупреждение «значений с дробной частью при единице тыс. руб.: N из M»
|
||||
(OQ-138). Строки без кода (разделы, «в том числе:», «Реализация иного имущества…» у НПЗ) не отдаются
|
||||
(`ASSUMPTION(OQ-36)`, OQ-141); повтор кода — остаётся первая строка, код в `checks.duplicate_line_codes`, `needs_check`.
|
||||
Пятизначные подстроки (11101, 12301…) отдаются как обычные строки.
|
||||
6. **Шапка**: организация (после метки «Организация», иначе текст с признаком ОПФ строкой выше метки — НПЗ), ИНН 10/12
|
||||
цифр у метки «ИНН» (**ИНН аудитора не берётся**: строки после метки «аудитор» исключены), ОКПО, ОКОПФ/ОКФС, ОКЕИ, адрес,
|
||||
единицы, `report_date_text`/`report_date` — из подзаголовка формы или тройки «число | месяц | год» у метки «Дата» (для
|
||||
ОФР — конец периода). Шапка другого листа не используется: у ОФР НПЗ нет метки «ИНН» → `inn = null`. Найденное поле —
|
||||
`*_confidence = 100`, не найденное — `null`.
|
||||
7. **Самопроверка**: соотношения §7 по каждой колонке — данные `statement_checks.yaml`. **Жёсткие** (нарушение →
|
||||
`needs_check`): ББ `1100`, `1200` (+1215), `1300`, `1400`, `1500`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`,
|
||||
`1600 = 1700`; ОФР `2100`, `2200`, `2300`. **Мягкие** (только предупреждение, `ASSUMPTION(OQ-12)`): `2400 = 2300 −
|
||||
2410 + 2420` и варианты ФСБУ 4/2023, `2500`, «сумма пятизначных подстрок = родитель» (по колонке — только если хоть одна подстрока в ней непуста; `null` ≠ 0, ноль — значение; AURORA-1527). Допуск ±1 **в единицах формы**,
|
||||
отсутствующий код = 0, соотношение проверяется, только если присутствует его цель; правая часть считается дважды — как
|
||||
записано и с членами `abs_terms` по модулю (1С выгружает расходы со знаком минус, 66н — в скобках). Результат —
|
||||
`documents[].checks` (`relations_failed`, `soft_relations_failed`, `duplicate_line_codes`) и предупреждения.
|
||||
Расхождение жёсткого соотношения больше ±1, но не больше 0,001 % от наибольшего по модулю числа соотношения — **округление итогов** (OQ-138, решение владельца 06.10.2026: единицам шапки верим): уходит в `soft_relations_failed` с предупреждением «сошлось в пределах округления», статус не меняется (`rounding_tolerance_percent` в словаре). Пример: книга НПЗ (`npz/2025-Q3`) — `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 при итогах ~17 млрд,
|
||||
ОФР Унагранде Компани (стр. 1) — итог 2100 округлён до тысяч, ОФР Унагранде Групп 3 кв. 2025 — 2200 = −1 365 000 как
|
||||
малая разность слагаемых ~393 млн; все `done`. Пятизначная подстрока относится к ближайшей четырёхзначной строке выше с
|
||||
теми же тремя первыми цифрами (12310 под 1230); то же правило ядро отдаёт экрану как `parent_code` (§7). ББ Унагранде (стр. 1) — только актив, `1600 = 1700` не сходится,
|
||||
`needs_check`.
|
||||
8. **Расшифровки** (0710003/0710004, пояснения, лист «Примечания» шаблона) — `DISCLOSURE` с `locator` и шапкой, если
|
||||
нашлась, без `columns`/`lines`; на статус не влияет (OQ-23). Книга только из таких листов — `done`.
|
||||
9. **Склейка**: листы-продолжения внутри книги присоединяются к последнему открытому документу формы, если число колонок
|
||||
сумм совпадает; строки добавляются в конец, `locator` перечисляет листы через запятую. **Страницы в разных файлах не
|
||||
склеиваются** (1С печатает форму постранично, один файл = одна страница): страница 2 без первой — документ с пустой шапкой
|
||||
и `needs_check`, предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, OQ-140).
|
||||
Продолжение без колонки кодов при непустом результате не теряется молча — `needs_check`.
|
||||
10. **Статус**: документ `ok` ⇔ жёсткие соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов
|
||||
∧ есть колонка-период ∧ в шапке есть организация или ИНН ∧ документ не одинокое продолжение. Книга — `done`, если `ok`
|
||||
у всех документов, иначе `needs_check`; книга без листов ОСВ и форм — `INVALID_DOCUMENT`. Порядок документов: сначала
|
||||
ОСВ в порядке листов, затем формы. Шаблоны (коды есть, сумм нет, шапки нет) — `needs_check`.
|
||||
11. **Отложено**: запасной путь по именам строк (OQ-139), приведение единиц сверх числового формата ячейки (OQ-138), признак продолжения в контракте и
|
||||
склейка страниц из разных файлов в ядре (OQ-140, Срез 3), строки без кода (OQ-141), образцы ФСБУ (OQ-142), лист
|
||||
продолжения с единственной кодовой строкой (OQ-143 реализовано: после листа формы хватает одного кода,
|
||||
`min_codes_continuation` = 1; первая страница по-прежнему требует `min_codes` = 3).
|
||||
|
||||
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_*` для PDF и Excel (маппинг форм —
|
||||
Срез 3); экран «Проверка распознавания» для Excel-источника показывает не страницы, а таблицу — UI-решение вне парсера.
|
||||
Правила разбора форм из Excel (tabular-parser) с контракта v2.0 — часть общей цепочки нормализации ББ/ОФР: источник
|
||||
Excel — §11.2, раскладка — §11.3, строки — §11.4, значения и числовой формат ячейки — §11.2 п. 3 и §11.5, тип и вариант —
|
||||
§11.7, самопроверка — §7 и §11.8, статус — §11.9. Прежний текст §12 (Срез 2b, контракт v1.2) — в истории git, коммит
|
||||
`43b4b80`. Ссылки «§12 п. N» в старых документах: п. 1 → §11.2 п. 1; п. 2 → §11.3 п. 1; п. 3 → §11.3 п. 7; п. 4 → §11.3
|
||||
п. 3, п. 6; п. 5 → §11.2 п. 3, §11.4, §11.5; п. 6 → §11.2 п. 2; п. 7 → §7, §11.8; п. 8 → §11.7 п. 1; п. 9 → §11.2 п. 4;
|
||||
п. 10 → §11.9; п. 11 → §11.11.
|
||||
|
||||
@ -38,10 +38,10 @@
|
||||
| OQ-30 | Макет: шахматка и КФО показаны в тыс. руб. с 2 знаками, ОСВ — в рублях; где и как округлять (связано с OQ-26) | макет [30] [31-01] | срез 5–6 | допущение: хранить и считать в рублях, переключатель единиц — только отображение |
|
||||
| OQ-31 | КПП ЮЛ: в макете колонка и поле форм (`50`, `61`, `51-02`, `79`), в DBML v5.13 у `legal_entity` поля нет. Нужен ли КПП (и в импорте, и для ИП, у которых его нет)? | макет [50] [51-02] | срез 1, DBML v5.14; пересмотр — DBML v5.17 (docs#53, arm-core#45) | **закрыт решением владельца 07.10.2026: КПП нужен** (пересмотр; повод — макет и комментарий фронтенда «в ручке получения ЮЛ не приходит КПП»): DBML v5.17 — `legal_entity.kpp`, `legal_entity_history.kpp` (nullable). Формат — 9 символов: 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры, иначе `INVALID_KPP`; у ИП КПП запрещён (422 `INVALID_KPP`, при импорте строка пропускается с предупреждением); для ЮЛ необязателен (старые записи и импорт без колонки валидны); уникален только `inn` (КПП — атрибут, сопоставление контрагентов ОСВ по ИНН не меняется); изменение КПП — обычный PATCH со снимком в `legal_entity_history`. *История: 04.10.2026 — «КПП не нужен» (DBML v5.14 без поля; импорт его не читал; формы — без КПП); отменено 07.10.2026* |
|
||||
| OQ-32 | ЮЛ в макете состоит одновременно в ГСК и ГСЗ («Входит в ГСК» и «Входит в ГСЗ» заполнены, [79]); ТЗ 6.1 / ANL-030 — только в одной. Что верно? | макет [79] [51-02] | срез 1 (partial unique `related_group_member.legal_entity_id`) | **закрыт решением владельца 04.10.2026: ЮЛ только в одной ГК, одновременно — в одной ГСК и одной ГСЗ.** DBML v5.14: `related_group_member.kind` + partial unique `(legal_entity_id, kind) WHERE is_current` (отменяет ANL-030); выгрузка ЕЦП это подтверждает (флаги «входит в ГСК» и «входит в ГСЗ» одновременно) |
|
||||
| OQ-33 | «ID ГСК/ГСЗ в CRM» в формах (`51-03`, `51-04`, `77-02`; пример «3-9CGSK01» в формате ЕЦП CRM): у `related_group` в DBML только внутренний `code` (GSK-001). Есть ли у ГСК/ГСЗ идентификатор в CRM? | макет [77-02] | срез 1, DBML | **закрыт реализацией Среза 1:** `related_group.code` генерируется системой (`GSK-NNNNNN`/`GSZ-NNNNNN`), ID из CRM не вводится и не импортируется (в выгрузке ЕЦП его нет — OQ-91). **Решение владельца 14.10.2026:** `POST`/`PATCH` ГСК/ГСЗ принимают `crm_id` и пока отбрасывают его (arm-core #111), колонка `related_group.crm_id` — отдельной задачей arm-core #112 (DBML v5.19) |
|
||||
| OQ-33 | «ID ГСК/ГСЗ в CRM» в формах (`51-03`, `51-04`, `77-02`; пример «3-9CGSK01» в формате ЕЦП CRM): у `related_group` в DBML только внутренний `code` (GSK-001). Есть ли у ГСК/ГСЗ идентификатор в CRM? | макет [77-02] | срез 1, DBML | **закрыт реализацией Среза 1:** `related_group.code` генерируется системой (`GSK-NNNNNN`/`GSZ-NNNNNN`), ID из CRM не вводится и не импортируется (в выгрузке ЕЦП его нет — OQ-91). **Решение владельца 14.10.2026:** `POST`/`PATCH` ГСК/ГСЗ принимают `crm_id` и пока отбрасывают его (arm-core #111), колонка `related_group.crm_id` — отдельной задачей arm-core #112 (DBML v5.19). **18.10.2026, DBML v5.19:** колонка `related_group.crm_id` (`varchar`, nullable, уникальна, как у ГК и ЮЛ) введена в миграцию v5.19 (arm-core#112, Р-10 change `unified-statement-normalization`); `POST`/`PATCH` ГСК/ГСЗ сохраняют её, повтор — 409 `CRM_ID_EXISTS`; импорт ЕЦП её не заполняет (OQ-91). Вопрос закрыт до реализации arm-core#112 |
|
||||
| OQ-34 | «Удалить» ГСК в меню строки (`77-01`): физическое удаление или закрытие с историей? В DBML у `related_group` нет флага/истории удаления | макет [77-01] | срез 1 | **закрыт реализацией Среза 1 (ASSUMPTION подтверждено отсутствием возражений):** удаляется только пустая ГСК/ГСЗ (409 `GROUP_NOT_EMPTY`), состав сохраняется в `before_value` аудита; группа с FK из консолидаций — follow-up `GROUP_IN_USE` (arm-core#8, T8.1) |
|
||||
| OQ-35 | База процента отклонения: в таблице корректировки % считается к сумме стороны ЮЛ-1 (−79,77 % для 1 000 ↔ 202,32, `31-02-02`), в «Подтверждении КФО клиента» — к версии банка (ст. 2, `31-06`); DBML и Методика (сноска 14) — к **меньшей** сумме. Что считать правильным для `correction.deviation_percent` и `client_kfo_line.deviation_percent`? | макет [31-02] [31-06] vs DBML, Методика | срезы 5–6 | допущение: к меньшей (Методика); макет считать неточным |
|
||||
| OQ-36 | Экран проверки показывает строки формы **без кода** («в том числе: Основные средства, пригодные к использованию…», `41`); DBML хранит только строки с кодом (`financial_statement_line`), контракт требует `line_code`. Хранить ли расшифровочные строки без кода (и как их показывать)? | макет [41] vs DBML, контракт | срез 3, контракт v1.x | допущение: строки без кода не хранить и не показывать в первой очереди; **Срез 3b: допущение реализовано** — `financial_statement_line` пишется только для строк с кодом (строк без кода в результате нет), экран их не получает. |
|
||||
| OQ-36 | Экран проверки показывает строки формы **без кода** («в том числе: Основные средства, пригодные к использованию…», `41`); DBML хранит только строки с кодом (`financial_statement_line`), контракт требует `line_code`. Хранить ли расшифровочные строки без кода (и как их показывать)? | макет [41] vs DBML, контракт | срез 3, контракт v1.x | допущение: строки без кода не хранить и не показывать в первой очереди; **Срез 3b: допущение реализовано** — `financial_statement_line` пишется только для строк с кодом (строк без кода в результате нет), экран их не получает. **Закрыт решением владельца 17.10.2026 (Р-8, change `unified-statement-normalization`, контракт v2.0):** строки без кода отдаются сервисами со статусом `detail` (расшифровка под строкой с кодом), `missing` или `unreadable`, хранятся ядром (`financial_statement_line.line_status`, `line_code` nullable, DBML v5.19); в проверки §7, агрегаты и `done` `detail` не входит; экран по умолчанию прячет расшифровки под переключателем «Показать расшифровки (N)». Правила — `05-domain-rules.md` §11.4. Действует с переходом сервисов на v2.0 |
|
||||
| OQ-37 | Строка «Гудвилл 1105» (ФСБУ 4/2023) в распознанной форме (`41`) vs строка `kfo_result_line.line_code = 'GOODWILL'` в разделе I КФО: использовать код 1105 или служебный GOODWILL? Влияет на шаблон строк на фронте и на формулы §7 (1100 = … + 1105?) | макет [41], ФСБУ 4/2023 | срез 3, 6 | допущение: хранить код 1105 как пришёл; строка КФО «GOODWILL» — отдельная, уточнить у методологов |
|
||||
| OQ-38 | ЕХ в макете показывает и **неверифицированные** консолидации («Консолидация 2025 К4», без даты верификации, `62` [83-01]); ЕХ ТЗ 6.2 — срез пишется только после «Верифицирован». Показывать ли в ЛК ЕХ незавершённые консолидации (как «в работе») или только `ex_snapshot`? | макет [62] [83-01] vs ЕХ ТЗ 6.2 | срез 7 | допущение: только срезы `ex_snapshot`; незавершённые — в разделе «Отчётность» |
|
||||
| OQ-39 | Состав «сформированной отчётности» в ЕХ (`83-01`): КФО .xlsx, Шахматка .xlsx и «Консолидированная отчетность .pdf» с датой «Сформирован» и подписями Утвердил/Верифицировал. Нужен ли PDF-экспорт КФО и какие ещё артефакты (сводная ОСВ, корректировки, гудвилл — ЕХ ТЗ 5.2) показывать? | макет [83-01] vs ЕХ ТЗ 5.2 | срез 7 | допущение: xlsx из `payload` на лету для всех `item_type`; PDF — не делать до решения |
|
||||
@ -146,22 +146,22 @@
|
||||
| OQ-138 | Единицы «тыс. руб.» в шапке против значений в рублях с копейками в выгрузках 1С (Унагранде Компани/Групп, НПЗ) и тысяч с дробями (Межениновская): кто и где приводит единицы. Примеры: книга НПЗ (`npz/2025-Q3`) — ББ `needs_check`, `1600 = 1700` расходится на 1.94 / 9.02 / 2.48 руб.; ОФР НПЗ и ОФР Унагранде (стр. 1) — `needs_check` по 2100/2200/2300 по той же причине; полные формы по факту сходятся только у Межениновской | `fixtures/statements-xlsx/README.md`, tabular-parser design 2b D-6, D-7, решения №22–24 (R-11, R-13, R-14) | Срез 3 (маппинг форм), §8 (КФО в тыс. руб.) | **закрыт решением владельца 06.10.2026:** единица «тыс. руб.» в шапке считается верной, значения отдаются как в файле; расхождения итогов в пределах округления (до 0,001 % наибольшего числа соотношения) статус не портят — мягкие нарушения «в пределах округления» (arm-tabular-parser#20). Книга НПЗ, ОФР стр. 1 Унагранде — `done`. Принимать ли расхождение и приводить ли единицы — решает КА в ядре (акцепт проверок, Срез 3). **Пересмотрено владельцем 15.10.2026 (AURORA-1528, arm-tabular-parser#42):** значение ББ/ОФР из Excel — как его показывает числовой формат ячейки (масштабирующие запятые `#,##0,` → тысячи, знаки после точки до 2, секции и условия `[=0]"-"`, литералы условных секций как текстовые ячейки, округление половина от нуля, знак от сырого значения); `General` — как в ячейке; неразобранный формат (%, экспонента, дробь, даты, @) — как в ячейке + предупреждение на документ; xlsb — как в файле + предупреждение; ОСВ не затронута. Решение 06.10.2026 «значения как в файле» заменено |
|
||||
| OQ-139 | Форма без колонки кодов: сопоставление по именам строк из шаблонов 66н/ФСБУ (словарь `statement_lines.yaml`, `line_code_confidence < 100`)? Сейчас документ без строк и `needs_check` («колонка кодов не найдена») | решение владельца 06.10.2026 (вопрос 6), tabular-parser design 2b D-4 | §12 п. 2, Срез 3 | открыт: нужен реальный файл без колонки кодов; допущение — не сопоставлять по именам; **связано (Срез 3a):** то же для PDF-пути — справочник кодов по редакциям, имена из OCR при пустой редакции (OQ-146, OQ-150) |
|
||||
| OQ-140 | Страница-продолжение в отдельном файле (1С печатает ББ/ОФР постранично, один файл = страница): склейка в ядре по ЮЛ + период + тип формы; признак `is_continuation` / `form_page` в контракте (MINOR, две необязательных поля) | решение владельца 06.10.2026 (вопрос 2), design 2b D-8 | §12 п. 9, Срез 3, контракт v1.x | допущение принято: страницы разных файлов не склеиваются; страница 2 без шапки — документ с пустой шапкой и `needs_check`, «загрузите форму одним файлом»; **Срез 3b: допущение реализовано** — страницы одной формы из разных файлов не склеиваются: каждый файл — отдельный документ со своим периодом. |
|
||||
| OQ-141 | Строка без кода, но с суммами в ОФР ФСБУ 4/2023 (НПЗ «Реализация иного имущества…»): хранить ли (OQ-36 расширяется с расшифровочных строк на строки с суммами) | `fixtures/statements-xlsx/npz`, design 2b D-6 | Срез 3, DBML `financial_statement_line` (хранит только строки с кодом) | допущение принято: строки без кода не отдаются, при наличии сумм — предупреждение «строк без кода с суммами пропущено: N» |
|
||||
| OQ-141 | Строка без кода, но с суммами в ОФР ФСБУ 4/2023 (НПЗ «Реализация иного имущества…»): хранить ли (OQ-36 расширяется с расшифровочных строк на строки с суммами) | `fixtures/statements-xlsx/npz`, design 2b D-6 | Срез 3, DBML `financial_statement_line` (хранит только строки с кодом) | допущение принято: строки без кода не отдаются, при наличии сумм — предупреждение «строк без кода с суммами пропущено: N». **Закрыт решением владельца 17.10.2026 (Р-8):** строка без кода с суммами отдаётся и хранится — `detail` под строкой с кодом (НПЗ «Реализация иного имущества…»), `missing` без строки с кодом выше; подтвердить документ, пока у `unreadable` / `missing` есть суммы, нельзя (409 `LINES_UNRESOLVED`, Р-13). Правила — `05-domain-rules.md` §11.4, §9.5 п. 4 |
|
||||
| OQ-142 | Образец ФСБУ 4/2023 (`templates/ББ после 01.01.2025.xlsx`): «АКТИВ» и код 1110 в одной строке из-за объединённых ячеек; шаблоны (коды есть, сумм и шапки нет) не данные. Нужен ли их разбор вообще | `fixtures/statements-xlsx/templates`, design 2b D-8 | §12 п. 10, тесты | допущение принято: шаблоны дают `needs_check` (шапка пустая, значения `null`); разбор шаблонов как источника данных не планируется, пока владелец не скажет иначе |
|
||||
| OQ-143 | Лист продолжения с **единственной** кодовой строкой (книга НПЗ: лист ОФР с одной строкой 2400): `min_codes` = 3 → лист не распознаётся как форма и пропускается, строка 2400 теряется при `needs_check` документа. Понизить порог / принимать лист по близости к открытому документу ОФР / маркеру продолжения | tabular-parser design 2b: решение №25, O-9 | §12 п. 2, п. 9; полнота ОФР НПЗ | **реализовано в 2b (финальная волна, I-1):** лист с 1–2 кодами форм после листа формы берётся как продолжение (`min_codes_continuation = 1` в словаре) и присоединяется при совпадении числа колонок сумм и типа формы; не присоединён — `needs_check` и предупреждение; первая страница с 1–2 кодами формой не считается. Строка 2400 НПЗ в результате есть |
|
||||
| OQ-144 | Права на удаление на SMB-шаре контура (`Input` — входящие файлы и `.done`, `Output/<job_id>/`, `Tmp`): можно ли сервису `scan-gateway` удалять файлы, и не удаляет ли их сама ИС Сканирование после обработки. Пока `SCAN_CLEANUP=false` — сервис ничего не удаляет (кроме `.part`, ставшего целью публикации записью без замены), в `Input`, `Output` и `Tmp` накапливаются остатки | решение владельца 06.10.2026, change `slice-3a-ocr-normalizer` D-2, решения №26/№27 | scan-gateway (горячие папки), эксплуатация стенда | открыт: допущение — очистка выключена; dev-compose включает `SCAN_CLEANUP=true`. Спросить платформу/владельцев ИС; при положительном ответе — включить флаг на стенде |
|
||||
| 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 при первом реальном случае |
|
||||
| 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 |
|
||||
| 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/` |
|
||||
| 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` на реальных сканах | открыт: допущение — перенос дословный; правило уточняется после прогона на стенде |
|
||||
| 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 |
|
||||
| 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 — словарь старого ядра). |
|
||||
| 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-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` не класть |
|
||||
| OQ-152 | Повтор кода строки в таблице OCR: остаётся **первая** строка (паритет с Excel-путём, §12 п. 5; решение №15), но в `aep_6.json` (ОФР) код 2410 повторён, оставленная первая строка пуста, а отброшенная содержит `(649)`. Выбрать правило «первая строка с суммой» вместо «первая» (для `aep_3` оно даёт иной результат, чем для `aep_6` — нужна проверка на стенде) | change `slice-3a-ocr-normalizer`, решение №34 (R-16), финальное ревью I-2 | scan-gateway `document.py`, §11.1 п. 4, Excel-путь §12 п. 5 | открыт: допущение — первая строка; предупреждение «код N повторяется» сигнализирует аналитику, сумма отброшенной строки остаётся в `raw/` |
|
||||
| OQ-152 | Повтор кода строки в таблице OCR: остаётся **первая** строка (паритет с Excel-путём, §12 п. 5; решение №15), но в `aep_6.json` (ОФР) код 2410 повторён, оставленная первая строка пуста, а отброшенная содержит `(649)`. Выбрать правило «первая строка с суммой» вместо «первая» (для `aep_3` оно даёт иной результат, чем для `aep_6` — нужна проверка на стенде) | change `slice-3a-ocr-normalizer`, решение №34 (R-16), финальное ревью I-2 | scan-gateway `document.py`, §11.1 п. 4, Excel-путь §12 п. 5 | открыт: допущение — первая строка; предупреждение «код N повторяется» сигнализирует аналитику, сумма отброшенной строки остаётся в `raw/`. **Закрыт решением владельца 17.10.2026 (Р-4):** повтор кода — законный случай, все строки сохраняются со статусом `ok`, для §7 и всего после — сумма по коду (в `aep_6` 2410 = пустая строка + `(649)`); неверно прочитанный совпавший код ловит структурная проверка (Р-16). `05-domain-rules.md` §11.4 |
|
||||
| OQ-153 | Журналировать ли скачивание первички (`GET /files/{id}/content`) и CSV чек-листа (ТЗ 8.2: перечень событий журнала включает «выгрузку»)? Экспорт сводной ОСВ в xlsx пишет `audit_event(EXPORT)` (Срез 4), а скачивание исходного файла и CSV чек-листа в Срезе 3b — нет; комментарии к документу тоже не аудируются (`comment` хранит автора и время) | change `slice-3b-statements-api`, design D-10, D-11, D-13, решение №8; ТЗ 8.2 | `audit_event`, `GET /files/{id}/content`, `GET …/checklist?format=csv`, §9.5 п. 6 | **закрыт решением владельца 07.10.2026 (вариант А):** скачивание исходного файла (`GET /files/{id}/content`, это же и просмотр PDF) и CSV чек-листа в `audit_event` не пишутся; `EXPORT` пишет только экспорт сводной ОСВ в xlsx (Срез 4) |
|
||||
| OQ-154 | Автозамена файла работает на уровне файла (ключ документа: тип, счёт/NULL, год, квартал, нарастающий): исправленный отдельный ОФР заменит весь прежний файл-книгу «ББ + ОФР» того же периода (`SUPERSEDED`), а подтверждённый ББ из неё отвяжется от версий «в работе». Допустима ли такая замена на уровне файла, или формы одного периода нужно заменять по документам | финальное ревью Среза 3b (заметка контроллера), change `slice-3b-statements-api` | Срез 3b / 5 | **закрыт решением владельца 07.10.2026 (вариант А):** замена остаётся на уровне файла (DBML `file_status.SUPERSEDED`, ТЗ 8.2); если у заменённого файла есть документы, ключа которых нет в новом, — предупреждение в `metrics.mapping_warnings` нового файла (какие документы выпали, был ли `CONFIRMED`, из каких версий сняты привязки). Правило — `05-domain-rules.md` §9.5 п. 3; реализация — RSHB/arm-core#58 |
|
||||
| OQ-155 | Эталонная ручная консолидация (`fixtures/golden/Консолидация_Группа РБ.xlsx`, лист `01.10.2024`, «в т. ч. взаимные обязательства») элиминирует у каждой компании **полную сумму её строк на компании группы** (Счастливочная 1230 = 108 062 тыс. = 62.01 «БП Производство» 106 311 116,46 + 62.01 «БП Аренда» 1 401 330 + 76.06 350 000; РБ 1520 = 106 311 тыс.), а ТЗ (DOC-223044, §4–§5) — по **парам** с `min` и порогом 5 %. Какой алгоритм верен | разбор OQ перед Срезом 5 (08.10.2026), сверка эталона с golden ОСВ РБ/Счастливочной Q3 2024 | Срез 5 (матчинг §5, тест сверки с эталоном), Срез 6 (КФО) | **закрыт решением владельца 08.10.2026: алгоритм ТЗ** — пары, `min`, порог 5 % (§4–§5); эталон сверяется только по точным парам (106 311 116,46 по правилу № 6), асимметрия эталона (1 751 тыс. у Счастливочной) — ожидаемое расхождение, описывается в тесте. Связано: OQ-07 (строго 1:1 или гибрид — ждёт владельца), OQ-08 |
|
||||
| OQ-156 | Ответ ИС Сканирование склеивает соседние строки формы в одну: код `1500\n1700`, наименование `Итого по разделу V\n\nБАЛАНС`, суммы `917 775\n2 371 333` (ББ РБ и Счастливочной за 1 кв. 2025, `fixtures/ocr/Бухгалтерская отчетность 1 кв.25.json`, `БО 1 кв 2025.json`). Нормализатор отбрасывал строку как нечисловой код — в документе не было 1500 и 1700 | прогон 1 кв. 2025 на реальных ответах ИС (arm-ka-fixtures#16) | scan-gateway `rows.py`, §11.1 п. 4 | **решено** (владелец 08.10.2026): разлеплять, если в каждой непустой ячейке значений столько же частей, сколько кодов; иначе — как раньше; предупреждение «строк с несколькими кодами разлеплено: N», `needs_check`; реализация — RSHB/arm-scan-gateway#16 |
|
||||
| OQ-156 | Ответ ИС Сканирование склеивает соседние строки формы в одну: код `1500\n1700`, наименование `Итого по разделу V\n\nБАЛАНС`, суммы `917 775\n2 371 333` (ББ РБ и Счастливочной за 1 кв. 2025, `fixtures/ocr/Бухгалтерская отчетность 1 кв.25.json`, `БО 1 кв 2025.json`). Нормализатор отбрасывал строку как нечисловой код — в документе не было 1500 и 1700 | прогон 1 кв. 2025 на реальных ответах ИС (arm-ka-fixtures#16) | scan-gateway `rows.py`, §11.1 п. 4 | **решено** (владелец 08.10.2026): разлеплять, если в каждой непустой ячейке значений столько же частей, сколько кодов; иначе — как раньше; предупреждение «строк с несколькими кодами разлеплено: N», `needs_check`; реализация — RSHB/arm-scan-gateway#16. **Пересмотрено решением владельца 17.10.2026 (Р-17):** разлепленные строки не держат документ в `needs_check` безусловно — их значения `is_recognized = false` и нуждаются в покрытии §7, при сошедшихся соотношениях не подсвечиваются; разлепление — предупреждение `ROW_SPLIT` уровня `info`; нечитаемая часть кода становится строкой `unreadable`. `05-domain-rules.md` §11.4 |
|
||||
| OQ-157 | Чистый документ (без замечаний) требовал ручного «Подтвердить» (`RECOGNIZED` → `CONFIRMED`), хотя строки ОСВ не правятся (OQ-114) и ТЗ 03_9 п. 4 / 10_10 даёт «Загружен» сразу при отсутствии замечаний; на комплекте 1 кв. 2025 — 16 пустых кликов | прогон владельца 08.10.2026 | arm-core `services/auto_confirm.py`, `result_mapping`, `primary_files.delete`; §9.3, §9.5 п. 4–5 | **решено** (владелец 08.10.2026): чистый документ при маппинге сразу `CONFIRMED` системой (`confirmed_by` NULL), переход версии — от загрузившего; удаление автоподтверждённого файла — пока версии «в работе»; RSHB/arm-core#64, change `auto-confirm-clean-documents` |
|
||||
| OQ-158 | В «Осталось N» (строки свода без ЮЛ и без ИНН) попадают строки аналитики, которые не являются контрагентами-организациями: «Ведомость в счет з/платы», подотчётные лица, физлица и т. п. — на РБ 2025 К1 таких большинство из 1291 строки без ИНН, и довести баннер до нуля сопоставлением с ЮЛ нельзя. Нужно ли отличать такие строки (признак «не организация» вручную / по словарю / по счёту) и исключать их из «Осталось N» и предупреждения [31-00] | жалоба владельца на «Указать» в сводной ОСВ (08.10.2026), RSHB/arm-core#74 | arm-core `domain/consolidated_osv.needs_mapping`, §3, экраны [30], [32-osv-no-inn], [31-00] | открыт: до решения такие строки считаются несопоставленными (EXTERNAL), переход к следующему шагу ими не блокируется — блокировки в API нет |
|
||||
| OQ-159 | Note DBML `recognition_job.reply_deadline` «таймаут ожидания (OCR — 5 мин)» расходится с решением владельца 09.10.2026: срок ответа OCR — 20 мин (`DEFAULT_REPLY_TIMEOUT` в arm-core, `SCAN_TIMEOUT_SECONDS=1200` в scan-gateway). Поправить note при следующей версии DBML | решение владельца 09.10.2026 (не выпускать версию DBML ради note); arm-core #87, arm-scan-gateway #26, arm-ka-docs #84 | DBML `recognition_job`, `05-domain-rules.md` §9.4 | **закрыт 09.10.2026:** note исправлен в DBML v5.18 (docs #90) |
|
||||
@ -170,6 +170,9 @@
|
||||
| OQ-162 | ОСВ 1С, шапка варианта 2 «Детализация по субсчетам, субконто: Контрагенты, Контрагенты.ИНН» (ООО «Зерновая компания „Новопетровская“», ОСВ 60 за 9 мес. 2025, замечание банка 13.10.2026): ИНН выводится **отдельной строкой под** контрагентом (уровень глубже, те же суммы, ИНН числом). Парсер принимал «Контрагенты.ИНН» за второй уровень контрагентов: в «Контрагент / аналитика» — `2370012036.0`, наименования нет, ИНН у строк `null` → нет признака ГК/ГСК/Внешний, суммы контрагента в своде дважды | замечание банка 13.10.2026; RSHB/arm-tabular-parser#32 | tabular-parser §1.1, §1.3; свод и резолв контрагента в ядре | **решено владельцем 13.10.2026:** `<группировка>.ИНН` — реквизит, не уровень; строка ИНН склеивается с родительским контрагентом (ИНН — родителю, строка из `rows` убирается, дети — к контрагенту), только если она единственная у родителя и суммы равны; иначе строка `OTHER` и `needs_check`; пустая ячейка ИНН (физлицо) — склейка без ИНН (рулинг R-1); целое число из ячейки xls — без «.0». `05-domain-rules.md` §1.1, §1.3; **реализовано** RSHB/arm-tabular-parser#32 (713 из 716 контрагентов файла получили ИНН, 3 физлица без ИНН). Правило строки ИНН заменено OQ-163 (13.10.2026). |
|
||||
| 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-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.
|
||||
|
||||
@ -48,9 +48,9 @@
|
||||
|
||||
| Файл | Что |
|
||||
|---|---|
|
||||
| `data-model-v5.18.dbml` | источник истины; changelog v5.1…v5.18 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.17 остаются рядом как история) |
|
||||
| `entities-v5.18.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.17 остаются как история) |
|
||||
| `diagrams/part01…09.{svg,png}` | диаграммы v5.18 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) |
|
||||
| `data-model-v5.19.dbml` | источник истины; changelog v5.1…v5.19 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.18 остаются рядом как история) |
|
||||
| `entities-v5.19.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.18 остаются как история) |
|
||||
| `diagrams/part01…09.{svg,png}` | диаграммы v5.19 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) |
|
||||
| `source/tz/data-model-part1_4.dbml` (лежит не в `model/`, а в экспорте ТЗ) | v4 часть 1 — историческое |
|
||||
|
||||
## Макет UI
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
|
||||
1. `CLAUDE.md` (workspace) и `docs/context/00-workspace-layout.md` — как устроена рабочая область, статус аналитики, макет.
|
||||
2. `docs/context/01-project-overview.md`, `02-glossary.md` — предметная область.
|
||||
3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.18.dbml` и
|
||||
`docs/model/entities-v5.18.md`.
|
||||
3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.19.dbml` и
|
||||
`docs/model/entities-v5.19.md`.
|
||||
4. `docs/context/05-domain-rules.md` — алгоритмы (особенно §1.0 и §12 — парсеры ищут структуру по содержимому);
|
||||
`06-test-data.md`; `07-open-questions.md`; `10-user-journey.md`; `09-delivery-plan.md`.
|
||||
5. `arm-core/`, `tabular-parser/`, `scan-gateway/` — три репозитория, развёрнутые из нулевого коммита шаблона команды,
|
||||
@ -37,7 +37,7 @@ NATS, MinIO, эмулятор ИС Сканирование), контракт `
|
||||
`07-open-questions.md`; если нужен ответ для среза — делай допущение по правилам из этого файла.
|
||||
|
||||
Ограничения на всё время работы:
|
||||
- имена таблиц, колонок и enum'ов — ровно как в DBML v5.18; поля, которых нет в DBML, не изобретать;
|
||||
- имена таблиц, колонок и enum'ов — ровно как в DBML v5.19; поля, которых нет в DBML, не изобретать;
|
||||
- алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать;
|
||||
- парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому,
|
||||
синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "2.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"service": {
|
||||
"name": "tabular-parser",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "NOT_IMPLEMENTED",
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "2.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" },
|
||||
"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",
|
||||
"result_schema_version": "2.0",
|
||||
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
|
||||
"metrics": {
|
||||
"duration_ms": 18400,
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "2.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" },
|
||||
"service": {
|
||||
"name": "tabular-parser",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"error": {
|
||||
"code": "NOT_IMPLEMENTED",
|
||||
"message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet"
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "2.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" }
|
||||
"service": {
|
||||
"name": "tabular-parser",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"schema_version": "2.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "0.1.0" },
|
||||
"service": {
|
||||
"name": "tabular-parser",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"status": "done",
|
||||
"documents": [
|
||||
{
|
||||
@ -24,7 +27,11 @@
|
||||
"header_variant": 1
|
||||
},
|
||||
"group_schema": [
|
||||
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
|
||||
{
|
||||
"type": "COUNTERPARTY",
|
||||
"name": "Контрагенты",
|
||||
"has_inn": true
|
||||
}
|
||||
],
|
||||
"columns": {
|
||||
"opening_debit": "C",
|
||||
@ -70,14 +77,18 @@
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50",
|
||||
"extra": { "КПП": "352801001" }
|
||||
"extra": {
|
||||
"КПП": "352801001"
|
||||
}
|
||||
}
|
||||
],
|
||||
"checks": {
|
||||
"total_row_matches_root": true,
|
||||
"balance_identity_violations": [],
|
||||
"parent_sum_violations": []
|
||||
}
|
||||
},
|
||||
"status": "done",
|
||||
"warnings": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
{
|
||||
"schema_version": "1.1",
|
||||
"schema_version": "2.0",
|
||||
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
|
||||
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
|
||||
"service": { "name": "tabular-parser", "version": "1.0.0" },
|
||||
"service": {
|
||||
"name": "tabular-parser",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"status": "needs_check",
|
||||
"metrics": {
|
||||
"rows": 3,
|
||||
"warnings": [
|
||||
"sheet=Лист_1 hierarchy=outline",
|
||||
"§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3"
|
||||
]
|
||||
"warnings": []
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
@ -28,8 +28,15 @@
|
||||
"header_variant": 1
|
||||
},
|
||||
"group_schema": [
|
||||
{ "type": "ACCOUNT", "name": "Счет" },
|
||||
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
|
||||
{
|
||||
"type": "ACCOUNT",
|
||||
"name": "Счет"
|
||||
},
|
||||
{
|
||||
"type": "COUNTERPARTY",
|
||||
"name": "Контрагенты",
|
||||
"has_inn": true
|
||||
}
|
||||
],
|
||||
"columns": {
|
||||
"opening_debit": "D",
|
||||
@ -76,7 +83,9 @@
|
||||
"turnover_credit": "200.50",
|
||||
"closing_debit": "0.00",
|
||||
"closing_credit": "700.50",
|
||||
"extra": { "КПП": "352801001" }
|
||||
"extra": {
|
||||
"КПП": "352801001"
|
||||
}
|
||||
},
|
||||
{
|
||||
"row_number": 3,
|
||||
@ -117,9 +126,24 @@
|
||||
],
|
||||
"checks": {
|
||||
"total_row_matches_root": true,
|
||||
"balance_identity_violations": [3],
|
||||
"balance_identity_violations": [
|
||||
3
|
||||
],
|
||||
"parent_sum_violations": []
|
||||
}
|
||||
},
|
||||
"status": "needs_check",
|
||||
"warnings": [
|
||||
{
|
||||
"code": "OSV_HIERARCHY_BY_OUTLINE",
|
||||
"severity": "info",
|
||||
"message": "иерархия строк — по группировке листа (outline)"
|
||||
},
|
||||
{
|
||||
"code": "OSV_BALANCE_IDENTITY",
|
||||
"severity": "info",
|
||||
"message": "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,44 +1,98 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"schema_version": "2.0",
|
||||
"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"] },
|
||||
"service": { "name": "tabular-parser", "version": "0.4.0", "statements_version": "abc1234" },
|
||||
"status": "needs_check",
|
||||
"metrics": { "duration_ms": 412, "rows": 12, "warnings": [] },
|
||||
"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" },
|
||||
"status": "done",
|
||||
"form_variant": "full",
|
||||
"warnings": [
|
||||
{ "code": "FRACTIONAL_IN_THOUSANDS", "severity": "info", "message": "значений с дробной частью при единице тыс. руб.: 6 из 14" }
|
||||
],
|
||||
"header": {
|
||||
"form_name": "Бухгалтерский баланс",
|
||||
"organization_name": "ООО \"Пример\"", "organization_name_text": "ООО \"Пример\"", "organization_name_confidence": 100,
|
||||
"inn": "7700000000", "inn_text": "7700000000", "inn_confidence": 100,
|
||||
"report_date": "2025-09-30", "report_date_text": "на 30 сентября 2025 г.", "report_date_confidence": 100,
|
||||
"units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100,
|
||||
"okud": "0710001", "okud_text": "0710001", "okud_confidence": 100,
|
||||
"okpo": "12345678", "okpo_text": "12345678", "okpo_confidence": 100,
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
|
||||
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
|
||||
{ "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" }
|
||||
],
|
||||
"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 } ] }
|
||||
{ "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 } ] }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "expected": "905786.99", "actual": "900000.00" } ], "duplicate_line_codes": [] }
|
||||
"checks": {
|
||||
"relations_failed": [],
|
||||
"soft_relations_failed": [ { "column_index": 1, "relation": "1110 = 11101", "codes": ["1110", "11101"], "expected": "905786.99", "actual": "900000.00" } ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 } ] }
|
||||
"status": "needs_check",
|
||||
"form_variant": "full",
|
||||
"warnings": [
|
||||
{ "code": "INN_NOT_FOUND", "severity": "info", "message": "в шапке не найден ИНН", "field": "inn" }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [], "duplicate_line_codes": [] }
|
||||
"header": {
|
||||
"form_name": "Отчет о финансовых результатах",
|
||||
"organization_name": "ООО \"Пример\"", "organization_name_text": "ООО \"Пример\"", "organization_name_confidence": 100,
|
||||
"inn": null, "inn_text": null, "inn_confidence": null,
|
||||
"report_date": "2025-09-30", "report_date_text": "за январь - сентябрь 2025 г.", "report_date_confidence": 100,
|
||||
"units": "тыс. руб.", "units_text": "в тыс. рублей", "units_confidence": 100,
|
||||
"okud": "0710002", "okud_text": "0710002", "okud_confidence": 100,
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "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 } ] }
|
||||
],
|
||||
"checks": {
|
||||
"relations_failed": [ { "column_index": 0, "relation": "2200 = 2100 - 2210 - 2220", "codes": ["2200", "2100", "2210", "2220"], "expected": null, "actual": "400.00" } ],
|
||||
"soft_relations_failed": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"document_type": "DISCLOSURE",
|
||||
"locator": "sheet:Пояснения",
|
||||
"header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах" },
|
||||
"status": "done",
|
||||
"form_variant": null,
|
||||
"warnings": [],
|
||||
"header": { "form_name": "Пояснения к бухгалтерскому балансу и отчету о финансовых результатах", "needs_review_fields": [] },
|
||||
"columns": [],
|
||||
"lines": []
|
||||
}
|
||||
|
||||
@ -1,50 +1,51 @@
|
||||
{
|
||||
"schema_version": "1.2",
|
||||
"schema_version": "2.0",
|
||||
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
|
||||
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
|
||||
"service": { "name": "scan-gateway", "version": "0.1.0" },
|
||||
"status": "done",
|
||||
"service": { "name": "scan-gateway", "version": "0.2.0", "statements_version": "abc1234" },
|
||||
"status": "needs_check",
|
||||
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
|
||||
"metrics": { "duration_ms": 18400, "pages": 2, "warnings": [] },
|
||||
"documents": [
|
||||
{
|
||||
"document_type": "BALANCE",
|
||||
"locator": "pages:1-2",
|
||||
"locator": "file:8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json;table:0",
|
||||
"status": "needs_check",
|
||||
"form_variant": "full",
|
||||
"warnings": [
|
||||
{ "code": "ORGANIZATION_UNCERTAIN", "severity": "info", "message": "организация распознана неуверенно", "field": "organization_name" },
|
||||
{ "code": "ROW_SPLIT", "severity": "info", "message": "строка с несколькими кодами разлеплена: 1370, 1300", "line_refs": ["row:14#1", "row:14#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": "тыс. руб."
|
||||
"organization_name": "ООО Ромашка", "organization_name_text": "ООО Ромашка", "organization_name_confidence": 97.5,
|
||||
"inn": "7700000000", "inn_text": "7700000000", "inn_confidence": 100,
|
||||
"report_date": "2025-09-30", "report_date_text": "30 сентября 2025 г.", "report_date_confidence": 100,
|
||||
"units": "тыс. руб.", "units_text": "тыс. руб.", "units_confidence": 100,
|
||||
"needs_review_fields": []
|
||||
},
|
||||
"columns": [
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
|
||||
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
|
||||
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "label_confidence": 100, "period_date": "2025-09-30", "period_kind": "absolute" }
|
||||
],
|
||||
"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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] },
|
||||
{ "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 } ] }
|
||||
],
|
||||
"checks": { "relations_failed": [], "soft_relations_failed": [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"$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",
|
||||
"$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 (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
|
||||
"$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, что и в файл результата.",
|
||||
"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\"" },
|
||||
"schema_version": { "type": "string", "pattern": "^2\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v2.0 пишет \"2.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"] },
|
||||
@ -47,7 +47,22 @@
|
||||
"fields_total": { "type": "integer" },
|
||||
"fields_low_confidence": { "type": "integer" },
|
||||
"fields_unrecognized": { "type": "integer" },
|
||||
"warnings": { "type": "array", "items": { "type": "string" } }
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/fileWarning" }, "description": "предупреждения уровня файла — те же, что metrics.warnings файла результата" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"fileWarning": {
|
||||
"type": "object",
|
||||
"required": ["code", "severity", "message"],
|
||||
"properties": {
|
||||
"code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" },
|
||||
"severity": { "enum": ["error", "info"] },
|
||||
"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"] },
|
||||
"locator": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"$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 отвергают незнакомые значения.",
|
||||
"$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 ядра.",
|
||||
"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\"" },
|
||||
"schema_version": { "type": "string", "pattern": "^2\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v2.0 пишет \"2.0\"" },
|
||||
"job_id": { "type": "string", "format": "uuid" },
|
||||
"file_id": { "type": "string", "format": "uuid" },
|
||||
"service": {
|
||||
@ -15,10 +15,11 @@
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": { "enum": ["tabular-parser", "scan-gateway"] },
|
||||
"version": { "type": "string" }
|
||||
"version": { "type": "string" },
|
||||
"statements_version": { "type": "string", "minLength": 1, "description": "коммит репозитория arm-statements, из которого разложена копия общего кода; обязателен, если в documents есть statementDocument" }
|
||||
}
|
||||
},
|
||||
"status": { "enum": ["done", "needs_check", "error"] },
|
||||
"status": { "enum": ["done", "needs_check", "error"], "description": "done ⇔ все документы done и нет предупреждений файла уровня error; error — документов нет" },
|
||||
"error": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
@ -35,7 +36,7 @@
|
||||
"fields_total": { "type": "integer" },
|
||||
"fields_low_confidence": { "type": "integer" },
|
||||
"fields_unrecognized": { "type": "integer" },
|
||||
"warnings": { "type": "array", "items": { "type": "string" } }
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/fileWarning" }, "description": "предупреждения уровня файла (ошибочный json набора ИС, потерянная таблица, формат файла)" }
|
||||
}
|
||||
},
|
||||
"raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" },
|
||||
@ -44,31 +45,68 @@
|
||||
"items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] }
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": { "status": { "enum": ["done", "needs_check"] } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": {
|
||||
"properties": { "documents": { "minItems": 1 } }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "status": { "enum": ["done", "needs_check"] } },
|
||||
"required": ["status"]
|
||||
},
|
||||
"then": {
|
||||
"properties": { "documents": { "minItems": 1 } }
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "documents": { "contains": { "properties": { "document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] } }, "required": ["document_type"] } } },
|
||||
"required": ["documents"]
|
||||
},
|
||||
"then": {
|
||||
"properties": { "service": { "required": ["name", "version", "statements_version"] } }
|
||||
}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"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"] },
|
||||
"warning": {
|
||||
"type": "object",
|
||||
"required": ["code", "severity", "message"],
|
||||
"properties": {
|
||||
"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" },
|
||||
"field": { "$ref": "#/$defs/headerField" }
|
||||
}
|
||||
},
|
||||
"fileWarning": {
|
||||
"allOf": [ { "$ref": "#/$defs/warning" } ],
|
||||
"properties": {
|
||||
"locator": { "type": "string", "examples": ["file:aep_3_ofr.json", "sheet:Лист3"], "description": "место в файле, к которому относится предупреждение" }
|
||||
}
|
||||
},
|
||||
"failedRelation": {
|
||||
"type": "object",
|
||||
"required": ["column_index", "relation", "expected", "actual"],
|
||||
"required": ["column_index", "relation", "codes", "expected", "actual"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"relation": { "type": "string", "examples": ["1600 = 1100 + 1200"] },
|
||||
"expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения как в файле" },
|
||||
"actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам" }
|
||||
"codes": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^\\d{4,5}$" }, "description": "коды цели и членов соотношения: нарушение подсвечивает все строки этих кодов в колонке" },
|
||||
"expected": { "$ref": "#/$defs/money", "description": "левая часть соотношения (цель) по строкам ok; null — цели нет" },
|
||||
"actual": { "$ref": "#/$defs/money", "description": "правая часть, вычисленная по строкам ok" }
|
||||
}
|
||||
},
|
||||
"osvDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "group_schema", "rows"],
|
||||
"required": ["document_type", "locator", "header", "group_schema", "rows", "status", "warnings"],
|
||||
"properties": {
|
||||
"document_type": { "const": "OSV" },
|
||||
"locator": { "type": "string", "examples": ["sheet:Лист_1"] },
|
||||
"status": { "$ref": "#/$defs/documentStatus", "description": "done — ведомость разобрана (§1.6)" },
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" }, "description": "предупреждения листа ОСВ; все уровня info, код с префиксом OSV_" },
|
||||
"header": {
|
||||
"type": "object",
|
||||
"required": ["organization_name", "account_code_raw", "period_text"],
|
||||
@ -142,79 +180,128 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"statementValue": {
|
||||
"type": "object",
|
||||
"required": ["column_index", "amount", "text", "confidence", "is_recognized", "needs_review"],
|
||||
"properties": {
|
||||
"column_index": { "type": "integer" },
|
||||
"amount": { "$ref": "#/$defs/money" },
|
||||
"string_value": { "type": ["string", "null"], "description": "текст ячейки, только когда amount = null и текст непуст и не прочерк" },
|
||||
"text": { "type": ["string", "null"], "description": "сырой текст источника всегда (у Excel — как показывает числовой формат); null — позиции нет" },
|
||||
"confidence": { "$ref": "#/$defs/confidence", "description": "уверенность источника (у Excel 100; позиции нет — 100)" },
|
||||
"is_recognized": { "type": "boolean", "description": "текст разобран как число без выброшенных и исправленных символов; от confidence не зависит" },
|
||||
"needs_review": { "type": "boolean", "description": "сводный признак: нуждается в покрытии §7 и не покрыто, не подтверждён знак, или в несошедшемся жёстком соотношении (§11)" }
|
||||
}
|
||||
},
|
||||
"statementLine": {
|
||||
"type": "object",
|
||||
"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": "положение в источнике; уникален в документе" },
|
||||
"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": "сырой текст ячейки кода" },
|
||||
"line_code_confidence": { "$ref": "#/$defs/confidence", "description": "от источника; у кода с заменой двойника — не выше 99" },
|
||||
"line_name": { "type": ["string", "null"], "description": "распознанное наименование как в источнике (не из справочника)" },
|
||||
"line_name_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"parent_ref": { "type": ["string", "null"], "description": "ref строки-родителя у detail" },
|
||||
"needs_review": { "type": "boolean", "description": "неуверенный код не подтверждён структурой или повтор кода с неуверенной копией" },
|
||||
"values": { "type": "array", "items": { "$ref": "#/$defs/statementValue" } }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "line_status": { "const": "ok" } }, "required": ["line_status"] },
|
||||
"then": { "properties": { "line_code": { "type": "string" } } },
|
||||
"else": { "properties": { "line_code": { "type": "null" } } }
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "line_status": { "const": "detail" } }, "required": ["line_status"] },
|
||||
"then": { "required": ["parent_ref"], "properties": { "parent_ref": { "type": "string" } } }
|
||||
}
|
||||
]
|
||||
},
|
||||
"statementDocument": {
|
||||
"type": "object",
|
||||
"required": ["document_type", "locator", "header", "columns", "lines"],
|
||||
"required": ["document_type", "locator", "header", "columns", "lines", "status", "warnings"],
|
||||
"properties": {
|
||||
"document_type": { "enum": ["BALANCE", "INCOME", "DISCLOSURE", "OTHER"] },
|
||||
"locator": { "type": "string", "examples": ["pages:1-2"] },
|
||||
"locator": { "type": "string", "examples": ["file:aep_3.json;table:0", "sheet:Баланс,Баланс (2)"] },
|
||||
"status": { "$ref": "#/$defs/documentStatus" },
|
||||
"form_variant": { "enum": ["full", "simplified", null], "description": "по структуре кодов (§11); null — DISCLOSURE/OTHER" },
|
||||
"warnings": { "type": "array", "items": { "$ref": "#/$defs/warning" } },
|
||||
"header": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"form_name": { "type": "string" },
|
||||
"form_name": { "type": ["string", "null"], "description": "заголовок формы как в источнике" },
|
||||
"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"] },
|
||||
"organization_name_text": { "type": ["string", "null"] },
|
||||
"organization_name_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"inn": { "type": ["string", "null"], "pattern": "^(\\d{10}|\\d{12})$" },
|
||||
"inn_text": { "type": ["string", "null"] },
|
||||
"inn_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"report_date": { "type": ["string", "null"], "format": "date" },
|
||||
"report_date_text": { "type": ["string", "null"] },
|
||||
"report_date_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"units": { "type": ["string", "null"], "examples": ["тыс. руб."] },
|
||||
"units_text": { "type": ["string", "null"] },
|
||||
"units_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"okud": { "type": ["string", "null"] },
|
||||
"okud_text": { "type": ["string", "null"] },
|
||||
"okud_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"okpo": { "type": ["string", "null"] },
|
||||
"okpo_text": { "type": ["string", "null"] },
|
||||
"okpo_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"okopf": { "type": ["string", "null"] },
|
||||
"okopf_text": { "type": ["string", "null"] },
|
||||
"okopf_confidence": { "$ref": "#/$defs/confidence" },
|
||||
"okfs": { "type": ["string", "null"] },
|
||||
"address": { "type": ["string", "null"] }
|
||||
"okfs_text": { "type": ["string", "null"] },
|
||||
"okfs_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)" }
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["column_index", "label"],
|
||||
"required": ["column_index", "label", "period_kind"],
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
"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); у относительной подписи — от эффективной даты документа" },
|
||||
"period_kind": { "enum": ["absolute", "relative:current", "relative:prev_year_end", "relative:prev_prev_year_end", "relative:prev_period", "none"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"lines": { "type": "array", "items": { "$ref": "#/$defs/statementLine" } },
|
||||
"checks": {
|
||||
"type": "object",
|
||||
"description": "Результаты самопроверки формы по соотношениям §7 (tabular-parser). Отсутствие объекта — «не проверялось» (OCR-путь); пустые массивы — «проверено, сошлось».",
|
||||
"required": ["relations_failed", "soft_relations_failed"],
|
||||
"description": "Самопроверка формы по соотношениям §7 общим кодом (оба конвейера). Есть у BALANCE/INCOME со строками, нет у документа без строк. Пустые массивы — «проверено, сошлось».",
|
||||
"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": "коды строк, встретившиеся в документе больше одного раза (осталась первая строка)" }
|
||||
"relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "жёсткие соотношения, не сошедшиеся вне допуска ±1 и округления 0,001 %: документ needs_check" },
|
||||
"soft_relations_failed": { "type": "array", "items": { "$ref": "#/$defs/failedRelation" }, "description": "мягкие соотношения (OQ-12, подстроки) и жёсткие в пределах округления (OQ-138): info" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "document_type": { "enum": ["BALANCE", "INCOME"] }, "lines": { "minItems": 1 } },
|
||||
"required": ["document_type", "lines"]
|
||||
},
|
||||
"then": { "required": ["checks"] }
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "lines": { "maxItems": 0 } }, "required": ["lines"] },
|
||||
"then": { "not": { "required": ["checks"] } }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm
|
||||
|
||||
Для `arm-core` дополнительно:
|
||||
|
||||
- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.18.dbml` (путь от `api/`, по умолчанию — переменная `DBML` Makefile, она указывает на текущую версию модели;
|
||||
- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.19.dbml` (путь от `api/`, по умолчанию — переменная `DBML` Makefile, она указывает на текущую версию модели;
|
||||
другой — `make dbml-check DBML=<путь относительно api/>`): ORM обязана совпадать с DBML; расхождения печатаются
|
||||
построчно, код 1.
|
||||
- Цикл миграций «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой БД, пустой
|
||||
|
||||
@ -411,6 +411,67 @@ UI прячет элементы по `GET /me → permissions` (объедин
|
||||
- `cross_form` зависит от второго документа ЮЛ за тот же период: появилась ОФР отдельным файлом — перечитайте ББ.
|
||||
- Документ формы появляется после того, как файл стал `PROCESSED`; до этого `…/statement` — 404.
|
||||
|
||||
### 5.6.1 Формы ББ/ОФР после единой нормализации — **после реализации**
|
||||
|
||||
> **Пока не работает на `main`.** Раздел описывает API после change `unified-statement-normalization`
|
||||
> (RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19; дочерние PR arm-core и arm-web ещё не созданы).
|
||||
> Правила — `context/05-domain-rules.md` §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спека `arm-web` того же change.
|
||||
> Поля добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. После слияния PR раздел
|
||||
> переедет в §5.6 и §1.
|
||||
|
||||
**`GET /documents/{id}/statement` — новые поля `StatementOut`:**
|
||||
|
||||
- `document.form_variant` — `full` | `simplified` | `null` (упрощённая форма — свой набор проверок §7).
|
||||
- `header.needs_review_fields[]` — поля шапки, требующие проверки (сейчас бывает только `report_date`); сырые тексты полей.
|
||||
- `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` у
|
||||
статусов кроме `ok`), `line_code_text` (сырой текст ячейки кода — показывать серым у `unreadable` / `missing`),
|
||||
`line_name` (наименование **из файла**), `reference_name` (справочное имя по коду или `null` — его показывать основным,
|
||||
файловое рядом, если отличается), `parent_row_index` (родитель расшифровки `detail`), `needs_review` (подсветить строку),
|
||||
`is_confirmed`, `line_code_modified`. **Один код может стоять у нескольких строк** (повтор кода законен): не строить
|
||||
словарь «код → строка», ключ строки — `id` (у достроенных шаблонных — `line_code`, их у кода с реальной строкой `ok` нет).
|
||||
- `values[]` — `text` (сырой текст источника; в подсказку ячейки вместе с `confidence`), `needs_review`, `is_confirmed`.
|
||||
**Подсветка ячейки — только по `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`.
|
||||
- `checks.failed[]` / `soft_failed[]` — у нарушения ещё `codes[]` и `line_ids[]` (все строки кодов соотношения).
|
||||
- Порядок `lines` — уже «как на бумаге»: строки `ok` по коду (одинаковые коды по `row_index`), `unreadable` / `missing` —
|
||||
после ближайшей предыдущей по `row_index` строки с кодом (без такой — в начале), `detail` — сразу после родителя.
|
||||
Расшифровки (`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` | — |
|
||||
|
||||
Общие ошибки `lines`: 422 `UNKNOWN_LINE` (строка не этого документа), 422 `INVALID_LINE_PATCH` (в элементе нет действия или
|
||||
их несколько). Правка суммы по `line_code` (достроенная строка) для кода, у которого уже есть строка `ok`, — 422
|
||||
`UNKNOWN_CELL`: передайте `line_id`. Правка `header.report_date` пересчитывает даты колонок с `period_kind = relative:*`.
|
||||
Удалять строки нельзя — для этого «Сделать расшифровкой». Ответ — `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`.
|
||||
|
||||
**Статус документа.** Форма из 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).
|
||||
|
||||
### 5.7 Консолидации
|
||||
|
||||
| Метод и путь | Право | Запрос | Ответ | Ошибки |
|
||||
@ -606,6 +667,10 @@ async function download(path: string, fallbackName: string): Promise<void> {
|
||||
| `ALREADY_CONFIRMED`, `FILE_SUPERSEDED`, `DOCUMENT_NOT_CONFIRMABLE` | 409 | подтверждение, правка формы | — | перечитать документ |
|
||||
| `PERIOD_REQUIRED` | 409 | подтверждение формы | — | «Укажите дату отчёта» → режим правки шапки |
|
||||
| `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) | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами |
|
||||
| `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 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр |
|
||||
| `UNKNOWN_CELL`, `DUPLICATE_CELL` | 422 | правка формы | — | ошибка клиента (перечитать форму); `UNKNOWN_CELL` также: `line_code` уже есть в форме (слать `line_id`), код вне справочника редакции, нет редакции |
|
||||
@ -657,6 +722,7 @@ async function download(path: string, fallbackName: string): Promise<void> {
|
||||
| Задача | Что поменяется для фронта | Как подготовиться |
|
||||
|---|---|---|
|
||||
| OQ-54 — SSO РАИСА | Меняется только получение токена (вместо dev-формы) | держать слой получения токена изолированным (`frontend-auth-guide.md` §9) |
|
||||
| arm-ka-docs#116 — единая нормализация ББ/ОФР (контракт v2.0, DBML v5.19) | `GET/PATCH …/statement`: строки со статусами и повторами кода, `needs_review`, `issues[]`, `lines`/`columns`/`confirmed` в PATCH; подтверждение — 409 `LINES_UNRESOLVED`, `DUPLICATE_FORM`; `crm_id` ГСК/ГСЗ сохраняется | §5.6.1; не держать словарь «код → строка», подсветку строить по `needs_review` |
|
||||
|
||||
Состояние этих задач — в Gitea (`RSHB/arm-core`, `RSHB/arm-ka-docs`); после слияния каждой этот раздел переезжает в §1.
|
||||
|
||||
|
||||
1481
model/data-model-v5.19.dbml
Normal file
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 513 KiB |
@ -4,9 +4,9 @@
|
||||
<!-- Generated by graphviz version 2.47.0 (20210316.0004)
|
||||
-->
|
||||
<!-- Title: dbml Pages: 1 -->
|
||||
<svg width="2735pt" height="3808pt"
|
||||
viewBox="0.00 0.00 2735.12 3808.15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3804.15)">
|
||||
<svg width="2735pt" height="3894pt"
|
||||
viewBox="0.00 0.00 2735.12 3893.90" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3889.9)">
|
||||
<title>dbml</title>
|
||||
<!-- entity_type -->
|
||||
<g id="entity_type" class="node">
|
||||
@ -26,17 +26,17 @@
|
||||
<!-- related_group_kind -->
|
||||
<g id="related_group_kind" class="node">
|
||||
<title>related_group_kind</title>
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="1774.01" cy="-3436.65" rx="299.63" ry="130.22"/>
|
||||
<polygon fill="#29235c" stroke="transparent" points="1564.01,-3466.65 1564.01,-3526.65 1984.01,-3526.65 1984.01,-3466.65 1564.01,-3466.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3466.65 1564.01,-3526.65 1984.01,-3526.65 1984.01,-3466.65 1564.01,-3466.65"/>
|
||||
<text text-anchor="start" x="1574.82" y="-3487.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group_kind       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="1564.01,-3406.65 1564.01,-3466.65 1984.01,-3466.65 1984.01,-3406.65 1564.01,-3406.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3406.65 1564.01,-3466.65 1984.01,-3466.65 1984.01,-3406.65 1564.01,-3406.65"/>
|
||||
<text text-anchor="start" x="1713.54" y="-3427.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#1d71b8">    gsk    </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="1564.01,-3346.65 1564.01,-3406.65 1984.01,-3406.65 1984.01,-3346.65 1564.01,-3346.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3346.65 1564.01,-3406.65 1984.01,-3406.65 1984.01,-3346.65 1564.01,-3346.65"/>
|
||||
<text text-anchor="start" x="1713.54" y="-3367.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#1d71b8">    gsz    </text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="1563.01,-3345.65 1563.01,-3527.65 1985.01,-3527.65 1985.01,-3345.65 1563.01,-3345.65"/>
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="1774.01" cy="-3514.65" rx="299.63" ry="130.22"/>
|
||||
<polygon fill="#29235c" stroke="transparent" points="1564.01,-3544.65 1564.01,-3604.65 1984.01,-3604.65 1984.01,-3544.65 1564.01,-3544.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3544.65 1564.01,-3604.65 1984.01,-3604.65 1984.01,-3544.65 1564.01,-3544.65"/>
|
||||
<text text-anchor="start" x="1574.82" y="-3565.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group_kind       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="1564.01,-3484.65 1564.01,-3544.65 1984.01,-3544.65 1984.01,-3484.65 1564.01,-3484.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3484.65 1564.01,-3544.65 1984.01,-3544.65 1984.01,-3484.65 1564.01,-3484.65"/>
|
||||
<text text-anchor="start" x="1713.54" y="-3505.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#1d71b8">    gsk    </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="1564.01,-3424.65 1564.01,-3484.65 1984.01,-3484.65 1984.01,-3424.65 1564.01,-3424.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="1564.01,-3424.65 1564.01,-3484.65 1984.01,-3484.65 1984.01,-3424.65 1564.01,-3424.65"/>
|
||||
<text text-anchor="start" x="1713.54" y="-3445.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#1d71b8">    gsz    </text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="1563.01,-3423.65 1563.01,-3605.65 1985.01,-3605.65 1985.01,-3423.65 1563.01,-3423.65"/>
|
||||
</g>
|
||||
<!-- le_history_event -->
|
||||
<g id="le_history_event" class="node">
|
||||
@ -462,42 +462,42 @@
|
||||
<g id="legal_entity_alias" class="node">
|
||||
<title>legal_entity_alias</title>
|
||||
<g id="a_legal_entity_alias"><a xlink:title="legal_entity_alias Словарь соответствий наименований контрагентов (ТЗ 6.5)">
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="341.53" cy="-2166.65" rx="315.24" ry="299.63"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="120.53,-2316.65 120.53,-2376.65 562.53,-2376.65 562.53,-2316.65 120.53,-2316.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2316.65 120.53,-2376.65 562.53,-2376.65 562.53,-2316.65 120.53,-2316.65"/>
|
||||
<text text-anchor="start" x="156.59" y="-2337.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       legal_entity_alias       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2256.65 120.53,-2316.65 562.53,-2316.65 562.53,-2256.65 120.53,-2256.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2256.65 120.53,-2316.65 562.53,-2316.65 562.53,-2256.65 120.53,-2256.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2277.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="156.42" y="-2277.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="491.07" y="-2277.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2196.65 120.53,-2256.65 562.53,-2256.65 562.53,-2196.65 120.53,-2196.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2196.65 120.53,-2256.65 562.53,-2256.65 562.53,-2196.65 120.53,-2196.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2216.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">legal_entity_id    </text>
|
||||
<text text-anchor="start" x="451.97" y="-2217.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="512.44" y="-2217.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.33" y="-2217.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2136.65 120.53,-2196.65 562.53,-2196.65 562.53,-2136.65 120.53,-2136.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2136.65 120.53,-2196.65 562.53,-2196.65 562.53,-2136.65 120.53,-2136.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2156.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">alias    </text>
|
||||
<text text-anchor="start" x="405.77" y="-2157.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="512.44" y="-2157.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.33" y="-2157.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2076.65 120.53,-2136.65 562.53,-2136.65 562.53,-2076.65 120.53,-2076.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2076.65 120.53,-2136.65 562.53,-2136.65 562.53,-2076.65 120.53,-2076.65"/>
|
||||
<text text-anchor="start" x="131.24" y="-2096.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">normalized_alias    </text>
|
||||
<text text-anchor="start" x="406.15" y="-2097.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="512.82" y="-2097.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.72" y="-2097.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2016.65 120.53,-2076.65 562.53,-2076.65 562.53,-2016.65 120.53,-2016.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2016.65 120.53,-2076.65 562.53,-2076.65 562.53,-2016.65 120.53,-2016.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2036.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_by    </text>
|
||||
<text text-anchor="start" x="491.07" y="-2037.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-1956.65 120.53,-2016.65 562.53,-2016.65 562.53,-1956.65 120.53,-1956.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-1956.65 120.53,-2016.65 562.53,-2016.65 562.53,-1956.65 120.53,-1956.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-1976.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_at    </text>
|
||||
<text text-anchor="start" x="379.08" y="-1977.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="119.53,-1955.65 119.53,-2377.65 563.53,-2377.65 563.53,-1955.65 119.53,-1955.65"/>
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="341.53" cy="-2195.65" rx="315.24" ry="299.63"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="120.53,-2345.65 120.53,-2405.65 562.53,-2405.65 562.53,-2345.65 120.53,-2345.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2345.65 120.53,-2405.65 562.53,-2405.65 562.53,-2345.65 120.53,-2345.65"/>
|
||||
<text text-anchor="start" x="156.59" y="-2366.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       legal_entity_alias       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2285.65 120.53,-2345.65 562.53,-2345.65 562.53,-2285.65 120.53,-2285.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2285.65 120.53,-2345.65 562.53,-2345.65 562.53,-2285.65 120.53,-2285.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2306.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="156.42" y="-2306.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="491.07" y="-2306.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2225.65 120.53,-2285.65 562.53,-2285.65 562.53,-2225.65 120.53,-2225.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2225.65 120.53,-2285.65 562.53,-2285.65 562.53,-2225.65 120.53,-2225.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2245.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">legal_entity_id    </text>
|
||||
<text text-anchor="start" x="451.97" y="-2246.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="512.44" y="-2246.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.33" y="-2246.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2165.65 120.53,-2225.65 562.53,-2225.65 562.53,-2165.65 120.53,-2165.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2165.65 120.53,-2225.65 562.53,-2225.65 562.53,-2165.65 120.53,-2165.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2185.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">alias    </text>
|
||||
<text text-anchor="start" x="405.77" y="-2186.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="512.44" y="-2186.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.33" y="-2186.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2105.65 120.53,-2165.65 562.53,-2165.65 562.53,-2105.65 120.53,-2105.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2105.65 120.53,-2165.65 562.53,-2165.65 562.53,-2105.65 120.53,-2105.65"/>
|
||||
<text text-anchor="start" x="131.24" y="-2125.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">normalized_alias    </text>
|
||||
<text text-anchor="start" x="406.15" y="-2126.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="512.82" y="-2126.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="521.72" y="-2126.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-2045.65 120.53,-2105.65 562.53,-2105.65 562.53,-2045.65 120.53,-2045.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-2045.65 120.53,-2105.65 562.53,-2105.65 562.53,-2045.65 120.53,-2045.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2065.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_by    </text>
|
||||
<text text-anchor="start" x="491.07" y="-2066.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="120.53,-1985.65 120.53,-2045.65 562.53,-2045.65 562.53,-1985.65 120.53,-1985.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="120.53,-1985.65 120.53,-2045.65 562.53,-2045.65 562.53,-1985.65 120.53,-1985.65"/>
|
||||
<text text-anchor="start" x="131.53" y="-2005.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_at    </text>
|
||||
<text text-anchor="start" x="379.08" y="-2006.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="119.53,-1984.65 119.53,-2406.65 563.53,-2406.65 563.53,-1984.65 119.53,-1984.65"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
@ -505,165 +505,169 @@
|
||||
<!-- legal_entity_alias->app_user -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>legal_entity_alias:e->app_user:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M563.53,-2046.65C715.65,-2046.65 663.49,-1523.5 683.07,-1372.65 689.31,-1324.56 688.5,-975.31 719.07,-937.65 922.01,-687.62 1169.03,-940.34 1413.44,-730.65 1441.81,-706.31 1417.86,-674.64 1449.44,-654.65 1693.23,-500.38 1876.21,-470.83 2098.57,-654.65 2446.09,-941.93 1845.73,-1647.86 2280.71,-1658.53"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M563.53,-2075.65C722,-2075.65 663.23,-1529.87 683.07,-1372.65 689.14,-1324.53 688.5,-975.31 719.07,-937.65 922.01,-687.62 1169.03,-940.34 1413.44,-730.65 1441.81,-706.31 1417.86,-674.64 1449.44,-654.65 1693.23,-500.38 1876.21,-470.83 2098.57,-654.65 2446.09,-941.93 1845.73,-1647.86 2280.71,-1658.53"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="2280.8,-1662.03 2290.85,-1658.65 2280.89,-1655.03 2280.8,-1662.03"/>
|
||||
<text text-anchor="middle" x="2281.95" y="-1668.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="569.75" y="-2056.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<text text-anchor="middle" x="569.75" y="-2085.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
<!-- legal_entity_alias->legal_entity -->
|
||||
<!-- legal_entity_alias->legal_entity -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>legal_entity_alias:e->legal_entity:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M563.53,-2226.65C849.35,-2226.65 574.78,-2793.55 846.91,-2807.39"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="847.17,-2810.9 857.25,-2807.65 847.34,-2803.9 847.17,-2810.9"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M563.53,-2255.65C837.91,-2255.65 585.92,-2793.72 846.93,-2807.39"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="847.17,-2810.89 857.25,-2807.65 847.35,-2803.9 847.17,-2810.89"/>
|
||||
<text text-anchor="middle" x="866.15" y="-2778.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="569.75" y="-2236.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<text text-anchor="middle" x="569.75" y="-2265.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
<!-- related_group -->
|
||||
<g id="related_group" class="node">
|
||||
<title>related_group</title>
|
||||
<g id="a_related_group"><a xlink:title="related_group ГСК/ГСЗ — current-state. Иерархия групп не поддерживается (ТЗ 6.1)">
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="1066.25" cy="-3392.65" rx="305.44" ry="299.63"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="852.25,-3542.65 852.25,-3602.65 1280.25,-3602.65 1280.25,-3542.65 852.25,-3542.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3542.65 852.25,-3602.65 1280.25,-3602.65 1280.25,-3542.65 852.25,-3542.65"/>
|
||||
<text text-anchor="start" x="905.31" y="-3563.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3482.65 852.25,-3542.65 1280.25,-3542.65 1280.25,-3482.65 852.25,-3482.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3482.65 852.25,-3542.65 1280.25,-3542.65 1280.25,-3482.65 852.25,-3482.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3503.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="888.14" y="-3503.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="1208.79" y="-3503.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3422.65 852.25,-3482.65 1280.25,-3482.65 1280.25,-3422.65 852.25,-3422.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3422.65 852.25,-3482.65 1280.25,-3482.65 1280.25,-3422.65 852.25,-3422.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3442.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">code    </text>
|
||||
<text text-anchor="start" x="1123.49" y="-3443.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="1230.16" y="-3443.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.06" y="-3443.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3362.65 852.25,-3422.65 1280.25,-3422.65 1280.25,-3362.65 852.25,-3362.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3362.65 852.25,-3422.65 1280.25,-3422.65 1280.25,-3362.65 852.25,-3362.65"/>
|
||||
<text text-anchor="start" x="862.93" y="-3382.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">company_group_id    </text>
|
||||
<text text-anchor="start" x="1169.98" y="-3383.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="1230.44" y="-3383.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.34" y="-3383.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3302.65 852.25,-3362.65 1280.25,-3362.65 1280.25,-3302.65 852.25,-3302.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3302.65 852.25,-3362.65 1280.25,-3362.65 1280.25,-3302.65 852.25,-3302.65"/>
|
||||
<text text-anchor="start" x="863.13" y="-3322.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">kind    </text>
|
||||
<text text-anchor="start" x="956.78" y="-3323.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">related_group_kind</text>
|
||||
<text text-anchor="start" x="1230.64" y="-3323.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.53" y="-3323.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3242.65 852.25,-3302.65 1280.25,-3302.65 1280.25,-3242.65 852.25,-3242.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3242.65 852.25,-3302.65 1280.25,-3302.65 1280.25,-3242.65 852.25,-3242.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3262.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">name    </text>
|
||||
<text text-anchor="start" x="1123.49" y="-3263.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="1230.16" y="-3263.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.06" y="-3263.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3182.65 852.25,-3242.65 1280.25,-3242.65 1280.25,-3182.65 852.25,-3182.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3182.65 852.25,-3242.65 1280.25,-3242.65 1280.25,-3182.65 852.25,-3182.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3202.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_date    </text>
|
||||
<text text-anchor="start" x="1207" y="-3203.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">date</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="851.25,-3181.65 851.25,-3603.65 1281.25,-3603.65 1281.25,-3181.65 851.25,-3181.65"/>
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="1066.25" cy="-3434.65" rx="305.44" ry="342.48"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="852.25,-3614.65 852.25,-3674.65 1280.25,-3674.65 1280.25,-3614.65 852.25,-3614.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3614.65 852.25,-3674.65 1280.25,-3674.65 1280.25,-3614.65 852.25,-3614.65"/>
|
||||
<text text-anchor="start" x="905.31" y="-3635.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3554.65 852.25,-3614.65 1280.25,-3614.65 1280.25,-3554.65 852.25,-3554.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3554.65 852.25,-3614.65 1280.25,-3614.65 1280.25,-3554.65 852.25,-3554.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3575.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="888.14" y="-3575.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="1208.79" y="-3575.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3494.65 852.25,-3554.65 1280.25,-3554.65 1280.25,-3494.65 852.25,-3494.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3494.65 852.25,-3554.65 1280.25,-3554.65 1280.25,-3494.65 852.25,-3494.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3514.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">code    </text>
|
||||
<text text-anchor="start" x="1123.49" y="-3515.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="1230.16" y="-3515.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.06" y="-3515.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3434.65 852.25,-3494.65 1280.25,-3494.65 1280.25,-3434.65 852.25,-3434.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3434.65 852.25,-3494.65 1280.25,-3494.65 1280.25,-3434.65 852.25,-3434.65"/>
|
||||
<text text-anchor="start" x="862.93" y="-3454.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">company_group_id    </text>
|
||||
<text text-anchor="start" x="1169.98" y="-3455.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="1230.44" y="-3455.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.34" y="-3455.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3374.65 852.25,-3434.65 1280.25,-3434.65 1280.25,-3374.65 852.25,-3374.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3374.65 852.25,-3434.65 1280.25,-3434.65 1280.25,-3374.65 852.25,-3374.65"/>
|
||||
<text text-anchor="start" x="863.13" y="-3394.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">kind    </text>
|
||||
<text text-anchor="start" x="956.78" y="-3395.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">related_group_kind</text>
|
||||
<text text-anchor="start" x="1230.64" y="-3395.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.53" y="-3395.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3314.65 852.25,-3374.65 1280.25,-3374.65 1280.25,-3314.65 852.25,-3314.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3314.65 852.25,-3374.65 1280.25,-3374.65 1280.25,-3314.65 852.25,-3314.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3334.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">name    </text>
|
||||
<text text-anchor="start" x="1123.49" y="-3335.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<text text-anchor="start" x="1230.16" y="-3335.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="1239.06" y="-3335.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3254.65 852.25,-3314.65 1280.25,-3314.65 1280.25,-3254.65 852.25,-3254.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3254.65 852.25,-3314.65 1280.25,-3314.65 1280.25,-3254.65 852.25,-3254.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3274.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_date    </text>
|
||||
<text text-anchor="start" x="1207" y="-3275.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">date</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="852.25,-3194.65 852.25,-3254.65 1280.25,-3254.65 1280.25,-3194.65 852.25,-3194.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="852.25,-3194.65 852.25,-3254.65 1280.25,-3254.65 1280.25,-3194.65 852.25,-3194.65"/>
|
||||
<text text-anchor="start" x="863.25" y="-3214.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">crm_id    </text>
|
||||
<text text-anchor="start" x="1162.58" y="-3215.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">varchar</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="851.25,-3193.65 851.25,-3675.65 1281.25,-3675.65 1281.25,-3193.65 851.25,-3193.65"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
<!-- related_group->related_group_kind -->
|
||||
<g id="edge34" class="edge">
|
||||
<title>related_group:e->related_group_kind:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M1281.25,-3332.65C1426.15,-3332.65 1418.11,-3496.65 1563.01,-3496.65"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M1281.25,-3404.65C1427.51,-3404.65 1416.75,-3574.65 1563.01,-3574.65"/>
|
||||
</g>
|
||||
<!-- related_group->company_group -->
|
||||
<!-- related_group->company_group -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>related_group:e->company_group:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M1281.25,-3392.65C1430.63,-3392.65 1363.85,-3224.55 1413.44,-3083.65 1484.91,-2880.64 1332.31,-2618.26 1535.72,-2609.86"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M1281.25,-3464.65C1460.49,-3464.65 1359.91,-3254.7 1413.44,-3083.65 1477.73,-2878.25 1332.08,-2618.18 1535.72,-2609.86"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="1536.08,-2613.35 1546.01,-2609.65 1535.94,-2606.35 1536.08,-2613.35"/>
|
||||
<text text-anchor="middle" x="1554.9" y="-2619.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="1287.48" y="-3402.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<text text-anchor="middle" x="1287.48" y="-3474.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
<!-- related_group_member -->
|
||||
<g id="related_group_member" class="node">
|
||||
<title>related_group_member</title>
|
||||
<g id="a_related_group_member"><a xlink:title="related_group_member Состав ГСК/ГСЗ — SCD2. Исключение = закрытие версии (valid_to, is_current=false)">
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="341.53" cy="-3332.65" rx="341.57" ry="384.83"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="102.53,-3542.65 102.53,-3602.65 581.53,-3602.65 581.53,-3542.65 102.53,-3542.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3542.65 102.53,-3602.65 581.53,-3602.65 581.53,-3542.65 102.53,-3542.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3563.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group_member       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3482.65 102.53,-3542.65 581.53,-3542.65 581.53,-3482.65 102.53,-3482.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3482.65 102.53,-3542.65 581.53,-3542.65 581.53,-3482.65 102.53,-3482.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3503.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="138.42" y="-3503.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="510.07" y="-3503.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3422.65 102.53,-3482.65 581.53,-3482.65 581.53,-3422.65 102.53,-3422.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3422.65 102.53,-3482.65 581.53,-3482.65 581.53,-3422.65 102.53,-3422.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3442.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">related_group_id    </text>
|
||||
<text text-anchor="start" x="470.97" y="-3443.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="531.44" y="-3443.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3443.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3362.65 102.53,-3422.65 581.53,-3422.65 581.53,-3362.65 102.53,-3362.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3362.65 102.53,-3422.65 581.53,-3422.65 581.53,-3362.65 102.53,-3362.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3382.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">legal_entity_id    </text>
|
||||
<text text-anchor="start" x="470.97" y="-3383.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="531.44" y="-3383.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3383.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3302.65 102.53,-3362.65 581.53,-3362.65 581.53,-3302.65 102.53,-3302.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3302.65 102.53,-3362.65 581.53,-3362.65 581.53,-3302.65 102.53,-3302.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3322.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">kind    </text>
|
||||
<text text-anchor="start" x="257.58" y="-3323.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">related_group_kind</text>
|
||||
<text text-anchor="start" x="531.44" y="-3323.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3323.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3242.65 102.53,-3302.65 581.53,-3302.65 581.53,-3242.65 102.53,-3242.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3242.65 102.53,-3302.65 581.53,-3302.65 581.53,-3242.65 102.53,-3242.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3262.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">valid_from    </text>
|
||||
<text text-anchor="start" x="358.99" y="-3263.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<text text-anchor="start" x="531.44" y="-3263.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3263.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3182.65 102.53,-3242.65 581.53,-3242.65 581.53,-3182.65 102.53,-3182.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3182.65 102.53,-3242.65 581.53,-3242.65 581.53,-3182.65 102.53,-3182.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3202.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">valid_to    </text>
|
||||
<text text-anchor="start" x="398.08" y="-3203.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3122.65 102.53,-3182.65 581.53,-3182.65 581.53,-3122.65 102.53,-3122.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3122.65 102.53,-3182.65 581.53,-3182.65 581.53,-3122.65 102.53,-3122.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3142.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">is_current    </text>
|
||||
<text text-anchor="start" x="417.61" y="-3143.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">boolean</text>
|
||||
<text text-anchor="start" x="531.44" y="-3143.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3143.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3062.65 102.53,-3122.65 581.53,-3122.65 581.53,-3062.65 102.53,-3062.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3062.65 102.53,-3122.65 581.53,-3122.65 581.53,-3062.65 102.53,-3062.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3082.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_by    </text>
|
||||
<text text-anchor="start" x="510.07" y="-3083.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="101.03,-3061.65 101.03,-3603.65 582.03,-3603.65 582.03,-3061.65 101.03,-3061.65"/>
|
||||
<ellipse fill="none" stroke="black" stroke-width="0" cx="341.53" cy="-3404.65" rx="341.57" ry="384.83"/>
|
||||
<polygon fill="#1d71b8" stroke="transparent" points="102.53,-3614.65 102.53,-3674.65 581.53,-3674.65 581.53,-3614.65 102.53,-3614.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3614.65 102.53,-3674.65 581.53,-3674.65 581.53,-3614.65 102.53,-3614.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3635.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#ffffff">       related_group_member       </text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3554.65 102.53,-3614.65 581.53,-3614.65 581.53,-3554.65 102.53,-3554.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3554.65 102.53,-3614.65 581.53,-3614.65 581.53,-3554.65 102.53,-3554.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3575.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">id</text>
|
||||
<text text-anchor="start" x="138.42" y="-3575.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">    </text>
|
||||
<text text-anchor="start" x="510.07" y="-3575.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3494.65 102.53,-3554.65 581.53,-3554.65 581.53,-3494.65 102.53,-3494.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3494.65 102.53,-3554.65 581.53,-3554.65 581.53,-3494.65 102.53,-3494.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3514.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">related_group_id    </text>
|
||||
<text text-anchor="start" x="470.97" y="-3515.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="531.44" y="-3515.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3515.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3434.65 102.53,-3494.65 581.53,-3494.65 581.53,-3434.65 102.53,-3434.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3434.65 102.53,-3494.65 581.53,-3494.65 581.53,-3434.65 102.53,-3434.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3454.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">legal_entity_id    </text>
|
||||
<text text-anchor="start" x="470.97" y="-3455.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<text text-anchor="start" x="531.44" y="-3455.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3455.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3374.65 102.53,-3434.65 581.53,-3434.65 581.53,-3374.65 102.53,-3374.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3374.65 102.53,-3434.65 581.53,-3434.65 581.53,-3374.65 102.53,-3374.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3394.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">kind    </text>
|
||||
<text text-anchor="start" x="257.58" y="-3395.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">related_group_kind</text>
|
||||
<text text-anchor="start" x="531.44" y="-3395.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3395.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3314.65 102.53,-3374.65 581.53,-3374.65 581.53,-3314.65 102.53,-3314.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3314.65 102.53,-3374.65 581.53,-3374.65 581.53,-3314.65 102.53,-3314.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3334.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">valid_from    </text>
|
||||
<text text-anchor="start" x="358.99" y="-3335.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<text text-anchor="start" x="531.44" y="-3335.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3335.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3254.65 102.53,-3314.65 581.53,-3314.65 581.53,-3254.65 102.53,-3254.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3254.65 102.53,-3314.65 581.53,-3314.65 581.53,-3254.65 102.53,-3254.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3274.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">valid_to    </text>
|
||||
<text text-anchor="start" x="398.08" y="-3275.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">timestamptz</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3194.65 102.53,-3254.65 581.53,-3254.65 581.53,-3194.65 102.53,-3194.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3194.65 102.53,-3254.65 581.53,-3254.65 581.53,-3194.65 102.53,-3194.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3214.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">is_current    </text>
|
||||
<text text-anchor="start" x="417.61" y="-3215.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">boolean</text>
|
||||
<text text-anchor="start" x="531.44" y="-3215.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c"> </text>
|
||||
<text text-anchor="start" x="540.33" y="-3215.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="32.00" fill="#29235c">(!)</text>
|
||||
<polygon fill="#e7e2dd" stroke="transparent" points="102.53,-3134.65 102.53,-3194.65 581.53,-3194.65 581.53,-3134.65 102.53,-3134.65"/>
|
||||
<polygon fill="none" stroke="#29235c" points="102.53,-3134.65 102.53,-3194.65 581.53,-3194.65 581.53,-3134.65 102.53,-3134.65"/>
|
||||
<text text-anchor="start" x="113.53" y="-3154.85" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">created_by    </text>
|
||||
<text text-anchor="start" x="510.07" y="-3155.85" font-family="Helvetica,sans-Serif" font-style="italic" font-size="32.00" fill="#29235c">uuid</text>
|
||||
<polygon fill="none" stroke="#29235c" stroke-width="2" points="101.03,-3133.65 101.03,-3675.65 582.03,-3675.65 582.03,-3133.65 101.03,-3133.65"/>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
<!-- related_group_member->related_group_kind -->
|
||||
<g id="edge35" class="edge">
|
||||
<title>related_group_member:e->related_group_kind:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3332.65C757.4,-3332.65 575.56,-3601.73 719.07,-3701.65 782.38,-3745.74 1344.67,-3736.61 1413.44,-3701.65 1513.98,-3650.55 1450.22,-3496.65 1563.01,-3496.65"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3404.65C762.41,-3404.65 571.97,-3682.11 719.07,-3785.65 782.16,-3830.06 1344.91,-3821.08 1413.44,-3785.65 1515.56,-3732.87 1448.06,-3574.65 1563.01,-3574.65"/>
|
||||
</g>
|
||||
<!-- related_group_member->app_user -->
|
||||
<!-- related_group_member->app_user -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>related_group_member:e->app_user:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3092.65C721.45,-3092.65 670.07,-2613.96 683.07,-2475.65 686.68,-2437.17 691.38,-1111.63 719.07,-1084.65 940.2,-869.16 1859.27,-1055.53 2098.57,-1250.65 2251.2,-1375.1 2096.04,-1648.81 2280.61,-1658.39"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3164.65C730.89,-3164.65 669.99,-2652.43 683.07,-2504.65 686.54,-2465.35 690.81,-1112.2 719.07,-1084.65 940.15,-869.1 1859.27,-1055.53 2098.57,-1250.65 2251.2,-1375.1 2096.04,-1648.81 2280.61,-1658.39"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="2280.76,-1661.9 2290.85,-1658.65 2280.94,-1654.9 2280.76,-1661.9"/>
|
||||
<text text-anchor="middle" x="2299.74" y="-1668.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3102.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3174.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
<!-- related_group_member->legal_entity -->
|
||||
<!-- related_group_member->legal_entity -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>related_group_member:e->legal_entity:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3392.65C732.67,-3392.65 655.1,-3219.48 719.07,-3083.65 776.03,-2962.69 722.91,-2815.35 847.18,-2807.94"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="847.36,-2811.44 857.25,-2807.65 847.16,-2804.44 847.36,-2811.44"/>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3464.65C762.41,-3464.65 648.33,-3249.04 719.07,-3083.65 771.64,-2960.72 722.69,-2815.25 847.17,-2807.94"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="847.36,-2811.43 857.25,-2807.65 847.16,-2804.44 847.36,-2811.43"/>
|
||||
<text text-anchor="middle" x="848.36" y="-2817.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3402.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3474.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
<!-- related_group_member->related_group -->
|
||||
<!-- related_group_member->related_group -->
|
||||
<g id="edge26" class="edge">
|
||||
<title>related_group_member:e->related_group:w</title>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3452.65C701.44,-3452.65 727.44,-3509.3 841.09,-3512.51"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="841.21,-3516.01 851.25,-3512.65 841.3,-3509.01 841.21,-3516.01"/>
|
||||
<text text-anchor="middle" x="860.15" y="-3522.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3462.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
<path fill="none" stroke="#29235c" stroke-width="3" d="M582.53,-3524.65C701.44,-3524.65 727.44,-3581.3 841.09,-3584.51"/>
|
||||
<polygon fill="#29235c" stroke="#29235c" stroke-width="3" points="841.21,-3588.01 851.25,-3584.65 841.3,-3581.01 841.21,-3588.01"/>
|
||||
<text text-anchor="middle" x="860.15" y="-3594.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">1</text>
|
||||
<text text-anchor="middle" x="588.75" y="-3534.25" font-family="Helvetica,sans-Serif" font-size="32.00" fill="#29235c">*</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 471 KiB After Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 554 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 91 KiB |
204
model/entities-v5.19.md
Normal file
@ -0,0 +1,204 @@
|
||||
# АРМ КА — описание сущностей модели данных v5.19
|
||||
|
||||
Источник истины по структуре — `data-model-v5.19.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась.
|
||||
|
||||
## Что изменилось в v5.19 (решения владельца 17.10.2026, Р-1…Р-19; RSHB/arm-ka-docs#116)
|
||||
|
||||
Change `openspec/changes/unified-statement-normalization`: формы ББ/ОФР нормализуются общим кодом сервисов, контракт
|
||||
результата v2.0; ядро хранит результат как пришёл (`context/05-domain-rules.md` §11, §9.3, §9.5).
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 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`) нужны экрану, чтобы показать строку и предупреждение у места |
|
||||
| 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` сервиса сохраняются, кроме снимаемых правкой |
|
||||
| 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)` в файле.
|
||||
|
||||
## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90)
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | `stored_file.consolidation_id` (`uuid`, nullable, FK → `consolidation`) + индекс `(legal_entity_id, consolidation_id)` | Файл загружается **в консолидацию** (период ГК) и становится основой её «рабочего стола» (полный состав стола — `context/05-domain-rules.md` §9.5 п. 11), общего для всех версий периода; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Панель «Первичные документы» в версии показывает рабочий стол консоли для ЮЛ плюс файлы с документами, привязанными к версиям консоли. Что использует конкретная версия — по-прежнему `consolidation_document`; заморозка после `VERIFIED` — привязки и `ex_snapshot`. Привязка файла к **версии** (`consolidation_file`) отвергнута: рабочие версии периода делят первичку, а новая версия после `VERIFIED` подхватывает подтверждённое (`openspec/changes/console-files/design.md`) |
|
||||
| 2 | Note `recognition_job.reply_deadline` | Без изменения структуры: срок ответа OCR — 20 мин (OQ-159) |
|
||||
|
||||
Остальные таблицы и enum — как в v5.17. Реализация — миграция `0006` в `arm-core` (backfill `consolidation_id` по `consolidation_document`).
|
||||
|
||||
## Что изменилось в v5.17 (решение владельца 07.10.2026, пересмотр OQ-31, RSHB/arm-ka-docs#53)
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | `legal_entity.kpp` и `legal_entity_history.kpp` (`varchar`, nullable) | КПП юридического лица нужен (макет: колонка «КПП» на экранах `50`, `61`, поле форм `51-02`, `79`; фронтенд: «в ручке получения ЮЛ не приходит КПП»). Формат — 9 символов: 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры, иначе `INVALID_KPP`. У ИП (`entity_type = ИП`) КПП запрещён. Для ЮЛ необязателен: существующие записи и импорт без колонки остаются валидными. Уникален по-прежнему только `inn` (КПП — атрибут; сопоставление контрагентов ОСВ по ИНН не меняется). Изменение КПП — обычная правка со снимком в `legal_entity_history`. Отменяет п. 2 v5.14 («КПП не вводится») |
|
||||
|
||||
Остальные таблицы и enum — как в v5.16. Реализация — миграция `0005` в `arm-core`.
|
||||
|
||||
## Что изменилось в v5.16 (Срез 4, решения владельца 06.10.2026)
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | Enum `counterparty_match_kind` (`INN`, `NAME`, `ALIAS`, `MANUAL`, `PROPAGATED`) и колонка `osv_entry.counterparty_match_kind` (nullable) | Способ сопоставления контрагента строки ОСВ хранится. `NULL` ⇔ `counterparty_id` `NULL`; `INN` / `NAME` / `ALIAS` — шаг автоматического резолва при маппинге (ИНН; точное имя; нормализованное имя ЮЛ или алиас); `MANUAL` — ручное сопоставление аналитиком; `PROPAGATED` — автоприменение по имени в версии (DOC-221278 п. 12–13). Вычислять на чтение нельзя: после ручного связывания появляется алиас, и ручное сопоставление неотличимо от `ALIAS`. Миграция `0004` заполняет колонку по уже смапленным строкам (ИНН без пробелов = `legal_entity.inn` → `INN`; `trim(name) = legal_entity.name` → `NAME`; остальные с `counterparty_id` → `ALIAS` — приближение: ручных сопоставлений до Среза 4 не было) |
|
||||
| 2 | Семантика `consolidation_document` | Без изменения структуры: строка создаётся при подтверждении документа (по документам ЮЛ — членов периметра версий «в работе») и при добавлении ЮЛ в периметр (подтверждённые подходящие документы подхватываются); удаляется при замене/удалении файла и исключении ЮЛ из периметра. Для одной (ЮЛ, `document_type`, `osv_account_code`, период, признак нарастающего) привязан один документ — самый поздний `confirmed_at` (D-5 Среза 4) |
|
||||
| 3 | Семантика `consolidation_lock` | Без изменения структуры: блокировка без истечения по времени — активна, пока её не снял владелец, администратор или не перехватил другой пользователь с `CONSOLIDATION_EDIT` по причине (OQ-119) |
|
||||
|
||||
Пробелы квартальной склейки и сама сводная ОСВ по-прежнему не хранятся (DECISION-5). Остальные таблицы и enum — как в v5.15 (43 таблицы; enum — 28). Реализация — миграция `0004` в `arm-core`.
|
||||
|
||||
## Что изменилось в v5.15 (Срез 2c, решения владельца 05.10.2026)
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | `osv_data_type` + `NU` (после `BU`) | Строки налогового учёта из ОСВ «БУ, НУ» хранятся в `osv_entry` как есть (OQ-45); контракт результата допускал `NU` с v1.0, ядро раньше такой строки принять не могло |
|
||||
| 2 | `app_user.email` — в нижнем регистре | Без изменения структуры: нормализация при записи (seed, администрирование), поиск по `lower(email)` сохраняется, уникальность — существующий UNIQUE по `email` (OQ-59). Expression-индекс `lower(email)` не вводится: сверка ORM↔DBML не умеет expression-индексы. Миграция `0003` приводит существующие email к нижнему регистру (при дублях по регистру — падает, слияние решает человек) |
|
||||
| 3 | Семантика `stored_file.status`, `recognition_job.group_schema` | Без изменения структуры: `PROCESSING` ставит inbox по ack сервиса, `PROCESSED`/`ERROR` — цикл маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); `group_schema` заполняет маппер; «Итого» в `osv_entry` не хранится (OQ-01) |
|
||||
| 4 | Индексы `osv_entry (parent_entry_id)`, `osv_entry (recognition_job_id)`, `stored_file (current_recognition_job_id)` | Индексы для удаления файла и обхода дерева строк: без них проверки FK (в том числе self-FK `parent_entry_id`) при `DELETE /files/{id}` и очистке файла перед повторным маппингом шли seq scan'ом. Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. Обычные (не unique) индексы; закрывает OQ-117 (I-1 финального ревью Среза 2c) |
|
||||
|
||||
Остальные таблицы и enum — как в v5.14 (43 таблицы, 27 enum). Реализация — миграция `0003` в `arm-core`.
|
||||
|
||||
## Что изменилось в v5.14 (Срез 1, решения владельца 04.10.2026)
|
||||
|
||||
| № | Изменение | Решение |
|
||||
|---|---|---|
|
||||
| 1 | `related_group_member.kind` + индекс `(legal_entity_id, kind) WHERE is_current` | ЮЛ состоит **одновременно в одной ГСК и одной ГСЗ** (но только в одной группе каждого вида). Отменяет ANL-030 / v5.8 («только в одной ГСК или ГСЗ»). `kind` — денормализация `related_group.kind` (равенство проверяет ядро при записи, тест на рассинхрон); индекс не выразить через join, триггер не выбран. Закрывает OQ-32 |
|
||||
| 2 | КПП | **Не вводится** *(отменено в v5.17: КПП введён)*. Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывал OQ-31 до v5.17) |
|
||||
| 3 | Семантика `company_group.crm_id`, `base_regional_office_id` | Без изменения структуры: `crm_id` = «iD ГК» выгрузки ЕЦП CRM, `base_regional_office_id` = «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила — `05-domain-rules.md` §13) |
|
||||
| 4 | `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | Журнал изменений ЮЛ (создание/изменение/закрытие/перевод, алиасы) и пользователей (создание/деактивация администратором) — ТЗ 8.2; в v5.13 для них не было действия (были `GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `USER_ROLE_CHANGE`) |
|
||||
|
||||
Остальные таблицы и enum — как в v5.13 (43 таблицы). Реализация — миграция `0002` в `arm-core`.
|
||||
|
||||
## Что изменилось в v5.13 (ревью 30.09–01.10)
|
||||
|
||||
| № | Замечание | Решение |
|
||||
|---|---|---|
|
||||
| 1 | Вместо RBAC «в коде» — роли **и** права | `permission` + `role_permission` возвращены; права только через роли, прямых назначений пользователю нет. Исключения — заведением роли |
|
||||
| 2 | `osv_entry.parent_entry_id` не нужен — храним только строки с контрагентами | **Отложено.** Открыт вопрос: хранить ли строки уровня ACCOUNT для сверки «Σ контрагентов = итог счёта»; аналитика «Парсинг ОСВ» п.11 пока говорит «все строки» |
|
||||
| 3 | `osv_group_schema` не особо нужна | Убрана. Схема шапки — `recognition_job.group_schema jsonb`; тип строки — `osv_entry.group_type` |
|
||||
| 4 | `financial_statement_value.needs_review` непонятен | Убран: производное от `confidence` / `is_recognized`, порог может меняться |
|
||||
| 5 | В `recognition_job` не нужны и `result`, и `raw_result` | Оставлен только `result_s3_object_id`. Сырой ответ ИС Сканирование — S3 `raw/{job_id}.json` по конвенции, в БД не регистрируется |
|
||||
| 6 | `stored_file.fields_modified` — что это | Счётчик «исправлено полей» с карточки документа (v5.7). Убран — `COUNT(is_modified)` по данным |
|
||||
| 6-бис | `document_type` не предусматривает несколько ОСВ или ББ+ОФР в одном файле | **Разделение файл / документ**: `stored_file` — физический файл, `document` — логическая единица (1:N). Чек-лист, проверки, подтверждение, FK разобранных данных, `consolidation_document`, `ex_snapshot_item` — на `document`. Замена (`superseded_by`) — файл целиком |
|
||||
| 7 | `file_type`: убрать Р7 | Убран. `xls/xlsx/xlsb → tabular-parser`, `pdf/png/jpg → scan-gateway` |
|
||||
|
||||
**Принципы модели (коротко):**
|
||||
- Первичка не редактируется: файлы и распознанные значения хранятся как пришли; правки аналитика — рядом (`is_modified`, `original_*`) и в аудите.
|
||||
- Производные консолидации в работе (сводная ОСВ, пары ВГО, шахматка) **не хранятся** — считаются запросом. Хранятся только ручные решения (корректировки, гудвилл, подтверждение КФО клиента).
|
||||
- При верификации всё замораживается в Единое хранилище (ЕХ) как самодостаточный срез, который не зависит от таблиц консолидации.
|
||||
- Единственный писатель в БД — ядро `arm-core`; сервисы распознавания видят только NATS и S3.
|
||||
- Права — данные (`role_permission`), ограничения по ЗО/филиалу/статусу — policy-слой поверх прав.
|
||||
|
||||
---
|
||||
|
||||
## Часть 01 — Группы компаний, организации, филиалы
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `regional_office` | Филиал банка: головной офис (ГО, `is_head_office`) и региональные филиалы (РФ). Определяет зону видимости пользователя и закрепление ГК. | ← `company_group.base_regional_office_id`, `legal_entity.regional_office_id`, `app_user.regional_office_id` | ТЗ 8.1 |
|
||||
| `company_group` | Группа компаний (ГК) — верхний уровень справочника клиентов. Текущее состояние (SCD4). `crm_id` = «iD ГК» из ЕЦП CRM (NULL, если создана вручную Куратором; v5.14 — семантика по выгрузке), внутренний `code`, закрепление за филиалом (`base_regional_office_id` = «Базовый РФ ГК в CRM»), флаг архива. | → `regional_office`; ← `legal_entity`, `related_group`, `consolidation` | ТЗ 6.1, Прил. 2; макет |
|
||||
| `company_group_history` | Прошлые снимки ГК: каждое изменение — новая строка с `valid_from/valid_to`. | → `company_group` | ТЗ 6.10, 8.2 |
|
||||
| `legal_entity` | Юридическое лицо / ИП. Натуральный ключ — ИНН; **v5.17:** `kpp` — КПП (9 символов, nullable, только у ЮЛ, не ключ). `company_group_id` = текущая ГК; **NULL = монозаёмщик**. `close_date` — закрыто/архивировано. | → `company_group`, `regional_office`; ← `related_group_member`, `stored_file`, `document`, `osv_entry.counterparty_id`, `consolidation_member` | ТЗ 6.1, Прил. 2; ЕХ 6.1 |
|
||||
| `legal_entity_history` | Прошлые снимки ЮЛ + вид события (создание, изменение, переход в другую ГК, закрытие, вывод из ГК) и дата вступления в силу. | → `legal_entity` | ТЗ 8.2; макет |
|
||||
| `legal_entity_alias` | Словарь написаний контрагента: как имя встречается в ОСВ и нормализованный ключ для матчинга. Пополняется при ручном сопоставлении. | → `legal_entity` | ТЗ 6.5 |
|
||||
| `related_group` | ГСК или ГСЗ внутри ГК; `kind` различает. Для ГСК формируется КФО. Иерархия не поддерживается. **v5.19:** `crm_id` — ID группы в ЕЦП CRM (вручную, уникален). | → `company_group`; ← `related_group_member`, `consolidation.related_group_id` | ТЗ 6.1, Прил. 2 |
|
||||
| `related_group_member` | Состав ГСК/ГСЗ — история членства (SCD2). **v5.14:** ЮЛ может одновременно входить в **одну ГСК и одну ГСЗ** (не более одной группы каждого вида): колонка `kind` = `related_group.kind` (денормализация) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current`. Решение владельца 04.10.2026, отменяет ANL-030. | → `related_group`, `legal_entity` | ТЗ 6.1; макет [79] |
|
||||
|
||||
## Часть 02 — Пользователи, роли, права
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `app_user` | Пользователь АРМ. Аутентификация внешняя (РАИСА/SSO, `external_id`). Филиал (ГО/РФ), зона ответственности (ДКБ/ДМСБ/ДМ), табельный номер. Видимость данных = права роли + филиал + ЗО. | → `regional_office`; ← `user_role`, все `*_by` поля | ТЗ 8.1, Прил. 2 |
|
||||
| `role` | Роли: seed из ТЗ 8.1 (КА РФ, КА ГО, РКА, КМ, Куратор ГО, Эксперт, Администратор; `is_system`) + роли, заведённые администратором для исключений. **v5.13:** код роли в бизнес-логике не проверяется — только права. | ← `user_role`, `role_permission` | ТЗ 8.1 |
|
||||
| `permission` | **v5.13.** Атомарное право «объект × действие»: `DIRECTORY/DOCUMENT/CONSOLIDATION/KFO/EX/EXPORT/USERS/AUDIT` × `VIEW/EDIT/APPROVE/VERIFY/DELETE/EXPORT/ADMIN`. Seed, привязан к коду — администратор новые коды не создаёт. Право говорит «может»; policy-слой (ЗО, филиал, статус КФО) — «эту ли и сейчас ли». | ← `role_permission` | ТЗ Прил. 2, сноска 29 |
|
||||
| `role_permission` | **v5.13.** Матрица доступов как данные: `effective_permissions(user) = ∪ permissions(roles(user))`. Редактируется администратором на экране «Изменение ролей и прав доступа»; история — `audit_event(ROLE_PERMISSION_CHANGE)`. Прямых прав у пользователя нет. | → `role`, `permission` | ТЗ Прил. 2; макет |
|
||||
| `user_role` | Назначение ролей пользователю (M:N). История — в аудите. | → `app_user`, `role` | ТЗ 8.1 |
|
||||
|
||||
## Часть 03 — Бухгалтерские справочники и правила
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `chart_of_account` | План счетов с иерархией, тип A/P/AP, признак забалансового. Seed — полный план 1С; нестандартные субсчета клиентов добавляет администратор. | ← `osv_entry.account_id`, `elimination_rule_account`, `required_document_rule` | ТЗ 6.2/6.5, Прил. 3/4 |
|
||||
| `elimination_rule` | Правило элиминации из Прил. 3 (ББ) / Прил. 4 (ОФР) как данные: пара статей, сторона, мера, режим, НДС, акцизы. Версионируется. | ← `elimination_rule_account`, `correction.rule_id`; версия фиксируется в `consolidation_version` | ТЗ 6.4, Прил. 3/4 |
|
||||
| `elimination_rule_account` | Счета/субсчета каждой стороны правила, включая исключения («76 кроме 76.7.1»). | → `elimination_rule`, `chart_of_account` | ТЗ Прил. 3/4 |
|
||||
| `required_document_rule` | Какие документы обязательны по подходу. **v5.13:** сверяется с `document` (тип + счёт + период), а не с файлом: выполнено = есть `document CONFIRMED` у каждого ЮЛ периметра. | → `chart_of_account` | ТЗ 5.1, 6.2, сноска 12 |
|
||||
|
||||
## Часть 04 — Файлы, документы, S3, задания распознавания
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 |
|
||||
| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен (основа «рабочего стола консоли», общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `consolidation`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 |
|
||||
| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. **v5.19:** `file_ordinal` — номер документа в результате; формы ББ/ОФР одного типа в файле могут повторяться (ОСВ по счёту, расшифровка, прочий — один на тип); предупреждения и нарушения — `document_issue`. | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `document_issue`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») |
|
||||
| `recognition_job` | Одна попытка распознать/распарсить файл. **Единственная точка обмена с NATS** (outbox/inbox, дедуп по `result_msg_id`, DLQ). Хранит сервис и версию, **одну** ссылку на нормализованный JSON-результат в S3 (v5.13), метрики, и **схему группировок шапки ОСВ по листам** (`group_schema jsonb`, v5.13 — вместо `osv_group_schema`). **v5.19:** `statements_version` — версия копии общего кода нормализации, которой посчитан результат форм. | → `stored_file`, `s3_object`; ← `osv_entry`, `financial_statement_meta` | ТЗ 6.2, 6.10; DECISION-4 |
|
||||
|
||||
## Часть 05 — Разобранные ОСВ
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `osv_entry` | Строка ОСВ: документ (счёт + период), владелец, тип строки по схеме шапки (`group_type`, v5.13), уровень и родитель (`parent_entry_id` — **открыт вопрос, замечание 2**), код счёта и резолв в план счетов, сырое имя/ИНН контрагента, резолв в ЮЛ и способ сопоставления (`counterparty_match_kind`, v5.16), шесть сумм, тип данных (БУ/валюта/кол-во), «подакцизный» для 90-го. Ручные правки: `is_modified`, `original_values`, `modified_by`. Признак ГСК/ГК/Внешний не хранится — выводится из периметра версии; способ сопоставления контрагента хранится (`NULL` ⇔ контрагент не сопоставлен). | → `document`, `recognition_job`, `legal_entity` ×2, `chart_of_account` | ТЗ 6.2/6.5, Прил. 2; «Парсинг ОСВ» |
|
||||
|
||||
## Часть 06 — Распознанные формы ББ/ОФР
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `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_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_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) |
|
||||
|
||||
## Часть 07 — Консолидация
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `consolidation` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). **v5.18:** файлы, загруженные в консолидацию, — `stored_file.consolidation_id` (основа рабочего стола, общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11). | → `company_group`, `related_group`; ← `consolidation_version`, `stored_file` | ТЗ 6.10, 7.2 |
|
||||
| `consolidation_version` | Версия консолидации: номер, статус, сценарий, подход, гудвилл, инициатор и его офис (маршрут), кто утвердил / верифицировал, решение УОБ, отклонения от Методики, версия правил элиминации, ссылка на предыдущую. После VERIFIED — read-only. | → `consolidation`, `app_user`, `regional_office`, self; ← всё в частях 07–08 | ТЗ 5.1, 6.4, 6.10; макет |
|
||||
| `consolidation_status_history` | Каждый переход статуса: из → в, кто, когда, комментарий (обязателен при возврате). | → `consolidation_version`, `app_user` | ТЗ 6.10, 8.2 |
|
||||
| `consolidation_lock` | «Взять в работу»: кто редактирует версию, перехват с причиной (v5.16: без истечения по времени, OQ-119). Из макета — ANL-036. | → `consolidation_version`, `app_user` | макет |
|
||||
| `consolidation_member` | Периметр версии — зафиксированный список ЮЛ; `is_head_entity` для гудвилла. | → `consolidation_version`, `legal_entity` | ТЗ 6.1, 6.10 |
|
||||
| `consolidation_document` | **v5.13:** какие **документы** (не файлы) участвуют в версии. Чек-лист = `required_document_rule` × периметр − `consolidation_document`. Переиспользование одного документа в нескольких версиях/периодах, монозаёмщики из ЕХ. | → `consolidation_version`, `document` | ТЗ Прил. 2; ЕХ 7 |
|
||||
|
||||
*Не хранятся (DECISION-5), считаются запросом по версии:* сводная ОСВ, признак контрагента ГСК/ГК/Внешний, пары ВГО, шахматка, результирующая таблица ВГО.
|
||||
|
||||
## Часть 08 — Корректировки, гудвилл, результат КФО
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `correction` | Ручная корректировка ВГО: пара (правило + компания 1 + компания 2 — естественный ключ), ББ/ОФР, строки и суммы по Дт и Кт, дочерние строки для распределения, причина, обязательный комментарий, статус, номер версии (история — в аудите). КФО = свод − Σ корректировок. | → `consolidation_version`, `elimination_rule`, `legal_entity` ×2, `chart_of_account` ×2, self | ТЗ 6.4, 6.8, Прил. 9 |
|
||||
| `goodwill` | Строка расчёта гудвилла: головная × дочерняя, доля владения, ФВ по 58.01, УК (1310) и добавочный капитал (1350 — вопрос методологам), гудвилл, «включить в КФО», комментарий. | → `consolidation_version`, `legal_entity` ×2, `app_user` | ТЗ 6.7, Прил. 3 п.2, Прил. 8 |
|
||||
| `kfo_result` | Шапка результата КФО: когда посчитан, заморожен ли, ЧА и ВБ, выручка, прибыль, расхождения, пороги, обоснование, итог гудвилла. | → `consolidation_version` (1:1); ← `kfo_result_line` | ТЗ 6.4, 6.8, Прил. 10 |
|
||||
| `kfo_result_line` | Строка консолидированного ББ/ОФР по коду ФСБУ 4/2023: сумма → авто-элиминация → корректировки → гудвилл → итог; «в т.ч. ВГО», источник, расхождение, комментарий. | → `kfo_result`; ← `kfo_result_line_company` | ТЗ 6.4, Прил. 10; макет |
|
||||
| `kfo_result_line_company` | Разрез строки КФО по компаниям периметра: сумма и доля. | → `kfo_result_line`, `legal_entity` | ЕХ 6.6; макет |
|
||||
| `client_kfo_line` | Сценарий «подтверждение КФО клиента»: по строке — значение и ВГО от клиента (ручной ввод), значение и ВГО банка, расхождение, причина. | → `consolidation_version`, `app_user` | ТЗ 5.1, 6.10; макет |
|
||||
|
||||
## Часть 09 — Комментарии, аудит, Единое хранилище
|
||||
|
||||
| Сущность | Что это | Ключевые связи | Откуда |
|
||||
|---|---|---|---|
|
||||
| `comment` | Полиморфный комментарий к объекту (документу, ячейке шахматки по ключу пары, строке КФО). | → `app_user`; `object_type + object_id` без FK | ТЗ 6.3/6.4; макет |
|
||||
| `audit_event` | Журнал действий (append-only): кто, когда, действие, раздел, объект, версия консолидации, before/after, результат, IP/браузер. Покрывает историю правок распознанных данных, корректировок, выгрузок, импорта справочников, входов, **изменений матрицы прав** (v5.13), подтверждения документов, **изменений ЮЛ и пользователей** (v5.14: `LEGAL_ENTITY_CHANGE`, `USER_CHANGE`). | → `app_user`, `consolidation_version` | ТЗ 8.2, Прил. 2; макет |
|
||||
| `ex_snapshot` | Срез ЕХ: одна ГСК на одну отчётную дату, версия, статус (актуальный/архив), когда и кем верифицирован и записан. Самодостаточен — на консолидацию ссылается только данными. | → `related_group`, `company_group`, `app_user`, self; ← `member`, `item` | ЕХ ТЗ 6.2, 6.4, 5.3 |
|
||||
| `ex_snapshot_member` | Периметр среза на дату: ЮЛ с ИНН и наименованием на момент верификации. | → `ex_snapshot`, `legal_entity` | ЕХ ТЗ 6.3 |
|
||||
| `ex_snapshot_item` | Содержимое среза. Два вида: исходный документ → ссылка на **`document`** (v5.13; скачать — через его файл) и всё производное → `payload` jsonb. | → `ex_snapshot`, `document`, `legal_entity` | ЕХ ТЗ 5.2, 6.7 |
|
||||
|
||||
---
|
||||
|
||||
## Как читать модель по сценарию «сформировать КФО»
|
||||
|
||||
1. **Справочники** (01–03): ГК → ЮЛ → ГСК/ГСЗ; план счетов; правила элиминации; что обязательно загрузить.
|
||||
2. **Создание консолидации** (07): `consolidation` от ГК + период → `consolidation_version` → `consolidation_member` (периметр).
|
||||
3. **Загрузка** (04): `stored_file` + `s3_object` → `recognition_job` (queued → NATS → сервис → результат в S3 → done) → ядро создаёт **`document`** на каждый ББ / ОФР / ОСВ по счёту, найденный в файле.
|
||||
4. **Разбор** (05–06): ОСВ → `osv_entry`; ББ/ОФР → `financial_statement_meta/column/line/value`. Аналитик проверяет, правит, подтверждает **документ**; логические проверки — на лету; сопоставляет контрагентов (→ `osv_entry.counterparty_id`, `legal_entity_alias`). Чек-лист закрывается по `document CONFIRMED`.
|
||||
5. **Консолидация** (вычисляется): сводная ОСВ, пары ВГО, шахматка — из `osv_entry` × периметр × правила. Аналитик вносит `correction`, заполняет `goodwill`, при сценарии подтверждения — `client_kfo_line`. Результат — `kfo_result(_line)`.
|
||||
6. **Согласование** (07): `consolidation_status_history` по маршруту; блокировка `consolidation_lock`. Что можно — решает `role_permission` + policy-слой по статусу/ЗО.
|
||||
7. **Верификация → ЕХ** (09): `ex_snapshot` + `member` + `item` (документы по ссылке, остальное — payload). Версия → read-only. Всё это время — `audit_event`.
|
||||
|
||||
## Открытые вопросы после v5.14
|
||||
|
||||
- **Замечание 2** (`osv_entry.parent_entry_id`, какие строки ОСВ хранить) — отложено; затрагивает и аналитику «Парсинг ОСВ» п.11.
|
||||
- Замена по-документно (одна ОСВ из файла с пятью) — сейчас не предусмотрена, замена файлом целиком. Если понадобится — `superseded_by` переезжает в `document`.
|
||||
- Статус `document.RECOGNIZED` (ручная проверка не требуется по порогу) — подтвердить с аналитиками, нужен ли, или все документы проходят через `NEEDS_CHECK → CONFIRMED`.
|
||||
@ -746,7 +746,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
одна запись.
|
||||
- **Подтверждение документа** (Р-13, Р-18): порядок проверок 409 по §9.5 п. 4: `PERIOD_REQUIRED` → `LINES_UNRESOLVED`
|
||||
(есть строки `unreadable` / `missing` с непустым `amount`; строки без сумм не мешают) → `DUPLICATE_FORM` (в файле уже
|
||||
подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §12 п. 3)
|
||||
подтверждена другая форма того же типа и периода; 409 со ссылкой на неё; между файлами работает автозамена §9.5 п. 3)
|
||||
→ жёсткие нарушения без акцепта. Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не
|
||||
допускает). `detail` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8.
|
||||
- Правка даты отчёта (PATCH шапки): для колонок с `period_kind = relative:<вид>` ядро досчитывает `period_end` общим
|
||||
@ -850,7 +850,7 @@ line_name, line_name_confidence, parent_ref, values}`: `line_code` — разо
|
||||
само разлепление — предупреждение `info`.
|
||||
- **Р-18** (ядро) — при двух и более формах одного типа и периода в одном файле ни одна не автоподтверждается;
|
||||
подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` только внутри файла (между файлами работает
|
||||
автозамена §12 п. 3).
|
||||
автозамена §9.5 п. 3).
|
||||
- **Р-19** (ядро) — `RECOGNIZED` не зависит от статуса конверта: достаточно `document.status = done` и отсутствия
|
||||
предупреждений `error` уровня файла (потерянный json или таблица ИС блокируют, `needs_check` соседнего документа
|
||||
из-за одной ячейки — нет).
|
||||
|
||||
114
openspec/changes/unified-statement-normalization/proposal.md
Normal file
@ -0,0 +1,114 @@
|
||||
## Why
|
||||
|
||||
Формы ББ/ОФР сегодня нормализуются двумя разными конвейерами: scan-gateway (PDF → ИС Сканирование) и tabular-parser
|
||||
(Excel). У них разные правила раскладки таблицы, отбора строк, разбора сумм, проверок §7 и статуса; ядро не доверяет
|
||||
ни одному из них и пересчитывает проверки и статус заново по косвенным признакам, дважды дедуплицирует строки и держит
|
||||
третью и четвёртую копии словарей (§7 и «код → наименование»). Пользователь видит последствия как отдельные «ошибки
|
||||
распознавания» — AURORA-1406, 1462, 1511, 1517, 1519, 1522, 1523, 1524, 1634: строки молча пропадают, повтор кода
|
||||
теряет сумму, «чистая» ячейка с потерянной цифрой не подсвечивается, предупреждения сервисов нигде не видны.
|
||||
|
||||
Дизайн `design.md` (финальная версия, решения владельца Р-1…Р-19 от 17.10.2026, три раунда ревью) приводит оба
|
||||
сервиса к одной цепочке шагов 1–9 с единым выходом каждого шага и общим кодом шагов 2–8; ядро хранит результат как
|
||||
пришёл и пересчитывает его тем же общим кодом только после правок аналитика.
|
||||
|
||||
Срез: вне плана срезов `09-delivery-plan.md` — переработка Среза 2b (Excel-формы), Среза 3a (OCR-нормализатор) и
|
||||
Среза 3b (формы в ядре). Issue RSHB/arm-ka-docs#116.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Общий код нормализации** — новый репозиторий `arm-statements`, пакет `arm_statements/` (Р-1): типы шага 1
|
||||
(`FormCandidate`, `Cell`), шаги 2–8 (раскладка, отбор строк, значения, код и наименование, сборка документа,
|
||||
самопроверки §7, статус и `needs_review`), словари §7 (полный и упрощённый наборы), периодов, заголовков форм,
|
||||
подписей колонок, упорядоченные перечни кодов форм по редакциям и вариантам, диапазоны кодов. Словаря «код →
|
||||
наименование» в пакете нет (Р-3). В tabular-parser, scan-gateway и arm-core пакет лежит копией, которую раскладывает
|
||||
и проверяет скрипт `sync-statements` репозитория docs (по образцу `sync-web.sh`).
|
||||
- **Строки не выбрасываются молча:** статусы строк `ok` | `unreadable` | `missing` | `detail`, `row_index`, `ref`,
|
||||
сырой текст кода и ячейки (`text`) доезжают до БД и экрана; повтор кода — законный случай, все строки сохраняются
|
||||
(Р-4, Р-8). **BREAKING** для потребителей: `line_code` становится nullable.
|
||||
- **Качество структурно и с привязкой к месту:** `needs_review` у значения, строки и полей шапки; «подтверждение
|
||||
проверкой» §7 вместо порога уверенности (Р-5, Р-7, Р-11, Р-12, Р-16, Р-17); `document.status` у каждого документа;
|
||||
предупреждения — структуры `{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`.
|
||||
- **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`,
|
||||
`related_group.crm_id` по arm-core#112); «агрегат формы по коду» вместо «одна строка на код»; приём 1.x и 2.0 на
|
||||
время перехода; новое условие `RECOGNIZED`; PATCH `…/statement` с `lines`, `columns`, `values[].confirmed`; 409
|
||||
`LINES_UNRESOLVED` и `DUPLICATE_FORM` при подтверждении.
|
||||
- **arm-web:** экран проверки формы — строки со статусами на своих местах, расшифровки под переключателем, подсветка
|
||||
только по `needs_review`, предупреждения и нарушения у места, назначение кода, «Это расшифровка», подтверждение кода
|
||||
и ячейки, дата колонки.
|
||||
- **Документы:** `05-domain-rules.md` — §11 становится общей главой нормализации ББ/ОФР с подразделами по
|
||||
источникам, §12 — заголовок-редирект; §7 — «посчитанное» соотношение, `abs_terms` и знак, упрощённый набор; §9.3,
|
||||
§9.5 — статус, маппинг и подтверждение формы по v2.0. `07-open-questions.md` — закрыты OQ-36, OQ-141 (Р-8) и
|
||||
вопросы, снятые решениями дизайна; новые вопросы — склейка половинок формы (Р-15) и открытое в дизайне.
|
||||
`frontend-api-guide.md` — новые поля формы и PATCH (после реализации).
|
||||
|
||||
Читают и пишут контракт: `arm-core` (читает результат и конверт, публикует запрос), `tabular-parser` и `scan-gateway`
|
||||
(читают запрос, пишут результат и конверт). Дочерние change'и (заводятся при планировании, по одному на репозиторий):
|
||||
`arm-statements` (новый), RSHB/arm-core, RSHB/arm-tabular-parser, RSHB/arm-scan-gateway, RSHB/arm-web; связанная
|
||||
задача — RSHB/arm-core#112 (`related_group.crm_id` входит в ту же миграцию, Р-10).
|
||||
|
||||
### Порядок выкладки — «сначала ядро» (обязателен)
|
||||
|
||||
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, экранные поля
|
||||
API. До этого шага ни один сервис не шлёт 2.0: первый же `line_code = null` дал бы файлу `ERROR`.
|
||||
4. arm-web: экран по новым полям (поля 1.x-документов заполнены по умолчанию — экран работает и на них).
|
||||
5. tabular-parser и scan-gateway переходят на 2.0 в любом порядке относительно друг друга.
|
||||
6. После перехода обоих сервисов маппер 1.x удаляется из ядра отдельным PR (не в этом change).
|
||||
|
||||
Копии схем в сервисах (`api/contracts/`, тест идентичности в `make check`) после слияния контракта v2.0 в docs
|
||||
устаревают: каждый сервис обновляет копию в своём PR шага 5, ядро — в PR шага 3.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Разбор ОСВ (`classify.py`, `to_money` ОСВ, правила §1) не меняется; путь ОСВ в tabular-parser меняется только в
|
||||
структуре предупреждений и `document.status`.
|
||||
- Переключатель «как в файле» (порядок строк по `row_index`) на экране — не в этой работе (Р-14).
|
||||
- Склейка двух половинок формы из одного файла в ядре не предоставляется — файл перезагружают; вопрос в
|
||||
`07-open-questions.md` (Р-15). Склейка страниц из разных файлов (OQ-140) — тоже нет.
|
||||
- Восстановление строк по наименованию (OQ-149) и сопоставление формы без колонки кодов по именам строк (OQ-139) — нет.
|
||||
- Запрос `recognition-request` остаётся v1.0; subject'ы NATS, `Nats-Msg-Id`, ack и повторы не меняются по механике.
|
||||
- Межформенная проверка `1370 − 1370(нач. года) = 2400` остаётся в ядре и мягкой; формулы §7 по редакциям (OQ-146) и
|
||||
знаки 2410/2420 (OQ-12) не пересматриваются.
|
||||
- Замена пакета-копии на зависимость из Nexus платформы — не сейчас (платформа Python-пакеты не собирает, Р-1).
|
||||
- Приведение единиц измерения сверх числового формата ячейки (OQ-138) — нет.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `statement-normalization`: общая цепочка нормализации ББ/ОФР (шаги 1–8) — пакет `arm_statements`, его типы,
|
||||
правила и словари, `sync-statements`.
|
||||
- `tabular-recognition-service`: адаптер Excel шага 1 в tabular-parser и что остаётся в сервисе (чтение книги, листы
|
||||
ОСВ, сборка конверта v2.0).
|
||||
- `scan-gateway`: адаптер json ИС шага 1 в scan-gateway и что остаётся в сервисе (горячие папки, склейка набора json,
|
||||
`raw/`, сборка конверта v2.0).
|
||||
- `arm-core`: хранение результата v2.0 (DBML v5.19), агрегат по коду, `document_issue`, условие `RECOGNIZED`, PATCH
|
||||
формы, порядок 409 подтверждения, приём 1.x и 2.0.
|
||||
- `arm-web`: экран проверки формы ББ/ОФР по новым полям.
|
||||
|
||||
### Modified Capabilities
|
||||
- `recognition-contract`: файл результата и конверт — v2.0; правила перехода (ядро принимает 1.x и 2.0, «сначала
|
||||
ядро»); статус и структурные предупреждения у всех документов, включая ОСВ.
|
||||
|
||||
## Impact
|
||||
|
||||
- Новый репозиторий `arm-statements`; скрипт `sync-statements` и цель Makefile в docs (вызывается из `make check-full`
|
||||
потребителей, не из `make check`).
|
||||
- tabular-parser: `parser/statements/*` (кроме разбора книги и адаптера шага 1) уходит в общий код; словари
|
||||
`statement_checks.yaml`, `statement_periods.yaml`, `statement_headers.yaml` переезжают в пакет; путь ОСВ — структура
|
||||
предупреждений и `document.status`.
|
||||
- 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` остаётся только словарём имён.
|
||||
- arm-web: `widgets/DocumentReview` (экран [41]), типы API.
|
||||
- Решение v5.13 «`needs_review` считается, не хранится» пересматривается (design, Decisions № 17).
|
||||
- Риск окна выкладки: пока сервис шлёт 1.x, документы его конвейера получают статус по прежнему правилу D-6 и поля
|
||||
v2.0 по умолчанию — экран и подтверждение работают, новых сигналов качества нет.
|
||||
@ -0,0 +1,202 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### 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` — по-прежнему один документ на тип (и счёт) на файл.
|
||||
|
||||
#### Scenario: Повтор кода сохраняется
|
||||
- **WHEN** в результате две строки `ok` с кодом 2410 (пустая и с `(649)`)
|
||||
- **THEN** в БД две строки 2410 с разными `row_index` и `source_ref`, обе со своими значениями
|
||||
|
||||
#### Scenario: Строка без кода
|
||||
- **WHEN** в результате строка `unreadable` с `line_code_text = "ИЧ,250"` и суммой
|
||||
- **THEN** строка хранится с `line_code = null`, `line_status = unreadable`, `line_code_text = "ИЧ,250"`
|
||||
|
||||
#### Scenario: Два баланса в одном файле
|
||||
- **WHEN** результат содержит два `BALANCE` (лист-продолжение другого типа или двойная печать)
|
||||
- **THEN** создаются два документа с разными `file_ordinal`; ни один не автоподтверждается (Р-18)
|
||||
|
||||
### Requirement: Агрегат формы по коду
|
||||
Все чтения формы «по коду» SHALL идти через одну чистую функцию «агрегат формы по коду» без хранения (DECISION-5):
|
||||
для кода и колонки — сумма `amount` строк `ok` с этим кодом (`null`, если ни одной непустой), список `line_id`,
|
||||
справочное имя. На неё переводятся межформенная проверка `1370 / 2400`, суммы формы для консолидации и выгрузок,
|
||||
`parent_code` подстрок, раскладка строк на экране, правка ячеек по `line_code`, метрики `fields_*`. Строки `detail`,
|
||||
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ MUST NOT попадать.
|
||||
|
||||
#### Scenario: Межформенная проверка по агрегату
|
||||
- **WHEN** в ББ две строки 1370 (сумма 500 и 300 на отчётную дату)
|
||||
- **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).
|
||||
|
||||
#### Scenario: Ячейка соседнего документа
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check`, предупреждений файла уровня `error` нет, ИНН совпал, период —
|
||||
30.09.2025
|
||||
- **THEN** ББ `RECOGNIZED` и автоподтверждается, ОФР — `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Потерянный json
|
||||
- **WHEN** все документы `done`, но в `metrics.warnings` файла `JSON_FAILED` уровня `error`
|
||||
- **THEN** все документы формы файла `NEEDS_CHECK`
|
||||
|
||||
#### Scenario: Подмена неуверенного кода на соседа
|
||||
- **WHEN** строка с кодом 1120 (уверенность 50) и наименованием «Нематериальные активы»
|
||||
- **THEN** наименование не совпало со справочным «Результаты исследований и разработок», документ `NEEDS_CHECK`,
|
||||
`NAME_MISMATCH` у строки
|
||||
|
||||
#### Scenario: Период не конец квартала
|
||||
- **WHEN** документ `done` с датой отчёта 15.08.2025
|
||||
- **THEN** квартал не определён, документ `NEEDS_CHECK` (как в D-6) и не автоподтверждается; ручное подтверждение —
|
||||
409 `PERIOD_REQUIRED`, пока аналитик не поправит дату отчёта
|
||||
|
||||
### 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` сервиса снимаются
|
||||
только перезагрузкой файла.
|
||||
|
||||
#### Scenario: Дата колонке
|
||||
- **WHEN** у колонки предупреждение `COLUMN_WITHOUT_DATE`, аналитик ставит ей `period_date` через PATCH
|
||||
- **THEN** предупреждение удаляется, статус пересчитывается
|
||||
|
||||
#### Scenario: Неснимаемое предупреждение
|
||||
- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения
|
||||
- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует)
|
||||
|
||||
### 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`. Акцепт сбрасывается любой правкой, как и
|
||||
прежде.
|
||||
|
||||
#### Scenario: Подтверждение ячейки без правки
|
||||
- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы
|
||||
- **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`);
|
||||
- `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`); `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`).
|
||||
|
||||
#### Scenario: Назначение кода строке-мусору (AURORA-1462)
|
||||
- **WHEN** аналитик назначает строке `unreadable` «ИЧ,250» код 1250
|
||||
- **THEN** строка `ok`, `line_code = 1250`, `line_code_confidence = 100`, `line_code_modified = true`,
|
||||
`line_code_original_value = null`; §7 и статус пересчитаны
|
||||
|
||||
#### Scenario: Отметка расшифровкой
|
||||
- **WHEN** аналитик отмечает строку `missing` с суммой как расшифровку
|
||||
- **THEN** `line_status = detail`, строка выходит из сумм и не блокирует подтверждение
|
||||
|
||||
#### Scenario: Код занят шаблонной строкой с суммами
|
||||
- **WHEN** аналитик назначает строке `unreadable` код 1250, а строка 1250 создана правкой аналитика и имеет сумму
|
||||
- **THEN** 409 `LINE_EXISTS`, ничего не изменено
|
||||
|
||||
### 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.
|
||||
|
||||
#### Scenario: Неразобранная строка с суммой
|
||||
- **WHEN** у формы строка `missing` с суммой и одновременно жёсткое нарушение
|
||||
- **THEN** 409 `LINES_UNRESOLVED` с этой строкой в `detail.lines`
|
||||
|
||||
#### 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` не заменяются шаблонной строкой молча.
|
||||
|
||||
#### Scenario: Код есть только у строки unreadable
|
||||
- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250»
|
||||
- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом
|
||||
|
||||
### 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` — после родителя.
|
||||
|
||||
#### Scenario: Документ, принятый по 1.x
|
||||
- **WHEN** форма загружена до перехода сервиса (результат 1.2)
|
||||
- **THEN** у строк `line_status = ok`, `row_index` по порядку, `source_ref = null`, у значений `text = string_value`,
|
||||
`issues` содержат прежние предупреждения маппинга, экран работает
|
||||
|
||||
### 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`.
|
||||
|
||||
#### Scenario: Цикл миграций
|
||||
- **WHEN** `alembic upgrade head` → `downgrade base` → `upgrade head` на чистой БД
|
||||
- **THEN** все три шага проходят, сверка ORM с DBML v5.19 — без расхождений
|
||||
|
||||
### Requirement: `crm_id` ГСК/ГСЗ
|
||||
`related_group.crm_id` (`varchar`, nullable, уникален) SHALL сохраняться в `POST …/related-groups` и
|
||||
`PATCH /directories/related-groups/{id}` и отдаваться в `RelatedGroupOut` / `RelatedGroupDetail`; повтор — 409
|
||||
`CRM_ID_EXISTS`; изменение — аудит `GROUP_CHANGE` с `before/after` (arm-core#112). Импорт ЕЦП `crm_id` групп не
|
||||
заполняет (OQ-91).
|
||||
|
||||
#### Scenario: Занятый crm_id
|
||||
- **WHEN** создаётся ГСЗ с `crm_id`, который уже есть у другой ГСК или ГСЗ
|
||||
- **THEN** 409 `CRM_ID_EXISTS`, ничего не создано
|
||||
@ -0,0 +1,46 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Раскладка строк формы со статусами
|
||||
Экран проверки формы ББ/ОФР ([41]) SHALL сохранять раскладку «как на бумаге» (Р-14): разделы, внутри — строки `ok` по
|
||||
возрастанию кода, несколько строк одного кода — в порядке файла (`row_index`), шаблонные строки на своих местах,
|
||||
пятизначные подстроки под родителем. Строка `unreadable` / `missing` SHALL стоять под ближайшей предыдущей по
|
||||
`row_index` строкой, размещённой по коду, а без такой — в начале первого раздела; она подсвечена целиком, в ячейке
|
||||
кода серым — сырой текст `line_code_text` с подсказкой. Строки `detail` SHALL быть скрыты под переключателем
|
||||
«Показать расшифровки (N)» и показываться под родителем (`parent_row_index`) с отступом. Экран показывает справочное
|
||||
имя строки (`reference_name`), файловое (`line_name`) — рядом, если отличается. Переключателя «как в файле» нет.
|
||||
|
||||
#### Scenario: Код-мусор на своём месте
|
||||
- **WHEN** строка `unreadable` «ИЧ,250» в файле стоит после строки 1240
|
||||
- **THEN** на экране она стоит сразу под 1240, подсвечена, в ячейке кода серым «ИЧ,250»
|
||||
|
||||
#### Scenario: Расшифровки скрыты
|
||||
- **WHEN** у формы три строки `detail`
|
||||
- **THEN** по умолчанию их не видно, переключатель «Показать расшифровки (3)»; включённый показывает их под родителями
|
||||
|
||||
### 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` напрямую нет.
|
||||
|
||||
#### Scenario: Догадка подтверждена проверкой
|
||||
- **WHEN** значение `is_recognized = false`, `needs_review = false`
|
||||
- **THEN** ячейка не подсвечена; в подсказке сырой текст и уверенность
|
||||
|
||||
### Requirement: Правка строк, колонок и подтверждение в режиме «Редактировать»
|
||||
В режиме «Редактировать» экран SHALL давать: у `unreadable` / `missing` — поле ввода кода с подсказкой из справочника
|
||||
(код → справочное имя) и пункт меню строки «Это расшифровка»; у `detail` — «Назначить код»; у строки `ok` — «Сделать
|
||||
расшифровкой», смену кода и «Подтвердить код» у строки с `needs_review`; подтверждение ячейки без правки; ввод даты у
|
||||
колонки без даты (`period_kind = none`) или с предупреждением о дате. «Сохранить» отправляет один `PATCH …/statement`
|
||||
(`header`, `values`, `columns`, `lines`). Кнопка «Подтвердить данные» при строках `unreadable` / `missing` с суммами
|
||||
SHALL быть отключена с подсказкой «Разберите строки без кода: N» и переходом к первой такой строке; ответ сервера 409
|
||||
`LINES_UNRESOLVED` показывается тем же текстом, 409 `DUPLICATE_FORM` — со ссылкой на подтверждённую форму, 409
|
||||
`LINE_EXISTS` — у поля кода.
|
||||
|
||||
#### Scenario: Назначить код
|
||||
- **WHEN** аналитик вводит 1250 в ячейку кода строки `unreadable` и сохраняет
|
||||
- **THEN** уходит `PATCH` с `lines: [{line_id, line_code: "1250"}]`; после ответа строка встаёт на место 1250
|
||||
|
||||
#### Scenario: Подтверждение заблокировано
|
||||
- **WHEN** у формы две строки `missing` с суммами
|
||||
- **THEN** «Подтвердить данные» отключена, подсказка «Разберите строки без кода: 2», клик по подсказке ведёт к первой
|
||||
@ -0,0 +1,129 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Сообщение результата распознавания
|
||||
Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_SUBJECT`, по умолчанию `files.tabular_parsed`)
|
||||
SHALL соответствовать `docs/contracts/recognition-result-message.schema.json`
|
||||
v2.0: обязательные поля `schema_version` (`"2.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` |
|
||||
`error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и
|
||||
`result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key`
|
||||
(только `scan-gateway`) и `metrics`. `metrics.warnings[]` — структуры `warning` (как в файле результата), не строки.
|
||||
Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` — файл результата.
|
||||
Производитель v2.0 SHALL писать в конверт тот же MAJOR, что и в файл результата.
|
||||
|
||||
#### Scenario: Успешный результат
|
||||
- **WHEN** сервис завершил обработку файла
|
||||
- **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт
|
||||
со `status = done | needs_check`, `result_s3` указывает на этот файл, `result_schema_version = "2.0"`, заголовок
|
||||
`Nats-Msg-Id` = `result:<job_id>:<attempt>`
|
||||
|
||||
#### Scenario: Ошибка обработки
|
||||
- **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается)
|
||||
- **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`,
|
||||
`TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`,
|
||||
`RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать
|
||||
|
||||
#### Scenario: Предупреждения в конверте
|
||||
- **WHEN** у файла есть предупреждение уровня файла (ошибочный json набора ИС)
|
||||
- **THEN** `metrics.warnings` конверта содержит объект `{code: "JSON_FAILED", severity: "error", message, locator}`
|
||||
|
||||
### Requirement: Файл результата
|
||||
Файл результата 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`.
|
||||
`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`).
|
||||
|
||||
`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`,
|
||||
`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` |
|
||||
`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`
|
||||
уникален в документе.
|
||||
|
||||
#### Scenario: Результат валидируется примерами
|
||||
- **WHEN** CI сервиса запускает тесты контракта
|
||||
- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата v2.0, а все
|
||||
`request-*.json` / `result-message-*.json` — схемы сообщений
|
||||
|
||||
#### Scenario: Лишняя колонка сохранена
|
||||
- **WHEN** в ОСВ между ИНН и суммами есть колонка «КПП»
|
||||
- **THEN** у строк контрагентов `extra = {"КПП": "<значение>"}`, файл проходит схему v2.0
|
||||
|
||||
#### Scenario: Форма из Excel с проверками
|
||||
- **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`
|
||||
|
||||
#### Scenario: Форма из OCR с сомнительными значениями
|
||||
- **WHEN** scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка
|
||||
`unreadable` (`examples/result-statement-minimal.json`)
|
||||
- **THEN** файл проходит схему v2.0; у покрытого значения `is_recognized = false`, `needs_review = false`, `text` —
|
||||
сырой; у строки `unreadable` `line_code = null`, `line_code_text` — сырой текст; документ `needs_check`
|
||||
|
||||
#### Scenario: Границы периода ОСВ
|
||||
- **WHEN** в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.»
|
||||
- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с
|
||||
`status` и `warnings`, файл проходит схему v2.0
|
||||
|
||||
#### Scenario: Строка без кода с кодом в поле
|
||||
- **WHEN** у строки `line_status = unreadable` передан непустой `line_code`
|
||||
- **THEN** файл не проходит схему v2.0
|
||||
|
||||
### Requirement: Версионирование контракта
|
||||
Поле `schema_version` SHALL быть строкой `MAJOR.MINOR`. Добавление необязательных полей или значений enum —
|
||||
`MINOR` (обратно совместимо); удаление/переименование полей, смена типов или семантики — `MAJOR`.
|
||||
Сервис 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).
|
||||
|
||||
#### Scenario: Несовместимая версия запроса
|
||||
- **WHEN** сервис, поддерживающий запрос `1.x`, получает запрос `schema_version = "2.0"`
|
||||
- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на
|
||||
запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не
|
||||
будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого
|
||||
запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`). `attempt` для `Nats-Msg-Id` берётся из тела запроса; при его
|
||||
отсутствии реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает,
|
||||
`scan-gateway` подставляет 1) — см. OQ-71
|
||||
|
||||
#### Scenario: Ядро получает MAJOR, которого не знает
|
||||
- **WHEN** ядро, знающее `1.x` и `2.x`, получает конверт или файл результата `schema_version = "3.0"`
|
||||
- **THEN** результат не проходит проверку схемы, файл получает `ERROR` с причиной в `metrics.mapping_error`, повтор не
|
||||
выполняется (как для любого результата не по схеме)
|
||||
|
||||
## 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` удаляется после
|
||||
перехода обоих сервисов отдельным изменением.
|
||||
|
||||
#### Scenario: Сервис ещё на v1.2
|
||||
- **WHEN** после выкладки ядра tabular-parser присылает результат `1.2` с повтором кода и `checks.duplicate_line_codes`
|
||||
- **THEN** ядро принимает его маппером `1.x`, статус документа — по D-6, файл не `ERROR`
|
||||
|
||||
#### Scenario: Ack нового сервиса
|
||||
- **WHEN** scan-gateway после перехода публикует ack `processing` со `schema_version = "2.0"`
|
||||
- **THEN** ядро переводит задание в `processing`, как для ack `1.0`
|
||||
|
||||
#### Scenario: Порядок нарушен
|
||||
- **WHEN** сервис прислал результат `2.0` с `line_code = null` ядру, знающему только `1.x`
|
||||
- **THEN** результат не проходит проверку схемы, файл получает `ERROR` (`metrics.mapping_error`) — поэтому порядок
|
||||
«сначала ядро» обязателен
|
||||
@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Адаптер json ИС шага 1
|
||||
scan-gateway SHALL оставить у себя обмен с ИС Сканирование (горячие папки, опрос, срок, склейку набора json в
|
||||
`raw/{job_id}.json` до нормализации, очистку папки) и адаптер «таблица `table_structure[i]` json → `FormCandidate`»;
|
||||
шаги 2–8 SHALL выполняться копией пакета `arm_statements`. Адаптер SHALL: создавать кандидата на каждую таблицу каждого
|
||||
успешного json; копировать шапку json (`organization`, `inn`, `units`, `date`) в шапку каждой своей таблицы как `Cell`
|
||||
(`text = form_name`, `confidence` ИС); передавать `label.form_name` таблицы как заголовок в `evidence` (тип — общим
|
||||
правилом шага 1 по словарю заголовков, `statement_labels.yaml` уходит в словарь пакета); строить плотную сетку по
|
||||
`row_index` и `column_index` ИС (пустые позиции — `null`), `Cell.ref = row:<row_index>`; `locator =
|
||||
file:<имя json>;table:<i>`. `value` у OCR-ячеек всегда `null`.
|
||||
|
||||
#### Scenario: Один json — ББ и ОФР
|
||||
- **WHEN** json `aep_3.json` содержит две таблицы — «Бухгалтерский баланс» и «Отчет о финансовых результатах» — под
|
||||
общей шапкой
|
||||
- **THEN** два кандидата с `locator` `file:aep_3.json;table:0` и `…;table:1`, у каждого копия шапки json
|
||||
|
||||
### Requirement: Ошибки набора json
|
||||
Ошибочный json набора (`parse_error`, непустой `error.code`, корень не объект) при хотя бы одном успешном соседе SHALL
|
||||
давать предупреждение файла `JSON_FAILED` (`error`, `locator = file:<имя json>`, в `message` — код и сообщение ИС);
|
||||
все json ошибочны — конверт `error` с кодом первого. Исключение при разборе таблицы — предупреждение `TABLE_PARSE_ERROR`
|
||||
(`error`) документа-кандидата, остальные таблицы разбираются; ни одной таблицы не разобрано — `error
|
||||
RECOGNITION_FAILED`.
|
||||
|
||||
#### Scenario: Потерянный ОФР (решение № 28 среза 3a)
|
||||
- **WHEN** `<job_id>.json` успешен (ББ), а `<job_id>_ofr.json` вернул `INVALID_DOCUMENT`
|
||||
- **THEN** документ ББ получает свой статус; в `metrics.warnings` — `JSON_FAILED` уровня `error`; конверт
|
||||
`needs_check`; ядро не автоподтверждает документы файла
|
||||
|
||||
### Requirement: Что уходит из шлюза
|
||||
scan-gateway SHALL удалить `api/rules/statement_lines.yaml` и подстановку наименований по справочнику, срезание
|
||||
уверенности кода до 99 по справочнику, выбор редакции для имён (`_choose_edition`), пропуск строк с нечисловым и пустым
|
||||
кодом со счётчиками, правило «оставлена первая строка» при повторе кода, синтез шапки, правило «ячейки под колонкой
|
||||
нет → `confidence = 0`, не распознано», привязку `is_recognized` к `confidence` и правило «любое предупреждение →
|
||||
`needs_check`». Модули `normalizer/ocr/text|dates|amounts` переезжают в общие разборы пакета, `layout|rows|header|
|
||||
document` — в шаги 2–6 пакета; `ocr_lookalikes.yaml` — в словарь пакета.
|
||||
|
||||
#### Scenario: Строки без кода в aep_3
|
||||
- **WHEN** в `aep_3.json` строки «БАЛАНС» и «Выручка» имеют пустой код, а в колонке значения мусор `211(1 |`
|
||||
- **THEN** строки попадают в результат со статусами (`detail` под строкой с кодом, `missing` в начале таблицы) и сырым
|
||||
текстом; счётчиков «строк без кода с суммами пропущено» нет
|
||||
@ -0,0 +1,530 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Пакет общего кода `arm_statements` и его копии
|
||||
Общий код нормализации ББ/ОФР SHALL жить в репозитории `arm-statements` пакетом `arm_statements/` с `pyproject`,
|
||||
своими тестами и конфигом линтеров. Пакет SHALL содержать: типы шага 1 (`FormCandidate`, `Cell`; сериализуемые в
|
||||
json dataclass-ы), шаги 2–8, общие разборы текста (двойники, даты, суммы, коды), словари — проверок §7
|
||||
`statement_checks.yaml` (полный и упрощённый наборы), периодов `statement_periods.yaml`, заголовков форм и ОКУД,
|
||||
подписей колонок («Код», «Наименование», исключения), букв-двойников, упорядоченные перечни кодов форм по редакциям
|
||||
(`66n`, `fsbu4`) и вариантам (`full`, `simplified`) без наименований, диапазоны кодов — и загрузчик словарей. Словаря
|
||||
«код → наименование» в пакете 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`.
|
||||
|
||||
#### Scenario: Прямая правка копии
|
||||
- **WHEN** в `tabular-parser` изменён файл копии пакета, а источник в `arm-statements` — нет
|
||||
- **THEN** `sync-statements --check` завершается с кодом 20 и называет файл; `make check` сервиса этого не проверяет
|
||||
|
||||
#### Scenario: Версия общего кода в результате
|
||||
- **WHEN** сервис собирает результат копией пакета, разложенной из коммита `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` — плотная сетка строк × ячеек, все страницы и листы формы уже в одной сетке, пустая позиция —
|
||||
`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>`).
|
||||
|
||||
`type_hint` SHALL вычисляться одним правилом у обоих адаптеров: ОКУД по словарю (0710001 → `BALANCE`, 0710002 →
|
||||
`INCOME`, прочие 0710xxx → `DISCLOSURE`) → иначе заголовок по словарю заголовков (у OCR заголовок — `label` таблицы
|
||||
ИС; «Пояснения», «Расшифровка» → `DISCLOSURE`) → иначе `null`.
|
||||
|
||||
#### Scenario: Excel-ячейка с числовым форматом
|
||||
- **WHEN** в Excel ячейка хранит 106311116.46 с форматом `#,##0,`
|
||||
- **THEN** у `Cell` `text = "106 311"`, `value = 106311`, `confidence = 100`, `ref = "Баланс!D27"` (лист и адрес
|
||||
ячейки)
|
||||
|
||||
#### Scenario: OCR-ячейка
|
||||
- **WHEN** в таблице ИС ячейка строки `row_index = 8` содержит `"[190 :"` с уверенностью 25
|
||||
- **THEN** у `Cell` `text = "[190 :"`, `value = null`, `confidence = 25`, `ref = "row:8"`
|
||||
|
||||
#### Scenario: Тип по ОКУД против заголовка
|
||||
- **WHEN** у блока формы ОКУД 0710002, а заголовок «Бухгалтерский баланс»
|
||||
- **THEN** `type_hint = INCOME`; окончательный тип выбирается по кодам на шаге 6
|
||||
|
||||
### Requirement: Шаг 1 — шапка кандидата
|
||||
У каждого кандидата SHALL быть своя шапка. Текст каждого поля разбирается общим кодом по `Cell.text`: ИНН — только
|
||||
цифры, без замены двойников, длина 10 или 12, иначе `null`; дата отчёта — словарём периодов (тот же, что для подписей
|
||||
колонок, шаг 2); единицы — словарём единиц. Сырой текст и уверенность каждого поля SHALL доезжать до контракта
|
||||
(`<поле>_text`, `<поле>_confidence`).
|
||||
|
||||
#### Scenario: ИНН с мусором клеток
|
||||
- **WHEN** поле ИНН шапки OCR — `"3|1 21 2|"` с цифрами ИНН между вертикальными чертами
|
||||
- **THEN** ИНН собирается только из цифр, `|` единицей не считается; `inn_text` хранит исходный текст
|
||||
|
||||
### Requirement: Очистка OCR-текста — первая стадия типовых разборов
|
||||
Очистка OCR SHALL выполняться не отдельным шагом, а первой стадией разборов даты, подписи колонки, кода строки,
|
||||
разлепления склеенной строки и суммы — для любой ячейки без `value` (у Excel — только текстовые ячейки). Буквы-
|
||||
двойники SHALL заменяться в датах, подписях, кодах и суммах (Р-11): `О/о/O/o → 0`, `З/з → 3`, `б → 6`, `S/s → 5`,
|
||||
`I/l/|/і → 1`, `Ч → 4` — только вплотную к цифре, в несколько проходов; слова не портятся. В суммах крайние `|`
|
||||
(след границы таблицы) SHALL убираться, а не заменяться. ИНН двойники MUST NOT трогать. Сумма с заменой двойника
|
||||
получает `is_recognized = false`; код с заменой считается неуверенным. Текст длиннее 200 символов разбором даты не
|
||||
обрабатывается (`null`).
|
||||
|
||||
#### Scenario: Двойник в сумме
|
||||
- **WHEN** ячейка суммы OCR — `"1 2О5"` с уверенностью 100
|
||||
- **THEN** `amount = 1205`, `is_recognized = false`, `text = "1 2О5"`
|
||||
|
||||
#### Scenario: Двойник в коде
|
||||
- **WHEN** ячейка кода — `"I1О0"` с уверенностью 100
|
||||
- **THEN** код разбирается как `1100` и считается неуверенным (подтверждается структурой, шаг 8)
|
||||
|
||||
#### Scenario: Граница таблицы в сумме
|
||||
- **WHEN** ячейка суммы — `"| : 122477"`
|
||||
- **THEN** крайняя `|` удаляется, а не становится единицей; сумма 122477, `is_recognized = false`
|
||||
|
||||
### Requirement: Шаг 1 — многостраничная форма
|
||||
Одна форма SHALL быть одной сеткой на выходе шага 1. Строки продолжения дописываются в конец сетки по маркеру
|
||||
«Форма … с.2» или по позиции листа; граница листа записывается в `segment_breaks`. Тип из маркера продолжения SHALL
|
||||
сверяться с `type_hint` формы: расхождение — отдельный кандидат и предупреждение `CONTINUATION_TYPE_CONFLICT`
|
||||
(`error`). Лист без маркера и без шапки присоединяется, только если его коды в диапазоне открытой формы и, при трёх и
|
||||
более кодах, число колонок сумм совпадает с формой; иначе — отдельный кандидат и предупреждение
|
||||
`SHEET_NOT_JOINED` (`error`).
|
||||
|
||||
#### Scenario: Лист-продолжение с одной строкой
|
||||
- **WHEN** после листа ОФР идёт лист без шапки с единственной строкой 2400
|
||||
- **THEN** лист присоединяется к ОФР (код в диапазоне формы, кодов меньше трёх — число колонок не сверяется), граница
|
||||
листа — в `segment_breaks`
|
||||
|
||||
#### Scenario: Лист другой ширины с тремя кодами
|
||||
- **WHEN** лист без маркера и шапки содержит три кода ББ и четыре колонки сумм при трёх у открытой формы
|
||||
- **THEN** лист — отдельный кандидат, предупреждение `SHEET_NOT_JOINED` уровня `error`
|
||||
|
||||
### Requirement: Диапазоны кодов и разбор кода
|
||||
Словарь диапазонов кодов SHALL содержать отрезки: ББ 1100–1799, ОФР 2100–2999, отчёт об изменениях капитала
|
||||
3100–3699, отчёт о движении денежных средств 4100–4499, пояснения 5000–5999, отчёт о целевом использовании
|
||||
6100–6499. Кодовые диапазоны для поиска столбца — объединение отрезков. Разбор кода: убрать пробелы и переносы →
|
||||
заменить двойники (`|` → 1 только если результат попадает в кодовый диапазон) → убрать краевую пунктуацию → 4–5 цифр
|
||||
(допускается хвост `.0`), первые четыре — в кодовом диапазоне. Многострочная ячейка — код, только если каждая её часть
|
||||
код. «Диапазон столбца» — отрезок, в который попадает большинство кодов найденного столбца.
|
||||
|
||||
#### Scenario: Год и сумма — не код
|
||||
- **WHEN** в ячейке «2024» или четырёхзначная сумма «3450»
|
||||
- **THEN** кодом это не считается (вне отрезков)
|
||||
|
||||
#### Scenario: Краевая пунктуация и черта
|
||||
- **WHEN** ячейка кода — `"1220 ,"` или `"| 140"`
|
||||
- **THEN** коды `1220` и `1140`
|
||||
|
||||
### Requirement: Шаг 2 — столбец кодов
|
||||
Столбец кодов SHALL находиться одним правилом: (а) столбец с подписью из словаря подписей кода («код», «коды», «код
|
||||
строки», «код показателя» — после нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью
|
||||
доля кодов среди непустых ячеек ≥ 0,6 или кодов ≥ 3; при нескольких кандидатах — с наибольшим числом кодов; берётся
|
||||
последняя такая подпись над первой строкой с кодом; (б) иначе — столбец с наибольшим числом ячеек-кодов при кодах ≥ 3
|
||||
и доле ≥ 0,6; иначе предупреждение `CODE_COLUMN_NOT_FOUND`.
|
||||
|
||||
#### Scenario: Титульный блок 1С с подписью «Коды»
|
||||
- **WHEN** под подписью «Коды» титульного блока выгрузки 1С стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ 12267, ОКФС, ОКЕИ
|
||||
(один код из девяти, доля 0,11), а коды строк формы — в другом столбце
|
||||
- **THEN** столбец под «Коды» не выбирается; столбец кодов — по правилу (б)
|
||||
|
||||
#### Scenario: Документ из двух строк
|
||||
- **WHEN** форма содержит две строки с кодами под подписью «Код»
|
||||
- **THEN** столбец кодов найден (два из двух, доля 1,0)
|
||||
|
||||
### Requirement: Шаг 2 — сегменты, повторная шапка и границы листов
|
||||
Раскладка SHALL состоять из сегментов. Новый сегмент открывает строка-шапка (в ней есть ячейка из словаря подписей
|
||||
кода, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница
|
||||
листа из `segment_breaks`. У сегмента своя раскладка: по его шапке; без шапки — столбец кодов по правилу шага 2, а если
|
||||
сегмент не первый и кодов в нём меньше трёх — ослабленный поиск: столбец с наибольшим числом кодов из диапазона
|
||||
столбца первого сегмента, минимум один. Колонки значений сегмента без шапки — столбцы правее кода, где в строках с
|
||||
кодами есть числа; они сопоставляются с колонками первого сегмента по порядку; число не совпало — предупреждение
|
||||
`SEGMENT_COLUMNS_MISMATCH` (`error`) и значения по позиции. Столбец кодов не найден ни так, ни так — предупреждение
|
||||
`SEGMENT_WITHOUT_CODES` (`error`) сегмента; строки сегмента не теряются (роль `data`, статусы по шагу 3).
|
||||
|
||||
#### Scenario: Лист «_2» НПЗ со столбцом кодов в другой колонке
|
||||
- **WHEN** второй лист формы без шапки содержит единственную строку 2400, и код стоит не в той колонке, что на первом
|
||||
листе
|
||||
- **THEN** у сегмента свой столбец кодов (ослабленный поиск), значения строки 2400 не сдвинуты
|
||||
|
||||
#### Scenario: Повторная шапка на второй странице скана
|
||||
- **WHEN** в таблице ИС посередине стоит повторная строка «Наименование показателя | Код | На 30.09.2025 | …»
|
||||
- **THEN** с неё начинается новый сегмент со своей раскладкой; строка-шапка в данные не попадает
|
||||
|
||||
### Requirement: Шаг 2 — строка заголовка, нумерация и роли строк
|
||||
Строка заголовка SHALL быть последней над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе
|
||||
последней над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом);
|
||||
двухстрочный заголовок склеивается. Строка нумерации — строка, где не меньше трёх непустых ячеек, каждая — целое число,
|
||||
и числа идут подряд, начиная с 1, слева направо — SHALL получать роль `numbering` и в данные не попадать. Роль
|
||||
`title` — только строки выше найденной строки заголовка; в сегменте без заголовка роли `title` нет. Заголовка нет ни у
|
||||
одного сегмента документа → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,
|
||||
`label = ""`, `period_end = null`, `period_kind = none`, одно предупреждение `HEADER_ROW_NOT_FOUND` (`error`).
|
||||
Заголовок есть хотя бы у одного сегмента — остальные сегменты берут его колонки без предупреждения.
|
||||
|
||||
#### Scenario: Нумерация в скане (AURORA-1517)
|
||||
- **WHEN** под шапкой таблицы ИС строка «1 | 2 | 3 | 4 | 5»
|
||||
- **THEN** роль `numbering`, строки данных из неё нет
|
||||
|
||||
#### Scenario: Листы 2–5 ББ без шапки
|
||||
- **WHEN** у первого листа ББ заголовок есть, у листов 2–5 — нет
|
||||
- **THEN** листы 2–5 берут колонки первого, предупреждения о заголовке нет, документ может быть `done`
|
||||
|
||||
### 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`:
|
||||
- абсолютная подпись («На 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 декабря
|
||||
предыдущего года» → `relative:prev_year_end` (31.12 года эффективной даты − 1); «На 31 декабря года,
|
||||
предшествующего предыдущему» → `relative:prev_prev_year_end` (31.12 года − 2); «За аналогичный период предыдущего
|
||||
года» → `relative:prev_period` (эффективная дата − 1 год, 29 февраля → 28 февраля);
|
||||
- подписи нет → `none`.
|
||||
|
||||
Эффективная дата документа SHALL быть датой отчёта из шапки, иначе самой поздней абсолютной датой колонок; нет ни
|
||||
той, ни другой → у относительных колонок `period_end = null`, `period_kind` остаётся относительным.
|
||||
|
||||
Подпись из словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») SHALL пропускаться с
|
||||
предупреждением `COLUMN_SKIPPED` (`info`). Подпись, не разобранная ни как период, ни как исключение, при числах в
|
||||
столбце в строках с кодами SHALL оставлять колонку с подписью как есть, `period_end = null`, `period_kind = none` и
|
||||
предупреждением `COLUMN_WITHOUT_DATE` (`error`, `column_index`); без чисел — пропуск. Подпись OCR с уверенностью ниже
|
||||
100, разобранная в дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два
|
||||
года, 31 декабря предыдущего и предшествующего ему года; иначе предупреждение `COLUMN_DATE_UNCERTAIN` (`error`,
|
||||
`column_index`). Уверенность подписи — `label_confidence` (у Excel 100; подписи нет — `null`).
|
||||
|
||||
#### Scenario: Бланк 66н без впечатанных дат
|
||||
- **WHEN** подписи колонок ББ — «На отчетную дату», «На 31 декабря предыдущего года», «На 31 декабря года,
|
||||
предшествующего предыдущему», дата отчёта шапки 30.09.2025
|
||||
- **THEN** `period_end` колонок 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` уровня
|
||||
`error` с `column_index`, документ `needs_check`
|
||||
|
||||
#### Scenario: Колонка «Изменение»
|
||||
- **WHEN** правее периодов колонка «Изменение, %»
|
||||
- **THEN** колонка пропускается, предупреждение `COLUMN_SKIPPED` уровня `info`
|
||||
|
||||
### Requirement: Шаг 2 — столбец наименования
|
||||
Столбец наименования SHALL быть столбцом с подписью, начинающейся с «наименование» (после нормализации), в строке
|
||||
заголовка; иначе — столбцом левее кода с наибольшим числом текстов, кроме столбца «Пояснения». Наименование строки —
|
||||
ячейка этого столбца; пустая — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||||
|
||||
#### Scenario: Наименование без подписи
|
||||
- **WHEN** у формы нет подписи «Наименование показателя», а левее кода два столбца — «Пояснения» (короткие номера) и
|
||||
тексты статей
|
||||
- **THEN** наименование берётся из столбца текстов статей
|
||||
|
||||
### Requirement: Шаг 3 — строки данных и их статусы
|
||||
Шаг 3 SHALL выдавать `RawLine` на каждую строку сетки с ролью `data`, у которой есть содержимое: код, число или
|
||||
непустой текст в колонке значений с уверенностью ниже 100. У строки — `row_index` (порядковый номер в документе с 0,
|
||||
по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||||
ячейки строки), ячейка кода, `line_status`, ячейка наименования, ячейки значений по колонкам сегмента, `parent_ref`.
|
||||
Статусы:
|
||||
- `ok` — код разобран;
|
||||
- `unreadable` — в ячейке кода есть хотя бы одна цифра, но код не разобран; ячейка кода без цифр считается пустой;
|
||||
- `detail` — строка без кода с содержимым под строкой с кодом (в том же или в предыдущих сегментах документа);
|
||||
`parent_ref` — `ref` ближайшей строки с кодом выше;
|
||||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки с кодом; строки роли `title`
|
||||
сюда не попадают.
|
||||
|
||||
Строка без кода и без содержимого — не строка данных. Счётчиков отброшенных строк MUST NOT быть. Повтор кода — не
|
||||
статус и не предупреждение: все строки с кодом — `ok`, порядок как в файле.
|
||||
|
||||
#### Scenario: Код-мусор (AURORA-1462)
|
||||
- **WHEN** ячейка кода — `"ИЧ,250"`, в колонках суммы
|
||||
- **THEN** строка `unreadable` с `line_code_text = "ИЧ,250"`, суммами и `ref`; строка не пропадает
|
||||
|
||||
#### Scenario: Расшифровка «в том числе»
|
||||
- **WHEN** под строкой 1150 стоит строка «в том числе: Оборудование к установке» без кода с суммой
|
||||
- **THEN** строка `detail`, `parent_ref` = `ref` строки 1150
|
||||
|
||||
#### Scenario: Потерянный код в начале таблицы
|
||||
- **WHEN** первая строка данных «Выручка» без кода с суммами (код уехал в ячейку значения), выше строк с кодом нет
|
||||
- **THEN** строка `missing`
|
||||
|
||||
#### Scenario: Повтор кода (AURORA-1406)
|
||||
- **WHEN** код 2410 стоит у двух строк с разными наименованиями
|
||||
- **THEN** обе строки `ok`, порядок как в файле, предупреждения нет
|
||||
|
||||
### Requirement: Шаг 3 — разлепление склеенных строк
|
||||
До разбора кода SHALL выполняться разлепление: ячейка кода содержит N ≥ 2 частей через перевод строки, и каждая
|
||||
непустая ячейка значений — ровно N частей; разлепление делается, если хотя бы одна часть кода разобралась как код
|
||||
диапазона столбца. Получается N строк с `ref` `row:<i>#<k>`; нечитаемая часть кода — строка `unreadable`; наименование —
|
||||
k-я из непустых частей, если их тоже N, иначе целиком у каждой; `line_code_text` и `text` значений — k-е части. Значения разлепленных строк получают
|
||||
`is_recognized = false`; разлепление — предупреждение `ROW_SPLIT` (`info`). Число частей не совпало или ни одна часть
|
||||
не код — одна строка, многострочное значение → `amount = null`, `string_value` = текст, `is_recognized = false`.
|
||||
|
||||
#### Scenario: Склейка «1500\n1700» (OQ-156)
|
||||
- **WHEN** ячейка кода — `"1500\n1700"`, суммы — `"917 775\n2 371 333"`
|
||||
- **THEN** две строки `ok` (`row:i#1`, `row:i#2`) с суммами 917775 и 2371333, `is_recognized = false`, предупреждение
|
||||
`ROW_SPLIT`; при сошедшихся соотношениях §7 значения не подсвечиваются (Р-17)
|
||||
|
||||
#### Scenario: Часть не из диапазона
|
||||
- **WHEN** в ББ ячейка кода — `"44 30\nI |"`
|
||||
- **THEN** строка не разлепляется (4430 вне диапазона ББ)
|
||||
|
||||
### Requirement: Шаг 4 — значение ячейки
|
||||
Шаг 4 SHALL выдавать `StatementValue {column_index, amount, string_value, text, confidence, is_recognized}`: `text` —
|
||||
всегда сырой текст источника; `string_value` — только когда `amount = null` и текст непуст и не прочерк. Правила:
|
||||
- `value` — число: `amount` квантуется до 2 знаков half-up; больше 16 целых цифр → `amount = null`, `string_value` =
|
||||
текст, `is_recognized = false`, предупреждение `VALUE_OUT_OF_RANGE` (`info`);
|
||||
- `value` — дата или логическое → `amount = null`, `string_value` = текст, `is_recognized = false`;
|
||||
- `value` нет → `parse_amount(text, source)`: двойники (Р-11), пробелы и тонкие пробелы, минус «−» (U+2212) как знак,
|
||||
скобки → минус, `}` и `]` — закрывающая скобка; прочерк (`-`, `–`, `—`, `−` без цифр) и пусто → `amount = null`,
|
||||
`is_recognized = true`; разделители: у `ocr` запятая — тысячи при шаблоне `[+-]?[1-9]\d{0,2}(,\d{3})+`, точка —
|
||||
тысячи при `[+-]?[1-9]\d*(\.\d{3})+`, иначе десятичный разделитель, три и более знаков после него → `amount = null`,
|
||||
`string_value` = текст; у `excel` запятая и точка — десятичный разделитель; пробел допустим только как разделитель
|
||||
тысяч (группы ровно по три цифры), иначе `amount` — цифры подряд («лучшая догадка», не длиннее 16 цифр, иначе
|
||||
`null`) и `is_recognized = false`; многострочное значение в строке с однострочным кодом → `amount = null`,
|
||||
`string_value` = текст, `is_recognized = false`;
|
||||
- `is_recognized = true` — текст разобран как число без выброшенных и исправленных символов (замена двойника, `}` или
|
||||
`]` вместо скобки — исправление); `confidence` — от источника и в `is_recognized` не входит;
|
||||
- позиции нет, текст пуст или прочерк → `amount = null`, `string_value = null`, `is_recognized = true`, `confidence`
|
||||
ячейки (позиции нет — 100).
|
||||
|
||||
`parse_amount` — единственное место шагов 2–8, где разбор зависит от `source`. Разбор сумм ОСВ этим правилом MUST NOT
|
||||
затрагиваться.
|
||||
|
||||
#### Scenario: Потерянная цифра (AURORA-1511)
|
||||
- **WHEN** ячейка OCR — `"42831 1"` с уверенностью 100
|
||||
- **THEN** `amount = 428311`, `is_recognized = false`, `text = "42831 1"`
|
||||
|
||||
#### Scenario: Скобка `}` (OQ-147)
|
||||
- **WHEN** ячейка OCR — `"(7744536}"`
|
||||
- **THEN** `amount = -7744536`, `is_recognized = false`
|
||||
|
||||
#### Scenario: Запятая по источнику
|
||||
- **WHEN** текст `"4,200"`
|
||||
- **THEN** у `ocr` `amount = 4200`; у `excel` `amount = 4.20`
|
||||
|
||||
#### Scenario: Длинное тире (OQ-148)
|
||||
- **WHEN** ячейка OCR — `"—"` с уверенностью 100
|
||||
- **THEN** `amount = null`, `string_value = null`, `is_recognized = true`
|
||||
|
||||
### Requirement: Шаг 4 — единицы и дробные значения
|
||||
При единицах «тыс. руб.» и доле значений с дробной частью ≥ 10 % SHALL выдаваться предупреждение документа
|
||||
`FRACTIONAL_IN_THOUSANDS` (`info`); то же сочетание в книге xlsb без числовых форматов — `XLSB_SCALE_SUSPECT`
|
||||
(`error`). Книга xlsb без числовых форматов сама по себе — `XLSB_NO_FORMATS` (`info`); формат ячейки не разобран —
|
||||
`NUMBER_FORMAT_UNPARSED` (`info`, одно на документ с перечнем форматов).
|
||||
|
||||
#### Scenario: xlsb с копейками при «тыс. руб.»
|
||||
- **WHEN** книга xlsb без стилей, в шапке «тыс. руб.», 40 % значений дробные
|
||||
- **THEN** предупреждение `XLSB_SCALE_SUSPECT` уровня `error`, документ `needs_check`
|
||||
|
||||
### Requirement: Шаг 5 — строка формы
|
||||
Шаг 5 SHALL выдавать `StatementLine {row_index, ref, line_status, line_code, line_code_text, line_code_confidence,
|
||||
line_name, line_name_confidence, parent_ref, needs_review, values}`: `line_code` — разобранный код (4–5 цифр) или
|
||||
`null` у `unreadable` / `missing` / `detail`; `line_code_text` — сырой текст ячейки кода; `line_code_confidence` — от
|
||||
источника, у кода с заменой двойника — не выше 99; `line_name` — распознанный текст наименования как есть, с
|
||||
уверенностью источника. Сервисы MUST NOT подставлять наименование из справочника и MUST NOT срезать уверенность кода
|
||||
по справочнику (Р-3). Редакция (`66n` / `fsbu4`) в сервисах выбирается по эффективной дате (`fsbu4` при дате после
|
||||
2024-12-31) только для перечня кодов; без даты проверяются обе редакции.
|
||||
|
||||
#### Scenario: Наименование из OCR (AURORA-1522)
|
||||
- **WHEN** у строки 1230 OCR-наименование «Дебиторская задолжен-ность»
|
||||
- **THEN** `line_name = "Дебиторская задолжен-ность"` с уверенностью ячейки; справочное имя показывает ядро
|
||||
|
||||
### Requirement: Шаг 6 — тип формы, вариант и документ
|
||||
Шаг 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`.
|
||||
|
||||
`form_variant` SHALL определяться только по структуре кодов: ББ — есть 1600 и нет ни 1100, ни 1200 → `simplified`;
|
||||
ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 → `simplified`; иначе `full`; у `DISCLOSURE` — `null`. ОКУД
|
||||
признаком варианта не служит. Шапка документа — поля `header` кандидата: `text` → `<поле>_text`, разобранное → поле,
|
||||
`confidence` → `<поле>_confidence`.
|
||||
|
||||
#### Scenario: Упрощённый баланс
|
||||
- **WHEN** в ББ есть 1150, 1210, 1600, 1310, 1700 и нет 1100 и 1200
|
||||
- **THEN** `form_variant = simplified`; шаг 7 и перечень кодов берут упрощённый набор
|
||||
|
||||
#### Scenario: Заголовок спорит с кодами
|
||||
- **WHEN** `type_hint = BALANCE`, а коды строк — 2110, 2120, 2100, 2200
|
||||
- **THEN** `document_type = INCOME`, предупреждение `TYPE_CONFLICT` уровня `error`
|
||||
|
||||
### Requirement: Шаг 7 — самопроверки §7
|
||||
Шаг 7 SHALL считаться у обоих источников одним словарём `statement_checks.yaml` и выдавать `StatementChecks
|
||||
{relations_failed[], soft_relations_failed[]}`; каждое нарушение — `{column_index, relation, codes, expected,
|
||||
actual}`, где `codes` — коды цели и членов соотношения. Правила:
|
||||
- значение кода в колонке — сумма по всем строкам `ok` с этим кодом; строки `detail`, `unreadable`, `missing` в
|
||||
соотношения не входят; нарушение относится ко всем строкам его кодов;
|
||||
- соотношение **посчитано**, если непуста цель или хотя бы один член (есть строка `ok` с кодом и `amount` не `null`);
|
||||
пусто всё — соотношение пропускается; пустая цель при непустых членах и непустая цель при пустых членах — нарушение;
|
||||
- жёсткое соотношение не сошлось, если расхождение больше допуска ±1 в единицах формы **и** больше 0,001 % наибольшего
|
||||
по модулю числа соотношения — тогда оно в `relations_failed`; расхождение в пределах округления и мягкие
|
||||
соотношения — в `soft_relations_failed`;
|
||||
- члены `abs_terms` (2120, 2210, 2220, 2330, 2350, 2410) сравниваются и как записаны, и по модулю; такое соотношение
|
||||
подтверждает величину, но не знак (шаг 8);
|
||||
- набор соотношений — по `form_variant` (полный или упрощённый);
|
||||
- родитель пятизначной подстроки — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код из
|
||||
первых четырёх цифр; «сумма подстрок = родитель» (мягкое) — только если хотя бы одна подстрока в колонке непуста.
|
||||
|
||||
`checks` SHALL быть у каждого `BALANCE` / `INCOME` со строками и отсутствовать у документа без строк.
|
||||
`duplicate_line_codes` MUST NOT выдаваться.
|
||||
|
||||
#### Scenario: «БАЛАНС» без кода
|
||||
- **WHEN** строка «БАЛАНС» ушла в `detail` (код потерян), в колонке есть 1100 и 1200
|
||||
- **THEN** соотношение `1600 = 1100 + 1200` посчитано и нарушено (пустая цель при непустых членах), в
|
||||
`relations_failed`
|
||||
|
||||
#### Scenario: Лист только с активом
|
||||
- **WHEN** в ББ заполнены 1100, 1200, 1600, а 1300–1700 отсутствуют
|
||||
- **THEN** `1600 = 1700` нарушено (непустая цель при пустых членах), документ `needs_check`
|
||||
|
||||
#### Scenario: Повтор кода в соотношении
|
||||
- **WHEN** две строки 2410 с суммами 100 и 649
|
||||
- **THEN** в соотношениях код 2410 равен 749
|
||||
|
||||
#### Scenario: Округление итогов
|
||||
- **WHEN** `1600 = 1700` расходится на 9,02 при итогах ~17 млрд
|
||||
- **THEN** нарушение в `soft_relations_failed`, на статус и подсветку не влияет
|
||||
|
||||
### Requirement: Шаг 8 — `needs_review` значения
|
||||
Сервис SHALL вычислять `needs_review` у каждого значения:
|
||||
- значение **нуждается в покрытии**, если `confidence < 100` или `is_recognized = false`; чистые значения в покрытии не
|
||||
нуждаются;
|
||||
- значение **покрыто**, если в его колонке есть жёсткое соотношение с участием его кода, которое посчитано и сошлось
|
||||
(в пределах допуска или округления), и `amount` значения не `null`;
|
||||
- **знак**: у члена `abs_terms`, нуждающегося в покрытии, знак подтверждён, если форма записи знака (скобки, минус,
|
||||
без знака — по `text`) совпадает с формой у большинства членов `abs_terms` этой колонки с непустым `amount`,
|
||||
`is_recognized = true` и без правки аналитика; не совпадает или сравнивать не с чем — знак не подтверждён;
|
||||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности;
|
||||
- иначе `needs_review = false`.
|
||||
|
||||
#### Scenario: Догадка подтверждена соотношением (Р-7)
|
||||
- **WHEN** у 1230 `"42831 1"` → 428311, `is_recognized = false`, и `1200 = 1210 + 1220 + 1230 + …` в колонке посчитано
|
||||
и сошлось
|
||||
- **THEN** `needs_review = false`; `text` и уверенность остаются в значении
|
||||
|
||||
#### Scenario: Знак расхода в фигурной скобке
|
||||
- **WHEN** в колонке ОФР 2210 и 2220 записаны в скобках, а 2120 — `"(7744536}"`, и `2100 = 2110 − 2120` сошлось
|
||||
- **THEN** у 2120 знак подтверждён, `needs_review = false`
|
||||
|
||||
#### Scenario: Знак вне конвенции
|
||||
- **WHEN** 2210 и 2220 в скобках, а сомнительная 2120 — `"{38378)"` без минуса после разбора
|
||||
- **THEN** `needs_review = true` у 2120 независимо от соотношения
|
||||
|
||||
#### Scenario: Справочная строка без проверки
|
||||
- **WHEN** значение 2510 с уверенностью 80 (строк 25xx нет в жёстких соотношениях)
|
||||
- **THEN** `needs_review = true`
|
||||
|
||||
### Requirement: Шаг 8 — подтверждение неуверенного кода структурой
|
||||
Код строки SHALL считаться неуверенным, если `line_code_confidence < 100` или при разборе заменён двойник; только к
|
||||
неуверенным кодам применяется структурная проверка (Р-12). Четырёхзначные коды строк `ok` документа образуют
|
||||
последовательность; для каждой проверяемой редакции перечня (по эффективной дате; без даты — обе) берётся наибольшая
|
||||
подпоследовательность, согласованная с порядком бланка (итоги после слагаемых); редакция — с более длинной
|
||||
подпоследовательностью, при равенстве — по эффективной дате; уверенные коды участвуют как подтверждённые якоря;
|
||||
при нескольких наибольших подпоследовательностях подтверждены только коды, общие для всех. Пятизначный код подтверждён,
|
||||
если подтверждён его родитель и подстрока стоит после него. Повтор кода, у которого хотя бы одна копия неуверенна, —
|
||||
`needs_review = true` у неуверенной копии (Р-16). Неподтверждённый неуверенный код — `StatementLine.needs_review = true`.
|
||||
У Excel и чистых OCR-кодов `needs_review` строки по структуре MUST NOT ставиться.
|
||||
|
||||
#### Scenario: Неуверенный код на своём месте
|
||||
- **WHEN** код 1230 с уверенностью 50 стоит между 1220 и 1240 (уверенными)
|
||||
- **THEN** код подтверждён, `needs_review = false`
|
||||
|
||||
#### Scenario: Переставленные неуверенные соседи
|
||||
- **WHEN** порядок кодов 1210, 1230 (уверенность 50), 1220 (уверенность 50), 1240
|
||||
- **THEN** у обеих неуверенных строк `needs_review = true`, документ `needs_check`
|
||||
|
||||
#### Scenario: Нестандартный код Excel
|
||||
- **WHEN** в Excel-форме код 1231 или пятизначная подстрока 11101 вне перечня
|
||||
- **THEN** `needs_review` строки не ставится; проверка «код не из редакции» — `info` ядра
|
||||
|
||||
### 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` не попадают; единицы и прочие поля на статус не влияют.
|
||||
|
||||
#### Scenario: Неуверенная, но верная дата
|
||||
- **WHEN** дата отчёта 30.09.2025 с уверенностью 60, самая поздняя дата колонок 30.09.2025
|
||||
- **THEN** `needs_review_fields` пуст, предупреждения о дате нет
|
||||
|
||||
#### Scenario: Неуверенный год
|
||||
- **WHEN** дата отчёта 30.09.2023 с уверенностью 60, колонки на 30.09.2025 и 31.12.2024
|
||||
- **THEN** `needs_review_fields = ["report_date"]`, `REPORT_DATE_UNCERTAIN` уровня `error`, документ `needs_check`
|
||||
|
||||
### 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 быть такими:
|
||||
|
||||
| Код | Уровень | Когда |
|
||||
|---|---|---|
|
||||
| `JSON_FAILED` | error (файл) | json набора ИС ошибочен при успешном соседе |
|
||||
| `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 | сегмент с содержимым без столбца кодов (в том числе лист-продолжение) |
|
||||
| `SHEET_NOT_JOINED` | error | лист с ≥ 3 кодами и другим числом колонок не присоединился |
|
||||
| `CONTINUATION_TYPE_CONFLICT` | error | тип маркера продолжения противоречит форме |
|
||||
| `SEGMENT_COLUMNS_MISMATCH` | error | колонки сегмента не совпали с первым |
|
||||
| `COLUMN_WITHOUT_DATE` | error | числовая колонка без разобранной подписи |
|
||||
| `COLUMN_DATE_UNCERTAIN` | error | неуверенная подпись не попала в ожидаемые даты |
|
||||
| `HEADER_ROW_NOT_FOUND` | error | заголовка нет ни у одного сегмента |
|
||||
| `TYPE_CONFLICT` | error | тип по кодам противоречит `type_hint` |
|
||||
| `NO_ORGANIZATION_NO_INN` | error | в шапке нет ни организации, ни ИНН |
|
||||
| `PERIOD_UNKNOWN` | error | нет ни даты отчёта, ни абсолютных дат колонок |
|
||||
| `REPORT_DATE_UNCERTAIN` | error | неуверенная дата отчёта не совпала с датами колонок |
|
||||
| `XLSB_SCALE_SUSPECT` | error | xlsb без форматов при «тыс. руб.» и ≥ 10 % дробных |
|
||||
| `INN_NOT_FOUND`, `INN_UNCERTAIN` | info | ИНН не найден или неуверен при найденной организации |
|
||||
| `ORGANIZATION_NOT_FOUND`, `ORGANIZATION_UNCERTAIN` | info | организация не найдена или неуверенна при найденном ИНН |
|
||||
| `REPORT_DATE_UNPARSED` | info | дата отчёта не разобрана при наличии дат колонок |
|
||||
| `UNITS_NOT_FOUND` | info | единицы не найдены |
|
||||
| `FILE_TYPE_MISMATCH` | info (файл) | формат файла не совпал с `file_type` запроса |
|
||||
| `COLUMN_SKIPPED` | info | подпись из словаря исключений пропущена |
|
||||
| `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` и кодов |
|
||||
| `ROW_SPLIT` | info | разлеплена склеенная строка |
|
||||
|
||||
Предупреждения пути ОСВ — уровня `info`, код с префиксом `OSV_`. Новый код предупреждения вводится словарём пакета и
|
||||
таблицей §11 `05-domain-rules.md`; `code` в контракте — строка `^[A-Z][A-Z0-9_]*$`, не перечисление.
|
||||
|
||||
#### Scenario: НПЗ ОФР без ИНН
|
||||
- **WHEN** в шапке ОФР есть организация и нет ИНН
|
||||
- **THEN** `INN_NOT_FOUND` уровня `info`, документ может быть `done`
|
||||
|
||||
#### Scenario: Привязка к ячейке
|
||||
- **WHEN** не сошлось `1200 = 1210 + … + 1260` в колонке 1
|
||||
- **THEN** нарушение в `checks.relations_failed` с `column_index = 1` и `codes`, `needs_review = true` у ячеек этих
|
||||
кодов в колонке 1
|
||||
|
||||
### Requirement: Шаг 8 — статус документа и конверта
|
||||
Статус документа SHALL вычисляться одной функцией общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧
|
||||
период определён (дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет
|
||||
значений, строк и полей шапки с `needs_review` ∧ нет жёстких нарушений §7 вне допуска и округления ∧ нет
|
||||
предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о раскладке, строках и
|
||||
проверках. Функция SHALL принимать на вход и подтверждения и акцепт аналитика (у ядра после правок): правленое или
|
||||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения. Статус конверта: `done` ⇔
|
||||
все документы `done` ∧ нет предупреждений файла уровня `error`; иначе `needs_check`; документов нет — `error`
|
||||
(`RECOGNITION_FAILED` у scan-gateway, `INVALID_DOCUMENT` у tabular-parser). Квартал периода, сверка ИНН и наименований
|
||||
со справочником в `done` MUST NOT входить.
|
||||
|
||||
#### Scenario: Excel-форма без замечаний
|
||||
- **WHEN** Excel-ББ с найденной раскладкой, организацией, датой, всеми строками `ok`, сошедшимися соотношениями
|
||||
- **THEN** документ `done`; уверенность 100 у Excel выполняет условия покрытия автоматически
|
||||
|
||||
#### Scenario: Строка без кода с суммой
|
||||
- **WHEN** в форме есть строка `unreadable` с суммой
|
||||
- **THEN** документ `needs_check`
|
||||
|
||||
#### Scenario: Соседний документ не тянет файл
|
||||
- **WHEN** ББ `done`, ОФР того же файла `needs_check` из-за одной ячейки, предупреждений файла нет
|
||||
- **THEN** конверт `needs_check`, у ББ `document.status = done`
|
||||
@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Адаптер Excel шага 1
|
||||
tabular-parser SHALL оставить у себя чтение книги (лимиты `PARSE_MAX_BYTES`, `PARSE_TIMEOUT_SECONDS`, формат по
|
||||
сигнатуре, лимиты zip и объединённых ячеек, `UNREADABLE`), классификацию листов (ОСВ — отдельный путь; форма;
|
||||
продолжение; прочее) и адаптер «блок формы на листе → `FormCandidate`» общего кода; шаги 2–8 по формам SHALL
|
||||
выполняться копией пакета `arm_statements`. Адаптер SHALL: выделять блоки форм на листе по заголовку или ОКУД (без
|
||||
кодов между ними блоки сливаются, заголовок после кодов — новая форма); собирать шапку каждого блока в его границах
|
||||
(организация, ИНН без ИНН аудитора, ОКПО, ОКОПФ, ОКФС, единицы, дата, адрес) как `Cell` с `confidence = 100`;
|
||||
заполнять у каждой ячейки `text` по числовому формату (tp#42) и `value`, `ref = <лист>!<A1>`; дописывать
|
||||
листы-продолжения в сетку открытой формы до раскладки (маркер «Форма … с.2» или позиция листа; проверки типа,
|
||||
диапазона кодов и, при трёх и более кодах, числа колонок — как `join_blocker`), записывая границы листов в
|
||||
`segment_breaks`. Несовпадение формата с `file_type` — предупреждение файла `FILE_TYPE_MISMATCH` (`info`).
|
||||
|
||||
#### Scenario: Книга ББ + ОФР
|
||||
- **WHEN** книга содержит лист «Баланс» (ОКУД 0710001), лист «Баланс (2)» с маркером «Форма 0710001 с.2» и лист «ОФР»
|
||||
- **THEN** адаптер выдаёт два кандидата: `BALANCE` с `locator = sheet:Баланс,Баланс (2)` и одной сеткой из двух листов
|
||||
(`segment_breaks` — первая строка второго листа) и `INCOME` с `locator = sheet:ОФР`
|
||||
|
||||
#### Scenario: Ссылки ячеек
|
||||
- **WHEN** код строки стоит в ячейке C27 листа «Баланс»
|
||||
- **THEN** `ref` строки — `Баланс!C27`
|
||||
|
||||
### Requirement: Строки без кода и неизвестные колонки в Excel
|
||||
tabular-parser SHALL отдавать строки без кода с содержимым со статусами `detail` / `missing` / `unreadable` (Р-8; ранее
|
||||
не отдавались, OQ-36 / OQ-141), числовые колонки с нераспознанной подписью — колонкой без даты и `error` (ранее
|
||||
пропускались), повторы кодов — всеми строками без `duplicate_line_codes`.
|
||||
|
||||
#### Scenario: НПЗ «Реализация иного имущества»
|
||||
- **WHEN** в ОФР ФСБУ 4/2023 строка «Реализация иного имущества…» без кода с суммами стоит под строкой 2340
|
||||
- **THEN** в результате строка `detail` с `parent_ref` строки 2340; ранее она отбрасывалась со счётчиком
|
||||
|
||||
### Requirement: Путь ОСВ в контракте v2.0
|
||||
У `osvDocument` tabular-parser SHALL выдавать `status` (`done`, когда ведомость разобрана, иначе `needs_check` — по
|
||||
прежним условиям §1.6 листа) и `warnings[]` — прежние предупреждения листа структурами с `severity = info` и кодом с
|
||||
префиксом `OSV_`; строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Остальной разбор ОСВ не меняется.
|
||||
|
||||
#### Scenario: Предупреждение ОСВ
|
||||
- **WHEN** на листе ОСВ есть строка без родителя
|
||||
- **THEN** у документа ОСВ предупреждение `{code: "OSV_…", severity: "info", message}`, статус документа по §1.6
|
||||
|
||||
### Requirement: Статус книги
|
||||
Статус конверта tabular-parser SHALL вычисляться по общему правилу: `done` ⇔ все документы `done` ∧ нет предупреждений
|
||||
файла уровня `error`; книга без листов ОСВ и форм — `error INVALID_DOCUMENT`. Порядок документов — сначала ОСВ в
|
||||
порядке листов, затем формы.
|
||||
|
||||
#### Scenario: Книга только из пояснений
|
||||
- **WHEN** книга содержит только листы пояснений (0710003, «Пояснения»)
|
||||
- **THEN** документы `DISCLOSURE` со статусом `done`, конверт `done`
|
||||
@ -1,12 +1,12 @@
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.18), правила домена, контракты между сервисами
|
||||
Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.19), правила домена, контракты между сервисами
|
||||
(docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк).
|
||||
Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения
|
||||
документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway),
|
||||
у каждого свой openspec/ и свой change; сквозной change ссылается на них.
|
||||
Источники истины по приоритету: docs/model/data-model-v5.18.dbml → docs/model/entities-v5.18.md →
|
||||
Источники истины по приоритету: docs/model/data-model-v5.19.dbml → docs/model/entities-v5.19.md →
|
||||
docs/context/05-domain-rules.md → docs/source/tz (черновик, имена v4). Открытые вопросы — docs/context/07-open-questions.md;
|
||||
план срезов — docs/context/09-delivery-plan.md; соглашения шаблона команды — docs/context/03-architecture.md.
|
||||
Язык документов и коммитов — русский, идентификаторы — английские, как в DBML. docs/source/** не редактируется.
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
- NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`.
|
||||
|
||||
## Правила репозитория
|
||||
- ORM 1:1 с `../docs/model/data-model-v5.18.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI.
|
||||
- ORM 1:1 с `../docs/model/data-model-v5.19.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI.
|
||||
- Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
|
||||
- Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц.
|
||||
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
|
||||
|
||||
@ -37,7 +37,13 @@ PARTS = {
|
||||
"05_osv_parsed": ("Разобранные ОСВ", ["osv_entry"]),
|
||||
"06_statements": (
|
||||
"Распознанные формы ББ/ОФР и логические проверки",
|
||||
["financial_statement_meta", "financial_statement_column", "financial_statement_line", "financial_statement_value"],
|
||||
[
|
||||
"financial_statement_meta",
|
||||
"financial_statement_column",
|
||||
"financial_statement_line",
|
||||
"financial_statement_value",
|
||||
"document_issue",
|
||||
],
|
||||
),
|
||||
"07_consolidation": (
|
||||
"Консолидация: версии, статусы, периметр, документы, блокировка",
|
||||
|
||||