Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway, statement-normalization, tabular-recognition-service) с Purpose, изменения quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19, entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1665 lines
241 KiB
Markdown
1665 lines
241 KiB
Markdown
# 05. Правила предметной области (выжимка из ТЗ, имена — v5.19)
|
||
|
||
Каждый раздел: источник в ТЗ → правило → где в модели. Где ТЗ молчит или противоречит — пометка **[открыто]** и
|
||
ссылка на `07-open-questions.md`.
|
||
|
||
---
|
||
|
||
## §1. Парсинг ОСВ (tabular-parser + маппинг в ядре)
|
||
|
||
Источник: «Парсинг ОСВ» (DOC-260010), FNR01 (DOC-037740), реальные файлы `fixtures/osv/`. Реализация — `tabular-parser`,
|
||
Срез 2a (change `slice-2a-osv-parser`); что именно сделано и как выглядит результат — §1.6.
|
||
|
||
### 1.0 Главный принцип: структура определяется динамически, не по координатам
|
||
|
||
ОСВ приходят из разных конфигураций 1С (8.2/8.3, УПП, ERP, БП 3.0), из других учётных систем и после ручной правки
|
||
бухгалтером. **Ни одна буква колонки, ни один номер строки, ни одно имя листа не являются контрактом.** Фикстуры в
|
||
`fixtures/osv/` — это примеры двух организаций, а не спецификация формата. Парсер обязан:
|
||
|
||
1. **Найти лист(ы) с ОСВ** по содержимому: ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости всегда
|
||
над таблицей), матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра, «ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`.
|
||
Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать.
|
||
2. **Найти заголовок таблицы** по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…»,
|
||
«Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит»,
|
||
«Дт/Кт», «Д-т/К-т»). **Обязательны группы «обороты» и «сальдо на конец»; отсутствовать может только «сальдо на
|
||
начало»** (иначе заголовок не найден → `INVALID_FORMAT`, §1.6). Заголовок может занимать 1–4 строки с merged-ячейками; группировки могут быть как под «Счет»
|
||
в первой колонке, так и строкой «Субконто», так и перечислением через запятую (вариант 2).
|
||
3. **Определить колонки сумм по тексту**, а не по позиции: для каждой из трёх групп («начало», «оборот», «конец»)
|
||
найти подзаголовки «Дебет»/«Кредит» в следующей строке под ней (с учётом merged-диапазонов). Допустимы ОСВ без
|
||
«начала» (только обороты и конец), с одной свёрнутой колонкой сальдо (знак = сторона), с дополнительными
|
||
колонками («Количество», «Валюта», «ИНН», «КПП», «Код»). Неизвестные колонки — сохранять в `extra` строки, не терять.
|
||
4. **Определить колонку текста строки** — первая непустая слева от сумм (обычно A, но не обязательно); ИНН — колонка с
|
||
заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — ровно 10/12 цифр (число из 9/11 цифр при этом выборе
|
||
нулём не дополняется, иначе числовой КПП сошёл бы за ИНН — R-24).
|
||
5. **Определить иерархию строк** по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) —
|
||
по отступу (ведущие пробелы / `alignment.indent`), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по
|
||
факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings` как
|
||
`sheet=<имя листа> hierarchy=<outline|indent|account_pattern|sums|trivial>`. Строка «Итого» в каскаде не участвует
|
||
(всегда уровень 0); при меньше чем двух строках данных (пустая ОСВ) — `trivial`.
|
||
6. **Шапку документа** (организация, счёт, период, «выводимые данные») искать регулярками по всем ячейкам выше заголовка
|
||
таблицы; отсутствие любого поля — не ошибка, а `null` + warning (ядро попросит аналитика уточнить при проверке).
|
||
7. **Самопроверка результата** (контрольные суммы §1.4) — единственный способ понять, что структура угадана верно.
|
||
Если тождества не сходятся — не «подгонять», а вернуть `needs_check` с диагностикой, какой шаг не уверен.
|
||
8. Правила парсера — **данные, а не код**: словари синонимов заголовков, группировок, периодов лежат в
|
||
`tabular-parser/api/rules/*.yaml` (`osv_headers`, `osv_groups`, `periods`, `data_types`) и расширяются без релиза. Новый формат ОСВ = новая фикстура + при необходимости новый
|
||
синоним, а не новая ветка `if`.
|
||
|
||
Тесты парсера строятся по принципу «свойств»: для любого входного файла результат удовлетворяет инвариантам §1.4,
|
||
число строк-контрагентов ≥ 0, `Итого` = корень. Фикстуры — регрессия, не оракул формата.
|
||
|
||
### 1.1 Шапка — два варианта 1С (как **примеры**, не исчерпывающий список)
|
||
|
||
**Вариант 1** (все наши фикстуры):
|
||
```
|
||
r1 <Наименование организации> ← владелец ОСВ (сверить с legal_entity → document.counterparty_check)
|
||
r2 Оборотно-сальдовая ведомость по счету 60 за 1 квартал 2025 г. ← счёт после «по счету», период после «за»
|
||
r4 Выводимые данные: БУ (данные бухгалтерского учета) ← data_type шапки
|
||
r7 Параметры: | Валюта БУ, НУ: RUB (или «руб.»)
|
||
r9 Счет | Сальдо на начало периода | Обороты за период | Сальдо на конец периода
|
||
r10 Контрагенты | [ИНН] | Дебет | Кредит | Дебет | Кредит | Дебет | Кредит
|
||
r11 Договоры ← может отсутствовать
|
||
```
|
||
Группировки — отдельные строки первой колонки блока заголовка (`Счет`, `Подразделение`, `Контрагенты`, `Договоры`),
|
||
**порядок строк = порядок вложенности**. Ячейка `ИНН` в той же строке, что `Контрагенты` → `has_inn = true`.
|
||
|
||
**Вариант 2** (встречается у других клиентов):
|
||
```
|
||
Оборотно-сальдовая ведомость по счету 60
|
||
Период: 1 квартал 2026 г.
|
||
Детализация по субсчетам, субконто: Контрагенты, Договоры ← группировки через запятую, первая неявно ACCOUNT
|
||
Выводимые данные: сумма
|
||
Субконто | Сальдо на начало периода | Оборот за период | Сальдо на конец периода
|
||
| Дебет | Кредит | Дебет | Кредит | Дебет | Кредит
|
||
```
|
||
|
||
**Реквизит субконто в списке группировок (OQ-162, решение владельца 13.10.2026).** Элемент вида `<группировка>.ИНН`
|
||
(вариант 2 — «Детализация по субсчетам, субконто: Контрагенты, Контрагенты.ИНН»; вариант 1 — строка `Контрагенты.ИНН` в
|
||
блоке заголовка под строкой `Контрагенты`; суффикс после последней точки — по словарю `service_columns.inn`, префикс —
|
||
имя группировки `COUNTERPARTY` этой же шапки; реквизит может стоять до или после владельца) — не уровень аналитики, а реквизит
|
||
этой группировки. В таблице 1С выводит его отдельной строкой **под** контрагентом (уровень глубже, те же суммы, ИНН часто
|
||
числом); элемент может стоять и **перед** владельцем («субконто: Контрагенты.ИНН, Контрагенты» / строка
|
||
`Контрагенты.ИНН` выше строки `Контрагенты` в варианте 1): тогда 1С выводит ИНН **над** наименованием. В `group_schema`
|
||
такой элемент не попадает, у группировки-владельца `has_inn = true`; элемент — подсказка для строк ИНН (§1.3, OQ-163):
|
||
задаёт направление и снимает проверку контрольной суммы ИНН. Строки ИНН склеиваются с наименованием по §1.3.
|
||
|
||
Нормализация значений шапки в типы:
|
||
|
||
| Текст в шапке | `group_type` |
|
||
|---|---|
|
||
| Счет, Счета, Субконто (первая) | ACCOUNT |
|
||
| Подразделение, Подразделения | DEPARTMENT |
|
||
| Контрагент, Контрагенты | COUNTERPARTY |
|
||
| Договор, Договоры, Договоры контрагентов | CONTRACT |
|
||
| прочее (Валюта, Номенклатура, Статьи…) | OTHER |
|
||
|
||
Результат шапки → `recognition_job.group_schema` (по листу): `{"locator":"sheet:Лист_1","account":"60",
|
||
"period_text":"1 квартал 2025 г.","data_type":"BU","groups":[{"type":"ACCOUNT"},{"type":"COUNTERPARTY","has_inn":true},
|
||
{"type":"CONTRACT"}]}` и → `document(document_type=OSV, osv_account_code, period_year, period_quarter, is_cumulative, locator)`.
|
||
|
||
**Шапка без меток и без строки организации (реализовано, PR tabular-parser #13, OQ-134; ОСВ 63 Новоросметалл).** Период
|
||
может стоять отдельной ячейкой («Бухгалтерский | 9 месяцев 2025 г.»: `periods.yaml` — `standalone`, `data_types.yaml` —
|
||
`standalone_header`), организация — хвостом названия ведомости, а колонки счёта — подписями «Счет: Код / Наименование»
|
||
(`osv_headers.yaml` — `column_captions`: «Код» и «Наименование» под группой «Счет» не группировки). Код счёта берётся из
|
||
первой строки-счёта таблицы, если «по счету …» в шапке нет, — с предупреждением «код счёта … из строки таблицы». Текст
|
||
строки может стоять правее колонки текста: уровень задаёт колонка, в которой он найден.
|
||
|
||
### 1.2 Колонки сумм
|
||
|
||
Заголовки сумм объединены (merged) и позиции колонок плавают между файлами даже одной организации. Определять по тексту
|
||
(§1.0 п. 3). Всё правее последней колонки «Кредит» — не часть таблицы (в фикстурах там встречаются посторонние
|
||
числа): игнорируется, в суммы и в `extra` не попадает. В `rows[].extra` (контракт v1.1) идут только колонки **внутри**
|
||
таблицы, не сопоставленные с шестью суммами и ИНН (ключ — заголовок колонки: «КПП», «Количество»…). Числа могут быть numeric **или** текстом («1 234 567,89», «(1 234)»,
|
||
«-», «—», неразрывные пробелы) — нормализовать в Decimal; пустая ячейка/прочерк = NULL (не 0). Отрицательные возможны.
|
||
|
||
### 1.3 Строки
|
||
|
||
Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5),
|
||
`row_value` (текст первой колонки; целое число из ячейки — без дробной части: xls хранит числа как float, и
|
||
`2370012036.0` — артефакт чтения, а не значение файла), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат
|
||
только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; число из 9/11 цифр — ИНН региона 01–09
|
||
с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН контрагента — в порядке
|
||
приоритета: колонка ИНН (в том числе склеенная «ИНН/КПП»), затем **строка ИНН** рядом с наименованием (ниже, §1.3 «Строки ИНН», OQ-163), затем конец текста названия
|
||
(«Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)» — 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»).
|
||
**Строки ИНН (OQ-163, решение владельца 13.10.2026; заменяет правила OQ-130 и OQ-162).**
|
||
Строка, подпись которой целиком — ИНН (`inn_only_row`: 10/12 цифр числом или текстом, «ИНН …», «ИНН/КПП»; число из 9/11
|
||
цифр дополняется нулём слева — R-24). Без подсказки `<группировка>.ИНН` в шапке ИНН строки **должен пройти контрольную
|
||
сумму ИНН** (алгоритм ФНС); строка из 10/12 цифр с неверной суммой рядом с наименованием остаётся как есть (обычная
|
||
строка), предупреждение «строка N: похожа на ИНН X, но контрольная сумма не сходится — строка оставлена как есть», документ —
|
||
`needs_check`
|
||
(решение H-1: синтетика `s28`). С подсказкой сумма не проверяется, пустая строка-ребёнок наименования — «ИНН не указан»
|
||
(рулинг R-1 #32), непустая не-ИНН на этом месте — «ИНН не распознан» (такой ребёнок бывает только у наименования, которое по
|
||
схеме контрагент; у подразделения единственный ребёнок — наименование). Восемь раскладок — ИНН над / под наименованием × в
|
||
дереве / не в дереве × с суммами / без:
|
||
|
||
| Положение строки ИНН | Пара |
|
||
|---|---|
|
||
| у неё есть дети (над, в дереве) | единственный ребёнок-наименование, он поднимается на её место; несколько детей — без пары. Если подсказки в шапке нет, в `group_schema` больше одной группировки и в листе нет строк глубже схемы (R-3), строка — наименование по схеме (контрагент, названный ИНН) |
|
||
| ребёнок наименования (под, в дереве) | родитель; её дети — к нему; несколько строк ИНН у одного наименования — без пары. Родитель — пара только при подсказке «под» или если он по схеме контрагент (R-2); строка ИНН с детьми под контрагентом остаётся у него и при подсказке «над» (с предупреждением о противоречии) |
|
||
| сосед того же родителя (не в дереве) | с суммами — сосед с теми же суммами; без сумм (или совпали оба) — сосед по направлению листа |
|
||
|
||
Суммы строки ИНН, если есть, должны совпасть с суммами пары по всем шести колонкам (пусто = 0). Направление листа —
|
||
подсказка шапки (порядок элемента `<группировка>.ИНН`), а без неё — сведения пар соседей по суммам и блоков детей одного
|
||
родителя вместе (начинается строкой ИНН — над, кончается — под, начинается и кончается — блок сломан); противоречие
|
||
сведений или их отсутствие — направление неизвестно, строки без сумм без пары. Две строки ИНН на одно наименование — обе без пары. ИНН колонки, отличный от ИНН строки,
|
||
остаётся, документ — `needs_check`. Склеенная строка в `rows` не попадает, предупреждение «строка N: ИНН … отнесён к
|
||
контрагенту в строке M»; **без пары** — строка в `rows` с `group_type = OTHER` (в свод не попадает), предупреждение с
|
||
причиной, документ — `needs_check`.
|
||
**Противоречие подсказке шапки (решение H-2).** Если пара найдена, но фактическая раскладка противоречит направлению
|
||
подсказки `<группировка>.ИНН` (подсказка «над», а строка ИНН — ребёнок наименования или сосед ниже него; подсказка «под»,
|
||
а строка ИНН — родитель наименования или сосед выше него), строка всё равно склеивается, но добавляется отдельное
|
||
предупреждение «строка N: ИНН … стоит <под/над> наименованием в строке M, а шапка («…») указывает <над/под> —
|
||
проверьте», документ — `needs_check`. Раскладка, совпадающая с подсказкой (в том числе «под, в дереве» в 1С),
|
||
предупреждения не даёт; для соседей без сумм направление берётся из подсказки, конфликта нет.
|
||
**Реализовано** (tabular-parser #35, модуль `inn_rows.py`; синтетика `s18`, `s20`–`s28`).
|
||
|
||
Из текста ИНН отрезается всегда: `counterparty_name` без ИНН и разделителя, `row_value` — исходный текст; только для строк
|
||
`COUNTERPARTY`, договоры и прочее не трогаются; правила — `osv_headers.yaml` (`counterparty_inn_suffix`, `inn_only_row`,
|
||
`inn_kpp_cell`); **реализовано**, OQ-100, OQ-163 (строки ИНН; ранее OQ-130, tabular-parser #9, PR #15; синтетика `s17`–`s19`),
|
||
`data_type` (строки «USD», «Вал.», «Кол.», «Кол-во» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки).
|
||
|
||
«Вал» **без точки** — не метка валюты: так называется материал (счёт 10); валютная строка — «Вал.» с точкой или код
|
||
валюты из словаря (**реализовано**, tabular-parser #47, AURORA-1663). «Кол-во» — строка количества, как «Кол.» и
|
||
«Количество» (там же).
|
||
|
||
Строки «Вал.», «Кол.», «Кол-во» и трёхбуквенный код валюты («USD») — не уровень группировки, а вид сумм родителя:
|
||
`group_type = OTHER`, `data_type = CURRENCY | QUANTITY`, позицию в `group_schema` не занимают, **не наследуют**
|
||
`counterparty_name`/`counterparty_inn` (контрагента потребитель находит по родителю), `account_code_raw` наследуют от
|
||
ветки; их суммы остаются в шести полях, но в проверки родителя не входят (§1.4).
|
||
|
||
Классификация строки (п. 6–10 «Парсинг ОСВ»):
|
||
1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (флаги `re.ASCII | re.DOTALL`:
|
||
цифры только 0–9, название после запятой может быть многострочным; регистр учитывается;
|
||
сегмент субсчёта — 1–2 цифры **или 1–3 заглавные буквы**: буквенные субсчета 1С «76.АВ», «76.ВА», «76.ОТ» встречаются в
|
||
8 реальных файлах; после запятой — опциональное название, пробел необязателен: «60.01, Расчеты с поставщиками»)
|
||
→ ACCOUNT, `account_code_raw` — код как в файле без названия, `account_code` (нормализованный) строит ядро, все
|
||
дочерние строки наследуют счёт. Остаточный риск: имя контрагента вида «12.ООО» будет принято за счёт. Счета с буквой в
|
||
**первом** сегменте (забалансовые «МЦ.04», «ГТД», «НЕ.01.1») не распознаются как ACCOUNT — **[открыто]** OQ-103.
|
||
2. Не-счёт: позиция в ветке = количество не-счётных предков + 1 → соответствующая группировка из `group_schema`
|
||
(после последнего ACCOUNT). Увеличение уровня — ребёнок, равный — сосед, уменьшение — конец группы.
|
||
3. После COUNTERPARTY дочерние продолжают классифицироваться по схеме (CONTRACT, затем OTHER).
|
||
4. Строка «Итого» (level 0, последняя) — контрольная сумма всего документа; не сохранять как `osv_entry`
|
||
(OQ-01 закрыт). Парсер отдаёт её в `rows` как `group_type = TOTAL` (level 0, без родителя); **ядро (Срез 2c)
|
||
при маппинге строку TOTAL отбрасывает** (`osv_entry` её не содержит, `TOTAL` — не значение хранимого `group_type`); в
|
||
`metrics.rows` она не считается.
|
||
5. Все остальные строки сохраняются (п. 11 «Парсинг ОСВ») с `parent_entry_id` по `parent_row_number` (OQ-01 закрыт):
|
||
дерево строится по номеру родителя, строка с несуществующим родителем сохраняется с `parent_entry_id = NULL` и даёт
|
||
предупреждение маппинга (документ `NEEDS_CHECK`). Суммы и `data_type` (включая `NU`) пишутся как в результате.
|
||
|
||
Нормализация кода счёта (ядро, `domain/osv_mapping.py`): `60.1 → 60.01`, `60.01.1 → 60.01.1`, `76.7.1 → 76.07.1` —
|
||
числовой **второй** сегмент (1–3 цифры) дополняется нулём слева до двух цифр, первый и третий сегменты и буквенные
|
||
(`76.АВ`, `МЦ.04`) остаются как в файле. `account_code` строки хранится как в результате (`account_code_raw`),
|
||
нормализация нужна для `document.osv_account_code` и сверки с планом счетов. `account_root` = первый сегмент кода, если он
|
||
цифровой (`66`, `001` — забалансовый), иначе NULL (`ASSUMPTION(OQ-103)`). `account_id` = точное совпадение
|
||
нормализованного кода с `chart_of_account.code`, иначе NULL (администратор добавит; в seed плана счетов нет части
|
||
субсчетов — OQ-113).
|
||
|
||
### 1.4 Контрольные суммы парсера
|
||
|
||
Допуск ±0.01, пустое значение = 0.
|
||
- По каждой строке: `opening_debit − opening_credit + turnover_debit − turnover_credit == closing_debit − closing_credit` (±0.01).
|
||
**Исключение — счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 91 и 99 строки аналитики несут
|
||
только обороты, сальдо закрывается на субсчёт закрытия (91.09), поэтому тождество строк на них не проверяется; список
|
||
счетов — данные в `rules/*.yaml` tabular-parser (начальный состав 91, 99; 90 не входит — внутри года у его субсчетов
|
||
сальдо есть). Проверки «сумма детей = родитель» и «Итого» на этих счетах сохраняются в таком виде (R-3 PR
|
||
arm-tabular-parser#26, подтверждено владельцем 08.10.2026): **обороты** родителя сверяются с суммой детей всегда;
|
||
**сальдо** — только у родителя с дочерними субсчетами (91 → 91.01/91.02/91.09); у субсчёта, под которым одна аналитика,
|
||
сальдо с детьми не сверяется (у аналитики оно пусто, у субсчёта — равно оборотам). Счёт строки сопоставляется со
|
||
списком по ближайшему счёту ветки: запись `91` покрывает `91`, `91.01`, `91.09`, но не `910` и не `90.01`; строка
|
||
«Итого» проверяется как обычно.
|
||
- По каждому родителю: суммы родителя = Σ детей того же `data_type`. Строки CURRENCY/QUANTITY в суммирование не входят
|
||
и сами родителями не проверяются. **Обороты** сверяются по колонкам (дебет с дебетом, кредит с кредитом). **Сальдо**
|
||
(начало, конец) — по колонкам либо, **если у родителя сальдо свёрнуто** (не больше одной ненулевой стороны), по нетто
|
||
`дебет − кредит` (R-21): 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета и контрагентов —
|
||
развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт). Без этого правила 32 из 66 реальных файлов не сходились бы
|
||
по шести колонкам только из-за способа показа. **[открыто]** OQ-105: подтвердить, что нетто-свёртка родителя —
|
||
штатное поведение 1С для всех версий выгрузки. Остаточные риски (проверками §1.4 не ловятся):
|
||
- RR-1 — у свёрнутого родителя сверяется только нетто: распределение сальдо детей между Дт и Кт (перестановка сторон
|
||
при совпавшем нетто) не проверяется;
|
||
- RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя с потерянной в файле стороной
|
||
неотличимо от нетто-показа 1С и проходит при совпавшем нетто;
|
||
- RR-3 — нетто допускается на любом уровне дерева (не только «счёт → субсчета»): ошибка распределения сальдо между
|
||
договорами под свёрнутым контрагентом не ловится;
|
||
- RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто
|
||
строки, ловится лишь сверкой с развёрнутым родителем по колонкам;
|
||
- слепая зона: обе стороны сальдо развёрнутого ребёнка увеличены на одно X, а его непосредственный родитель свёрнут —
|
||
не ловится ничем: достаточно свёрнутого прямого родителя (сверка с ним — по нетто, нетто ребёнка не изменилось,
|
||
значения родителя и выше не затронуты);
|
||
- родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом;
|
||
- вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16).
|
||
- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0); если корней уровня 0 несколько (лист без
|
||
строки корневого счёта: верхний уровень — субсчета 58.01, 58.03, OQ-133, PR #13) — «Итого» == **сумме строк уровня 0**
|
||
(показатели без суммы и сама «Итого» не входят; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя
|
||
выше, R-21); при одном корне сверка прежняя. Вторая итоговая строка сразу за «Итого» («Итого развернутое») — тоже `TOTAL`,
|
||
в сверку не входит; строка сумм без подписи в конце таблицы принимается за «Итого» с предупреждением.
|
||
**Таблица кончается на первой «Итого»** (на корневом уровне оформления; плюс «Итого развернутое» сразу за ней): всё
|
||
ниже (подпись «Ответственный:», блоки, дописанные аналитиком, в том числе строки, начинающиеся с «Итого») — вне
|
||
документа, в `rows` не попадает. Сначала ищется первая строка корневого уровня с текстом ровно «Итого»
|
||
(`total_row_exact`), и только если такой нет — первая, начинающаяся с «Итого» (`total_row`): так контрагент
|
||
«Итого-Сервис» на плоском листе без outline и отступов не обрезает таблицу. Остаточный риск — только когда точного
|
||
«Итого» нет вовсе (ловит проверка «TOTAL = корень»). Числа в хвосте **в колонках таблицы** (от колонки текста до
|
||
правого края сумм) — предупреждение «ниже таблицы … есть числа» без смены статуса (статус решают контрольные суммы);
|
||
числа правее таблицы (контрольные суммы) хвостом не считаются. **Реализовано**, tabular-parser #47, AURORA-1664 (закрывает tabular-parser #7, M-2).
|
||
Если корня нет (**пустая ОСВ**: заголовок и
|
||
сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ
|
||
штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не
|
||
найдена вовсе — сверка невозможна: `total_row_matches_root = false` и предупреждение «строка «Итого» не найдена»
|
||
(результат `needs_check`, R-20).
|
||
Нарушение → `recognition_job.status = needs_check`, `document.status = NEEDS_CHECK`, причина — в `metrics.warnings`,
|
||
номера строк — в `checks` результата. Парсер структуру под сходимость не подгоняет.
|
||
|
||
### 1.5 Маппинг периода (FNR01) → `period_quarter`, `is_cumulative`
|
||
|
||
| Текст | `period_quarter` | `is_cumulative` |
|
||
|---|---|---|
|
||
| 1 квартал, I кв., 3 месяца, январь-март, 01.01–31.03 | Q1 | true |
|
||
| 2 квартал, II кв. | Q2 | **false** [открыто: 1С может считать нарастающим] |
|
||
| полугодие, 1 полугодие, 6 месяцев, январь-июнь | Q2 | true |
|
||
| 3 квартал, III кв. | Q3 | **false** [открыто] |
|
||
| 9 месяцев, январь-сентябрь | Q3 | true |
|
||
| 4 квартал, IV кв. | Q4 | **false** [открыто] |
|
||
| год, за 2025 год, 12 месяцев, годовая | Q4 | true |
|
||
|
||
**Период-диапазон месяцев** («Январь - Май 2025 г.», «Октябрь 2024 г. – Сентябрь 2025 г.»; `periods.yaml`: `months`, `range`;
|
||
OQ-131, PR #13 и 2b): парсер заполняет границы `period_start`/`period_end`/`is_from_year_start` (контракт v1.2; для квартала
|
||
и года: конец — последний день квартала, начало — 1 января у нарастающего и первый день квартала у квартального;
|
||
`is_from_year_start` — `period_start` = 1 января). Диапазон «январь – конец квартала» — нарастающий
|
||
(`is_cumulative = true`, квартал по месяцу конца), ровно один квартал — квартальный, остальное (скользящие 12 месяцев,
|
||
полгода с апреля, конец не на границе квартала) — `period_year` = год конца, `period_quarter = null`,
|
||
`is_cumulative = null`, границы по тексту и предупреждение «не с начала года» / «конец диапазона не совпадает с концом
|
||
квартала»: ядро такой документ к версии не привязывает (OQ-131).
|
||
|
||
`ASSUMPTION(OQ-02)`: парсер определяет `period_quarter` и `is_cumulative` **только по тексту периода**
|
||
(`rules/periods.yaml`, правила идут от специфичных к общим): квартальная ведомость Q2–Q4 считается не нарастающей;
|
||
сверку по данным (`Q3.opening == Q1.opening`) выполняет ядро и может переопределить признак. `period_year` — четыре
|
||
цифры года 19xx/20xx из текста, не являющиеся частью длинного числа.
|
||
|
||
Практика на фикстурах: ОСВ «за 3 квартал 2024» у обеих компаний — **квартальные** (opening ≠ сальдо на 01.01), а
|
||
ОСВ «за 2024 г.» — годовые. Признак можно проверить по данным: если есть ОСВ Q1 того же года и
|
||
`Q3.opening == Q1.opening` → нарастающая.
|
||
|
||
**Сверка по данным (ядро, Срез 2c, `ASSUMPTION(OQ-02)`).** Для документа с `period_quarter ∈ {Q2, Q3, Q4}` и
|
||
`is_cumulative = false` маппер берёт документ `Q1` того же ЮЛ, года и счёта (файл не `SUPERSEDED`; при нескольких —
|
||
последний по `created_at`) и сравнивает сальдо на начало корневой строки листа (дебет и кредит, `NULL` = 0, допуск ±0.01
|
||
включительно). Совпало → `is_cumulative = true` и предупреждение маппинга «нарастающая по данным Q1»; предупреждение
|
||
делает документ `NEEDS_CHECK` (§9.3, OQ-116). Нет документа Q1 или корня — признак остаётся по тексту периода.
|
||
|
||
Сверка периода документа с периодом версии → `document.period_check` (MATCH / MISMATCH / UNKNOWN); загрузка не блокируется;
|
||
до появления версии консолидации (Срез 4) ядро пишет `UNKNOWN`.
|
||
|
||
### 1.6 Результат парсера (Срез 2a): статусы, коды, лимиты, словари
|
||
|
||
Результат — файл по контракту v1.1: по одному `osvDocument` на лист ОСВ (`locator = sheet:<имя листа>`; имя листа
|
||
подставляется в `locator` и сообщения, но поиск по нему не ведётся). Форма и пример — `contracts/examples/
|
||
result-osv-needs-check.json`.
|
||
|
||
| Исход | Условие |
|
||
|---|---|
|
||
| `done` | контрольные суммы §1.4 сошлись на всех листах |
|
||
| `needs_check` | нарушена любая проверка §1.4 хотя бы на одном листе (или «Итого» не найдена); причины — `metrics.warnings`, номера строк — `checks` |
|
||
| `error` / `INVALID_DOCUMENT` | в книге нет ни одного листа ОСВ; в сообщении — число просмотренных листов |
|
||
| `error` / `INVALID_FORMAT` | лист ОСВ есть, но нет заголовка таблицы, обязательной группы сумм (§1.0 п. 2) или иерархию определить не удалось; в сообщении — лист и шаг |
|
||
| `error` / `UNREADABLE` | книга не открывается (лог WARNING) |
|
||
| `error` / `UNSUPPORTED_FORMAT` | сигнатура файла не XLS/XLSX/XLSB, размер больше `PARSE_MAX_BYTES` либо превышен лимит чтения (распакованный zip, площадь объединённых ячеек) |
|
||
| `error` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` |
|
||
| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение`, стек — в лог ERROR |
|
||
|
||
Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по
|
||
`file_type` запроса; несовпадение — предупреждение. Листы без признаков ОСВ и форм пропускаются с
|
||
предупреждением (`sheet=<имя> skipped: нет признаков ОСВ или формы`; листы форм теперь разбираются, §11.2). Другие предупреждения:
|
||
`sheet=<имя> hierarchy=<стратегия>` (всегда), свёрнутое сальдо, нераспознанное число (с номером строки), не найденное
|
||
поле шапки, «нет строк данных». xlsb не содержит outline level — каскад начинается с отступа (предупреждение
|
||
«xlsb: outline level недоступен»). xls (BIFF5/7) без записи CODEPAGE читается как cp1251 (кодовая страница 1С и Excel в
|
||
русской локали; по содержимому кодировка не угадывается) с предупреждением «xls без кодовой страницы — строки прочитаны
|
||
как cp1251»; без этого xlrd декодирует строки как latin-1 и лист ОСВ не находится. BIFF8 хранит строки в Unicode и от
|
||
CODEPAGE не зависит. **Реализовано**, tabular-parser #47, AURORA-1662.
|
||
**С контракта 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 с; кооперативный — проверка между листами,
|
||
в циклах по листу и между стратегиями; чтение книги библиотекой он не прерывает). Поэтому до библиотеки чтения действуют
|
||
лимиты чтения (R-23, R-25) → `UNSUPPORTED_FORMAT`: распакованный zip (xlsx/xlsb) ≤ 10 × `PARSE_MAX_BYTES`, суммарная
|
||
площадь объединённых ячеек книги ≤ 200 000 (реальный максимум 10 090; xlsx — потоковый expat-скан всех XML-частей до
|
||
openpyxl, после чтения — только диапазоны, которые индекс листа раскладывает по ячейкам). Объём самих ячеек,
|
||
`sharedStrings`/стили и XML-бомбы (`defusedxml`) не ограничены — **[открыто]** OQ-108. Любой реальный файл разбирается быстрее 5 с (максимум ≈ 0,6 с).
|
||
|
||
Словари: `osv_headers.yaml` (признаки листов, группы сумм, «Дебет»/«Кредит», колонка ИНН, «Итого», метки шапки),
|
||
`osv_groups.yaml` (группировка → `group_type`), `periods.yaml` (период → квартал и нарастающий итог), `data_types.yaml`
|
||
(БУ/НУ/сумма, строки «Вал.»/«Кол.»/«Кол-во», коды и написания валют). Каждая запись словаря обязана срабатывать хотя бы на одной
|
||
фикстуре или синтетике (тест покрытия).
|
||
|
||
---
|
||
|
||
## §2. Приведение периодов (DOC-259006; Срез 4, решения владельца 06.10.2026: OQ-03, OQ-19)
|
||
|
||
Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), при построении
|
||
сводной ОСВ (§3) строится **расчётное представление** (не хранится, DECISION-5). Реализация —
|
||
`domain/consolidation_periods.py:select_sources` (выбор документов) и `domain/consolidated_osv.py:build` (склейка).
|
||
|
||
**Выбор источников на пару (ЮЛ, счёт документа)** среди документов, привязанных к версии (§3 п. 1); `n` — квартал версии:
|
||
|
||
1. есть привязанный документ с `is_cumulative = true` и `period_quarter = n` — он один, строки берутся как есть;
|
||
2. иначе нужны привязанные квартальные документы за **каждый** квартал `Q1..Qn`; любого нет — **пробел**
|
||
(`Gap(missing_quarters)`), строк пары в своде нет, пробел показывается в сводке (`gaps[]`) и в листе «Пробелы» экспорта;
|
||
3. нарастающий и квартальные одновременно — берётся нарастающий (он полнее по определению);
|
||
4. **Q1-эквивалентность ОСВ (R-14, design №81; OQ-124):** документ ОСВ за `Q1` равнозначен и нарастающему, и квартальному
|
||
(парсер помечает ОСВ за Q1 как нарастающую: период с начала года = Q1) — в версиях Q2+ он служит квартальным источником
|
||
Q1. ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14a: баланс на конец Q1).
|
||
|
||
**Склейка квартальных строк** — ключ `(account_code строки, counterparty_name.strip(), normalize_inn(counterparty_inn))`;
|
||
договоры (`group_type = CONTRACT`) в свод не входят — строка COUNTERPARTY уже содержит их сумму:
|
||
|
||
```
|
||
opening_* = opening_* строки Q1
|
||
turnover_* = Σ turnover_* по кварталам Q1..Qn
|
||
closing_* = closing_* строки Qn
|
||
```
|
||
|
||
Отсутствующая в квартале строка ключа = нули; `None` в ячейке = 0; две строки с одним ключом внутри квартала
|
||
суммируются (design №1); `entry_ids` строки свода перечисляет все источники. Нарастающий документ отдаётся строка в
|
||
строку (OQ-125: различие с квартальным Q1 версии Q1 — только на дублях ключа и `None`). Строки свода сортируются:
|
||
имя ЮЛ, счёт (натуральный порядок: `9 < 60 < 60.2 < 60.10`), номер строки.
|
||
|
||
---
|
||
|
||
## §3. Сводная ОСВ и сопоставление контрагентов (DOC-221278, FNR01 п. 9)
|
||
|
||
Вход: `consolidation_version_id`. Периметр — `consolidation_member` версии (не текущий `related_group_member`!).
|
||
|
||
1. Для каждой компании периметра: `document` типа OSV, входящие в `consolidation_document` версии (**явная привязка**, а не
|
||
«последние подтверждённые»). Документ **подходит** версии, если ЮЛ — член периметра, `period_year` = год версии, и:
|
||
нарастающий (ББ/ОФР — всегда) — `period_quarter` = квартал версии; квартальный — `period_quarter` ≤ квартал версии;
|
||
ОСВ за `Q1` подходит версии любого квартала года (§2 п. 4, OQ-124). Привязываются только `CONFIRMED` документы
|
||
незаменённых файлов. Точки привязки: подтверждение документа (ко всем версиям «в работе»), создание версии и добавление
|
||
ЮЛ в периметр (подхват: на ключ «тип, счёт, квартал, признак нарастающего» — документ с самым поздним `confirmed_at`);
|
||
при привязке документ с тем же ключом у того же ЮЛ заменяется. Точки отвязки: автозамена файла (§9.5 п. 3), удаление ЮЛ
|
||
из периметра. Версии `CANCELLED` привязок не меняют. Выбор источников и склейка кварталов — §2.
|
||
2. Строки `osv_entry` этих документов с `group_type = COUNTERPARTY`; `data_type IN (BU, SUM)`.
|
||
3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются
|
||
как в файле.
|
||
4. Резолв контрагента (при маппинге результата — Срез 2c, и повторно при построении свода, если `counterparty_id IS NULL`):
|
||
ИНН → `legal_entity.inn`; иначе точное имя → `legal_entity.name`; иначе `normalize(name)` →
|
||
`legal_entity_alias.normalized_alias`. Несколько кандидатов → NULL (ручное сопоставление). На шаге нормализованного
|
||
имени **алиас приоритетнее имени** (решение владельца 07.10.2026, OQ-128; реализация — RSHB/arm-core#57): алиас уникален
|
||
(`409 ALIAS_TAKEN`), поэтому совпадение с `normalized_alias` однозначно — берётся владелец алиаса, даже если есть ЮЛ-тёзка
|
||
по нормализованному имени; NULL — только при нескольких совпадениях по имени без алиаса.
|
||
`normalize` = lower, trim, убрать пунктуацию/кавычки, убрать ОПФ (ООО, АО, ПАО, ИП…), схлопнуть пробелы.
|
||
Реализация при маппинге (`repositories/counterparties.py:CounterpartyIndex`): справочник читается целиком **без policy
|
||
видимости** (контрагент может быть ЮЛ другого филиала); применяется к строкам `group_type = COUNTERPARTY` с
|
||
`data_type ∈ {BU, SUM}`; ИНН строки сравнивается точно после удаления пробелов (мусор вроде «нерез.» не ищется, строка
|
||
проваливается к имени); каждый шаг считает кандидатов отдельно: единственное точное совпадение имени побеждает
|
||
неоднозначность нормализованного; имя из одних ОПФ (пусто после `normalize`) не ищется.
|
||
5. Признак контрагента: ГСК / ГК / Внешний (см. `04-data-model-guide.md`).
|
||
6. Количество строк свода = количеству отобранных `osv_entry` для нарастающей ОСВ; для квартальных документов строки
|
||
склеиваются по ключу (§2), `entry_ids` строки свода перечисляет источники.
|
||
7. **Дорезолв на чтение.** Строка без `counterparty_id` резолвится на лету тем же `CounterpartyIndex`; результат **не
|
||
сохраняется** (производное, DECISION-5): в ответе `match_stored = false`, `match_kind` — шаг резолва. Сохранённое значение
|
||
`osv_entry.counterparty_id` приоритетнее; при расхождении между кварталами берётся значение самого позднего квартала
|
||
(расхождение — в `warnings` строки).
|
||
8. **Способ сопоставления** `osv_entry.counterparty_match_kind` (DBML v5.16): `INN`/`NAME`/`ALIAS` — шаг, на котором при
|
||
маппинге нашёлся единственный кандидат; `MANUAL` — ручное сопоставление; `PROPAGATED` — автоприменение; `NULL` ⇔
|
||
`counterparty_id IS NULL`. Отдельное поле нужно, потому что после ручного сопоставления появляется алиас и ручное решение
|
||
становится неотличимым от `ALIAS`.
|
||
|
||
**Ручное сопоставление** (алгоритм в DOC-221278; Срез 4, `POST …/osv/entries/{entry_id}/counterparty`): правка версии —
|
||
право `CONSOLIDATION_EDIT`, статус «в работе» и своя блокировка (§10).
|
||
|
||
- **Кандидаты:** `scope=group` (по умолчанию) — ЮЛ периметра и ЮЛ той же ГК, `q` необязателен; `scope=all` — весь справочник
|
||
без policy видимости, `q` не короче 3 символов (иначе 422 `QUERY_TOO_SHORT`); поиск — ИНН по префиксу, имя и алиасы —
|
||
подстрока без регистра; не больше 100; `recommended` — нормализованное имя/алиас совпало с `normalize_alias` имени
|
||
строки `entry_id` (точное совпадение, OQ-127); рекомендуемые первыми.
|
||
- **Строка** — строка свода версии: `group_type = COUNTERPARTY`, `data_type ∈ {BU, SUM}` привязанного ОСВ-документа; строки
|
||
`NU`/`ACCOUNT` — 404 и в автоприменение не входят (R-11, §3 п. 2; OQ-126).
|
||
- **Шаги в одной транзакции:** выбор → проверка дубля `normalized_alias` у другой компании (`409 ALIAS_TAKEN` со списком
|
||
владельцев; с `move_alias = true` алиасы владельцев удаляются и алиас переносится — право `DIRECTORY_DELETE` для переноса
|
||
не требуется, решение владельца 06.10.2026) → создать `legal_entity_alias` (`LEGAL_ENTITY_CHANGE`) →
|
||
`osv_entry.counterparty_id = выбранное`, `counterparty_match_kind = MANUAL`; `counterparty_name/inn` не трогать →
|
||
автоприменить к строкам **всех** ОСВ-документов версии с тем же `counterparty_name.strip()`, пустым `counterparty_id` и
|
||
ИНН пустым или равным ИНН выбранного (`PROPAGATED`; ранее связанные строки не перевязываются) →
|
||
`audit_event(OSV_COUNTERPARTY_MAP)` с `before/after` и `propagated_entry_ids`.
|
||
- Строка `osv_entry` общая у всех версий, куда входит документ: сопоставление в одной версии меняет её для всех (OQ-123).
|
||
- Пустое имя (после нормализации) — алиас не создаётся и автоприменения нет. Тёзка по имени после ручного сопоставления не мешает: алиас приоритетнее имени (п. 4, OQ-128).
|
||
|
||
Наблюдение по фикстурам: у «Счастливочной» контрагент «Русский бисквит» идёт **с ИНН 3528052718** в колонке E, а у
|
||
«Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу.
|
||
|
||
---
|
||
|
||
## §4. Правила элиминации — пары счетов (Прил. 3 ТЗ, DOC-223044, FNR03)
|
||
|
||
Хранятся как данные: `elimination_rule` (+ `elimination_rule_account`), версионируются; версия фиксируется в
|
||
`consolidation_version.elimination_rules_version`. Seed-набор для ББ (`statement_type = balance`, `measure = closing`):
|
||
|
||
| № | Статья А (Дт) | Счета А (closing_debit) | Статья Б (Кт) | Счета Б (closing_credit) | mode | Примечание |
|
||
|---|---|---|---|---|---|---|
|
||
| 1 | 1150 ОС | 03, 76.07.9 (01 — резервный) | 1450 / 1550 Прочие обязательства | 76.07.1 | manual | лизинг/аренда внутри группы; у Б: 76.07.1 − 76.07.5 − 76.07.9. Если у А нет ОСВ 03 — результат в строку «ППА» [открыто] |
|
||
| 2 | 1170 ФВ долгосрочные | 58.01 | 1310 / 1350 УК / добавочный | 80, 83 | manual | **гудвилл**; если у Б нет ОСВ 80/83 — элиминируется 58.01 А; `1310(Б) − 58.01(А) > 0` → строка «Гудвилл» |
|
||
| 3 | 1170 ФВ долгосрочные | 58.03 | 1410 Займы долгосрочные | 67.03 | min | |
|
||
| 4 | 1240 ФВ краткосрочные | 58.03 | 1510 Займы краткосрочные | 66.03 | min | |
|
||
| 5 | 1230 Дебиторка | 76 (кроме 76.07.5, 76.07.9) | 1510 Заёмные краткосрочные | 66.04, 67.04 | min | проценты |
|
||
| 6 | 1230 Дебиторка | 60.02, 60.22, 60.32, 62.01, 62.03, 62.21, 62.31, 76 (кроме 76.07.5, 76.07.9) | 1520 Кредиторка | 60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76 (кроме 76.07.1) | min | основная пара |
|
||
| 7 | 1230 Дебиторка | те же, что № 6 | 1410 Заёмные долгосрочные | 67 | min | |
|
||
| 8 | 1230 Дебиторка | те же, что № 6 | 1510 Заёмные краткосрочные | 66 | min | |
|
||
|
||
Правило 5 % для пар: суммы равны → маппим; расхождение ≤ 5 % (к меньшей) → берём `min`; > 5 % → подсветка, авто не
|
||
маппится, ждёт корректировки (`ADJUSTMENT`).
|
||
|
||
ОФР (`statement_type = income`, `measure = turnover`), из FNR03 — **[открыто]**: формализовать с методологами:
|
||
|
||
| Статья | Сторона А | Сторона Б | mode / НДС |
|
||
|---|---|---|---|
|
||
| 2110 Выручка | Кт 90.01 (с НДС, приоритет) либо Дт 62.01/62.21/62.31 по контрагенту | Кт 60.01/60.21/60.31 + 76 (кроме 76.07.*, 76.03.*, плюс 76.07.2) | `full` — берётся сторона А; без НДС = с НДС / (1 + средневзвешенная ставка/100) |
|
||
| 2120 Себестоимость | — | — | элиминация = ВГО по 2110 (зеркально, без НДС) |
|
||
| 2310 Доходы от участия | Дт 76.03 | Кт 76.03 | сторона А |
|
||
| 2320 Проценты к получению | Дт 76.03/76.05/76.08/76.09 | Кт 66.02/.04/.06/.22/.24/.26, 67.02/.04/.06/.22/.24/.26 | min |
|
||
| 2330 Проценты к уплате | Кт чётных субсчетов 66/67 + 76.07.5/76.08/76.28/76.38 | Дт 76.03/.05/.08/.09 | min |
|
||
|
||
Дополнительные ручные корректировки ОФР (ТЗ 6.4, `correction.reason_code`): нереализованная выручка, акцизы
|
||
(`osv_entry.is_excisable` для 90), агентское вознаграждение, внутригрупповая амортизация по аренде, проценты по
|
||
внутригрупповым займам, доходы от участия, прочее.
|
||
|
||
---
|
||
|
||
## §5. Автоматическое сопоставление ВГО (DOC-223044)
|
||
|
||
Границы: только `closing_debit` / `closing_credit`; только компании периметра версии; пары счетов — из §4; **1:1**;
|
||
строка участвует в одном сопоставлении целиком.
|
||
|
||
```
|
||
для каждой строки a компании A с counterparty_id = B ∈ периметр, group_type = COUNTERPARTY, data_type ∈ (BU, SUM):
|
||
сторона a = closing_debit, если > 0, иначе closing_credit
|
||
кандидаты = строки b компании B с counterparty_id = A, противоположная сторона,
|
||
(счёт a, счёт b) входит в допустимую пару правила r (для каждого r — отдельный набор)
|
||
1. точное 1:1 (сумма a == сумма b, одна пара счетов) и единственный вариант → матч, обе строки выбывают
|
||
2. несколько точных по РАЗНЫМ правилам → не матчим, вниз таблицы на ручное
|
||
3. нет точного: по каждому правилу — минимальное |a − b|; выбираем правило с минимальным расхождением;
|
||
равное минимальное по разным правилам → не матчим (ручное)
|
||
результат пары: rule, A, B, сумма A, сумма B, deviation_amount = |A−B|, deviation_percent = dev / min(A,B) * 100,
|
||
исходные osv_entry.id обеих сторон
|
||
```
|
||
Результат **не хранится** (DECISION-5) — возвращается как структура для шахматки, корректировок и КФО. Ручное решение по
|
||
паре — `correction` с естественным ключом (`rule_id`, `legal_entity_id`, `counterparty_id`).
|
||
|
||
Открыто: агрегировать ли несколько строк одной компании по одному контрагенту и счёту перед матчингом (FNR03 говорит
|
||
«несколько строк суммируются», DOC-223044 — «одна позиция = одна строка»). Рекомендация: агрегировать по
|
||
(компания, контрагент, счёт, сторона), сохраняя список `osv_entry.id` как детализацию.
|
||
|
||
**Алгоритм — по ТЗ** (решение владельца 08.10.2026, OQ-155): пары, `min`, порог 5 %, а не полная элиминация каждой
|
||
стороны, как в эталонном Excel. Агрегация перед матчингом (OQ-07) ещё не решена; на фикстурах несколько строк одной
|
||
компании по одному контрагенту и счёту возникают из группировки «Подразделение» (ОСВ 62 Счастливочной, Q3 2024: 62.01
|
||
«БП Производство» 106 311 116,46 и «БП Аренда» 1 401 330 на «Русский бисквит» при одной строке 60.01 у РБ). Варианты —
|
||
строго 1:1 по строкам (DOC-223044) или гибрид: сначала точные пары 1:1, затем агрегация остатка.
|
||
|
||
---
|
||
|
||
## §6. Шахматка и результирующая таблица (DOC-221277)
|
||
|
||
Вход: версия + набор дебетовых счетов (`kind ∈ A, AP`) и кредитовых (`kind ∈ P, AP`); предустановки: «58.03 vs
|
||
67.03 + 66.03», «60 + 62 + 76 vs 60 + 62 + 76», «все счета».
|
||
|
||
Строки/столбцы — компании периметра; диагональ пуста.
|
||
```
|
||
amount_a(A,B) = Σ closing_debit строк A с counterparty_id = B по дебетовым счетам
|
||
amount_b(A,B) = Σ closing_credit строк B с counterparty_id = A по кредитовым счетам
|
||
variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amount_b) * 100; > 5 % — подсветка
|
||
ячейка: amount_a / amount_b / (variance)
|
||
```
|
||
Фильтры строк — как в §3 (COUNTERPARTY, BU/SUM, `account_id IS NOT NULL`).
|
||
|
||
Результирующая таблица «компания против группы» по статьям: Дебиторка (Дт 60.02, 60.22, 60.32, 62.01, 62.03, 62.21,
|
||
62.31, 76 кроме 76.07.5/76.07.9), Кредиторка (Кт 60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76 кроме 76.07.1),
|
||
ФВ (Дт 58.01, 58.03), Долгосрочные займы (Кт 67, 67.03), Краткосрочные (Кт 66, 66.03, 66.04); колонка «Итого по группе».
|
||
|
||
---
|
||
|
||
## §7. Логические проверки ББ/ОФР (DOC-221281, FNR03 шаг 1)
|
||
|
||
Выполняются по **каждой колонке-дате** отдельно; ничего не меняют; результат не хранится.
|
||
|
||
ББ: `1700 − 1600 = 0`; `1100 = 1110 + 1150 + 1160 + 1170 + 1180 + 1190` (+1120, 1130, 1140 если есть);
|
||
`1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260`; `1300 = 1310 + 1320 + 1340 + 1350 + 1360 + 1370`;
|
||
`1400 = 1410 + 1420 + 1430 + 1450`; `1500 = 1510 + 1520 + 1540 + 1550`; `1600 = 1100 + 1200`; `1700 = 1300 + 1400 + 1500`.
|
||
ОФР: `2100 = 2110 − 2120`; `2200 = 2100 − 2210 − 2220`; `2300 = 2200 + 2310 + 2320 − 2330 + 2340 − 2350`;
|
||
`2400 = 2300 − 2410 + 2420` (знаки 2410/2420/2430 — по ФСБУ 4/2023 уточнить [открыто]).
|
||
Межформенная: `1370(отчётная дата) − 1370(начало года) = 2400(ОФР за период)` — при расхождении КА может акцептовать
|
||
с комментарием (`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`) и
|
||
пересчитывает тем же кодом только после правок аналитика; своей копии словаря у ядра нет. Правила счёта:
|
||
|
||
- **Жёсткие** (нарушение → `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). Мягкое соотношение на статус и подсветку не влияет, но
|
||
**сошедшееся точно** (расхождение в пределах допуска ±1, без «округления») подтверждает неуверенные значения своих
|
||
кодов (Р-21, §11.9): так покрываются 2400, 2410, 2411, 2412, 2460, 25xx и пятизначные подстроки.
|
||
- **Значение кода** в колонке — сумма по всем строкам `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` (**мягкое**, как `2400` полного набора: знак 2410 не определён, OQ-12; `abs_terms` те же; решение
|
||
владельца 18.10.2026, Р-22 / S-5). Оба набора — в одном словаре `statement_checks.yaml`. Без упрощённого набора полный
|
||
дал бы нарушения «пустой цели» на каждой упрощённой форме. `form_variant = null` (документ, оставшийся в БД с времён до
|
||
v5.19, если база не очищена) — полный набор; у результата 1.x вариант определяется по структуре кодов при приёме.
|
||
- **Родитель пятизначной подстроки** — ближайшая четырёхзначная строка выше с теми же первыми тремя цифрами, иначе код
|
||
из первых четырёх цифр (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` после правок) — с теми же правилами, что в
|
||
сервисах. Пересчёт идёт точкой входа пакета для пересчёта по сохранённым данным (спека `statement-normalization`):
|
||
она же заново определяет вариант формы и редакцию документа для проверок ядра. Результат 1.x (окно выкладки) `checks`
|
||
не несёт: ядро **один раз при приёме** считает §7 той же точкой входа (вариант — по структуре кодов) и пишет нарушения в
|
||
`document_issue` (`source = core`, `relation` / `soft`); условие D-6 «проверки сошлись» для 1.x берётся из этого
|
||
расчёта. Формы, существовавшие до миграции v5.19, не пересчитываются: перед выкладкой база очищается от форм ББ/ОФР
|
||
(Р-24, §9.5 п. 2), а у формы, оставшейся в неочищенной базе, записей `document_issue` нет — до первой правки её
|
||
нарушения не показываются и подтверждение их не проверяет (корректность не гарантируется). Экран (`GET …/statement`) и подтверждение (`CHECKS_NOT_PASSED`) читают нарушения **только** из
|
||
`document_issue` — у любой формы, какой бы версией контракта она ни пришла. Межформенная `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` (правило выше); экран показывает подстроки под родителем, пустые подстроки свёрнуты за переключателем.
|
||
**Акцепт** — по жёстким нарушениям, с комментарием (1–2000 символов), `POST /documents/{id}/statement/checks/accept`;
|
||
без нарушений — 409 `NOTHING_TO_ACCEPT`. Акцепт **сбрасывается любой правкой формы** (с v2.0 — правкой данных:
|
||
подтверждение ячейки или кода без изменения его не сбрасывает, §9.5 п. 9). Аудит акцепта —
|
||
`RECOGNIZED_DATA_EDIT`. Формулы одни для редакций `66n` и `fsbu4` (`ASSUMPTION(OQ-12, OQ-146)`); редакцию ядро определяет
|
||
по дате периода документа и отдаёт в ответе (`edition`: `66n` при дате ≤ 2024-12-31, `fsbu4` — позже, `null` без даты;
|
||
OQ-145); тем же правилом сервисы разрешают ничью между редакциями упорядоченного перечня кодов (§11.6, §11.9).
|
||
|
||
**Полный состав формы (arm-core#80, решение владельца 09.10.2026).** Экран проверки показывает все строки формы
|
||
редакции документа (`edition`): справочник `arm-core/api/rules/statement_lines.yaml` (66н и ФСБУ 4/2023, ББ и ОФР, из
|
||
шаблонов банка `fixtures/statements-xlsx/templates/`). Строки и ячейки, которых нет в источнике, ядро достраивает в
|
||
ответе (`id = null`, `origin = template`) и в БД не пишет; первая непустая правка создаёт строку (признак «не из
|
||
источника» — до v5.19 `line_code_original_value = NULL`, с v5.19 — `row_index = NULL`) и ячейку как правку аналитика
|
||
(аудит `RECOGNIZED_DATA_EDIT`). Пустая добавленная строка не влияет на статус, проверки §7 и автоподтверждение; созданная
|
||
правкой строка участвует в проверках, пока у неё есть хоть одна сумма. Без редакции (нет даты периода) строки не
|
||
достраиваются. Строки «БАЛАНС» (1600, 1700) на экране выделяются цветом. **С v2.0** (change
|
||
`unified-statement-normalization`): справочник ядра — только словарь имён (код → наименование по редакциям; у кода может
|
||
быть несколько имён — синонимы и законные повторы кода в бланке, Р-20: 1220 «Налог на добавленную стоимость по
|
||
приобретенным ценностям» и «НДС, начисленный по отгрузке», 1520 «Кредиторская задолженность» и «Обязательство по
|
||
аренде»; загрузчик повтор кода не запрещает, сверка наименования — с любым именем кода, шаблонная строка берёт первое);
|
||
порядок и состав редакций ядро берёт из копии упорядоченного перечня кодов `arm_statements` (данные —
|
||
`contracts/statement-code-orders.yaml`; `sync-statements --check` сверяет, что все коды словаря имён есть в перечне); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не
|
||
достраивается; строки `unreadable` / `missing` шаблонной строкой молча не заменяются — показываются отдельно со статусом.
|
||
Наименование строки из источника хранится как распознано; экран показывает справочное имя по коду, файловое — рядом
|
||
(Р-3, AURORA-1406 / 1522).
|
||
|
||
Когда: в сервисе при распознавании; в ядре после «Сохранить» правок. Непройдено → `document.status = NEEDS_CHECK`;
|
||
пройдено/акцептовано + нажато «Подтвердить» → `CONFIRMED` + `audit_event(DOCUMENT_CONFIRM)`. Отрицательные значения в
|
||
формах — в скобках `(2100)` → `-2100`. Подсветка ячеек — только по `needs_review` (§11.9): уверенность, `is_recognized` и
|
||
нарушения §7 сведены в него сервисом; «подтверждение проверкой» — сошедшееся посчитанное жёсткое соотношение снимает
|
||
подсветку с сомнительной ячейки (Р-7).
|
||
|
||
---
|
||
|
||
## §8. Корректировки, гудвилл, КФО, пороги
|
||
|
||
**Корректировка** (DOC-227278, ТЗ 6.8): запись на пару (правило, компания 1, компания 2) и форму (ББ/ОФР); `debit_line_code
|
||
/ debit_adjustment`, `credit_line_code / credit_adjustment`; распределение суммы одной стороны по нескольким строкам —
|
||
дочерние записи (`parent_correction_id`, глубина 1). Контроль: `Σ debit_adjustment` ≈ `Σ credit_adjustment` (±5 %),
|
||
иначе ВГО распределено не полностью. Комментарий обязателен. История — `audit_event(CORRECTION_*)`, `version_no++`.
|
||
|
||
**Гудвилл** (DOC-259025, ТЗ 6.7): по каждой паре ГО→ДО периметра:
|
||
`FV = Σ closing_debit (58.01, контрагент ДО)`; `UK = 1310 ББ ДО на отчётную дату` (FNR03: Кт 80 + Кт 83, при
|
||
отсутствии — 1310); `share` — ручной ввод; `participation = (1310 [+ 1350?]) × share / 100`;
|
||
`goodwill = FV − participation` (может быть < 0); `include_in_kfo` + комментарий обязателен. Положительный итог —
|
||
строка `GOODWILL` в разделе I КФО.
|
||
|
||
**КФО** (ТЗ 6.4, Прил. 10): по каждому коду строки ББ/ОФР:
|
||
`sum_amount = Σ по компаниям` (значения отчётной колонки ББ / ОФР за период, в тыс. руб.) →
|
||
`eliminated_auto` (по §4–5, пересчёт в тыс. руб.) → `eliminated_manual = Σ correction APPLIED` → `goodwill_adjust` →
|
||
`consolidated_amount`; `vgo_amount = eliminated_auto + eliminated_manual`; разрез `kfo_result_line_company` (`amount`,
|
||
`share_percent` = доля компании в консолидированном показателе). Эталон колонок — `Консолидация_Группа РБ.xlsx`, лист
|
||
`01.10.2024`: «значение на дату | в т. ч. взаимные обязательства | очищенные показатели | доля в показателе по группе».
|
||
|
||
**Пороги** (ТЗ 6.8, FNR03 шаги 4–5), `kfo_result.threshold_*`:
|
||
- ББ: если `1300 < 0.1 × 1600` → Σ расхождений ≤ 1 % × 1600 (ВБ), иначе ≤ 10 % × 1300 (ЧА).
|
||
- ОФР: если `2200 / 2110 < 10 %` или отрицательна → ≤ 1 % × 2110 (выручка), иначе ≤ 10 % × 2200.
|
||
Превышение → `threshold_justification` обязателен (принятие риска), подсветка.
|
||
|
||
**Сравнение с КФО клиента** (`scenario = CONFIRM`): `client_kfo_line` построчно: `client_amount`, `client_vgo_amount`
|
||
(ручной ввод), `bank_amount`, `bank_vgo_amount` (из `kfo_result_line`), `deviation_percent` к наименьшему, причина.
|
||
|
||
---
|
||
|
||
## §9. Статусные модели
|
||
|
||
### 9.1 КФО (`consolidation_status`, DOC-037734 + ТЗ 6.10)
|
||
|
||
| Из | В | Кто | Когда |
|
||
|---|---|---|---|
|
||
| — | INITIATED | КА РФ / КА ГО | создана карточка, зафиксирован периметр, сценарий, подход |
|
||
| INITIATED | DOCUMENTS_PROCESSING | система/КА | загрузка и распознавание |
|
||
| DOCUMENTS_PROCESSING | DATA_CORRECTION | КА | есть документы NEEDS_CHECK |
|
||
| DATA_CORRECTION | CONSOLIDATION_RF | КА РФ | все обязательные документы CONFIRMED; инициатор — РФ |
|
||
| DATA_CORRECTION | CONSOLIDATION_GO | КА ГО | инициатор — ГО |
|
||
| CONSOLIDATION_RF | ADJUSTMENT | система | расхождение ≥ 5 % в парах (подстатус) |
|
||
| CONSOLIDATION_RF | VERIFICATION_RF | КА РФ | «отправить на верификацию» |
|
||
| VERIFICATION_RF | CONSOLIDATION_GO | РКА РФ | подтвердил |
|
||
| VERIFICATION_RF | CONSOLIDATION_RF | РКА РФ | вернул (комментарий обязателен) |
|
||
| CONSOLIDATION_GO | CONSOLIDATION_RF | КА ГО | вернул на доработку (комментарий) |
|
||
| CONSOLIDATION_GO | VERIFICATION_GO | КА ГО | если инициатор — КА ГО, верифицирует РКА ГО [открыто] |
|
||
| CONSOLIDATION_GO / VERIFICATION_GO | VERIFIED | КА ГО / РКА ГО | внесены № и дата решения УОБ; версия read-only; пишется `ex_snapshot` |
|
||
| любой нефинальный | INITIATED | КА | нужен другой периметр/сценарий/комплект |
|
||
| любой нефинальный | CANCELLED | КА | аннулировано |
|
||
| VERIFIED | — | — | только «новая версия» (`previous_version_id`) |
|
||
| из макета | REJECTED, ARCHIVED | | ANL-035 — подтвердить с заказчиком |
|
||
|
||
Каждый переход → `consolidation_status_history` + `audit_event(CONSOLIDATION_STATUS_CHANGE)`.
|
||
|
||
**Срез 4 реализует только переходы, не требующие маршрута согласования** (`consolidation_status.transition` — единственное
|
||
место смены статуса; остальные переходы таблицы — Срез 7, вручную дают 409 `TRANSITION_NOT_ALLOWED`):
|
||
|
||
| Событие | Переход | Кто (`changed_by`) |
|
||
|---|---|---|
|
||
| загрузка файла в консолидацию (`POST /consolidations/{id}/legal-entities/{le}/files`), принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING у **каждой** версии консолидации «в работе», где ЮЛ в периметре | загрузивший |
|
||
| маппинг создал документ `NEEDS_CHECK`, подходящий версии | DOCUMENTS_PROCESSING → DATA_CORRECTION | `stored_file.uploaded_by` (нет — переход пропускается, лог) |
|
||
| подтверждение документа, привязанного к версии | INITIATED / DOCUMENTS_PROCESSING → DATA_CORRECTION | подтвердивший |
|
||
| `POST …/status {CANCELLED, comment}` (комментарий обязателен, 422 `COMMENT_REQUIRED`) | любой «в работе» → CANCELLED | пользователь с блокировкой |
|
||
| `POST …/status {INITIATED}` | DOCUMENTS_PROCESSING / DATA_CORRECTION → INITIATED (привязки сохраняются) | пользователь с блокировкой |
|
||
|
||
«В работе» = INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION. `CANCELLED` снимает активную блокировку и `is_in_work`.
|
||
Загрузка и подтверждение блокировкой не ограничиваются; запрет загрузки/подтверждения после DATA_CORRECTION (ТЗ 03_9 A1) —
|
||
Срез 7 (OQ-118). Условие «все обязательные документы CONFIRMED» для DATA_CORRECTION → CONSOLIDATION_* — Срез 7.
|
||
|
||
### 9.2 Файл (`file_status`): UPLOADED → PROCESSING → PROCESSED | ERROR; любой → SUPERSEDED (замена файлом целиком,
|
||
`superseded_by_file_id`, `version_no + 1`). Статус меняют только по **текущему** заданию файла
|
||
(`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян);
|
||
`ERROR` — задание `error`/`timeout`/`dlq` либо ошибка маппинга; `PROCESSED` — маппинг завершён; `SUPERSEDED` — автозамена.
|
||
`PROCESSED` и `SUPERSEDED` событиями обмена не перезаписываются. Физическое удаление: ANL-020 запрещает его после
|
||
INITIATED; в Срезе 2c консолидаций ещё нет, поэтому удаление разрешено, кроме: (а) файла с документом, подтверждённым пользователем; (б) файла с автоподтверждённым документом, привязанным к версии вне INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION; (в) файла, заменившего другие (§9.5); поведение после появления консолидаций — OQ-110.
|
||
|
||
### 9.3 Документ (`document_status`): создаётся ядром при маппинге результата, по одному на лист ОСВ:
|
||
`RECOGNIZED` — задание `done`, `checks` парсера без нарушений, `counterparty_check ≠ MISMATCH` и нет предупреждений
|
||
маппинга **этого листа** (повтор счёта в файле, строки без родителя, нарастающая по данным Q1); иначе `NEEDS_CHECK`
|
||
(OQ-05 закрыт: `RECOGNIZED` нужен). **Автоподтверждение (OQ-157, решение владельца 08.10.2026; ТЗ 03_9 п. 4, 10_10):** документ, получивший при маппинге `RECOGNIZED`, в той же транзакции становится `CONFIRMED` от имени системы (`confirmed_by` NULL, `audit_event(DOCUMENT_CONFIRM)` с `user_id` NULL и описанием «автоподтверждение: без замечаний»), привязывается к версиям и двигает их, как при ручном подтверждении (§3 п. 1); переход версии — от `stored_file.uploaded_by` с комментарием «автоподтверждение документа …». Форма ББ/ОФР — при тех же условиях, что ручное подтверждение (п. 4: период определён, нет непройденных жёстких проверок §7; мягкие не учитываются); `RECOGNIZED` формы после правки или акцепта — только вручную. `NEEDS_CHECK` — только вручную. `DISCLOSURE`/`OTHER` (без данных) подтверждаются автоматически тоже (в версии не привязываются; удаление — по §9.5 п. 5). «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро
|
||
документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки
|
||
равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`;
|
||
иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN`: подходящесть документа версии — функция (документ, версия), а один документ входит в несколько версий (§3 п. 1, Срез 4; OQ-120).
|
||
|
||
**Документ формы ББ/ОФР (Срез 3b).** `RECOGNIZED` — только если одновременно: задание `done`; нет непройденных
|
||
неакцептованных **жёстких** проверок §7; `counterparty_check ≠ MISMATCH`; определены `period_year` и `period_quarter`; у
|
||
каждого значения либо `is_modified`, либо `is_recognized` не `false` и `confidence` равна 100 или пуста; нет предупреждений
|
||
маппинга по документу (повтор кода, значение вне колонок, пропущенная вторая форма); есть хотя бы одна строка. Иначе
|
||
`NEEDS_CHECK`. Статус пересчитывается при маппинге, после каждой правки формы и после акцепта (кроме `CONFIRMED`); при
|
||
пересчёте после правки/акцепта предупреждения маппинга не учитываются — на уровне документа они не хранятся
|
||
(`metrics.mapping_warnings` — на уровне файла). Расшифровки (`DISCLOSURE`) и прочие (`OTHER`) — один документ на тип на
|
||
файл без данных, статус `RECOGNIZED` при маппинге, затем автоподтверждается → `CONFIRMED` (OQ-23, OQ-157). Документ ББ/ОФР, созданный до Среза 3b и не имеющий данных формы
|
||
(`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` из результата и он согласован с
|
||
содержимым документа (нет строк `unreadable` / `missing` с суммой или текстом в значениях, нет строк, значений и полей
|
||
шапки с `needs_review`, нет `checks.relations_failed` и предупреждений документа уровня `error`; иначе ядро считает
|
||
документ `needs_check` и пишет `RESULT_STATUS_INCONSISTENT` `info` — страховка от ошибки сервиса; проверка — вызов
|
||
общей функции статуса §11.9 по входу, собранному из результата, а не пересчёт §7: значения строк `detail` в неё не
|
||
входят); в `metrics.warnings`
|
||
файла нет предупреждений уровня `error` (потерянный json, таблица ИС или исключение разбора таблицы — решение № 28 среза
|
||
3a; `needs_check` соседнего документа и статус конверта не учитываются); `counterparty_check ≠ MISMATCH`; определены
|
||
`period_year` и `period_quarter` (дата — конец квартала); у каждой строки `ok` с неуверенным кодом (`line_code_confidence
|
||
< 100`) распознанное наименование совпало со справочным по коду (Р-16, правило совпадения — §11.9); в файле нет второй
|
||
формы того же типа и периода (Р-18; «того же периода» — та же пара `period_year`, `period_quarter`, формы без квартала не
|
||
считаются). Иначе `NEEDS_CHECK`. Проверки §7, `needs_review` и предупреждения сервиса при загрузке не пересчитываются.
|
||
После правки или акцепта статус пересчитывается общим кодом (§11.9) с подтверждениями аналитика и теми же условиями ядра;
|
||
`RECOGNIZED` после правки — только вручную, как и прежде. Документ, принятый по 1.x, после правки пересчитывается так же
|
||
(общим кодом), его `LEGACY_WARNING` и `MAPPING_WARNING` — `info` и статус не держат.
|
||
|
||
**Документ ОСВ при приёме контракта v2.0.** `RECOGNIZED` — только если `osvDocument.status = done` (контрольные суммы
|
||
§1.4 листа сошлись), `checks` листа без нарушений, в `metrics.warnings` файла нет предупреждений уровня `error`,
|
||
`counterparty_check ≠ MISMATCH` и нет предупреждений маппинга ядра по этому листу; статус конверта не учитывается (как
|
||
Р-19 у форм: ОСВ в книге с сомнительной ОФР автоподтверждается). Предупреждения листа (`osvDocument.warnings`, `OSV_*`,
|
||
`info`) хранятся в `document_issue` документа ОСВ и отдаются в `DocumentOut.issues`.
|
||
|
||
### 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 мин.
|
||
|
||
### 9.5 Первичные документы: загрузка, маппинг, замена, удаление, подтверждение (Срез 2c)
|
||
|
||
1. **Загрузка** (`POST /legal-entities/{id}/files` — без контекста, `stored_file.consolidation_id = NULL`; в консолидацию —
|
||
п. 8; право `DOCUMENT_EDIT`, ЮЛ в видимости): тип файла по расширению и
|
||
сигнатуре (`xls` — BIFF8, `xlsx`/`xlsb` — zip, `pdf`, `png`, `jpg`); лимит `UPLOAD_MAX_BYTES` (50 МиБ); дедуп по
|
||
`sha256` в пределах ЮЛ среди файлов не `SUPERSEDED`. Принятый файл: объект S3 `uploads/{file_id}/{job_id}/{имя}`,
|
||
`stored_file(UPLOADED, version_no = 1)`, `recognition_job(queued)`, `audit_event(FILE_UPLOAD)`. Тип документа и период
|
||
при загрузке не запрашиваются: их определяет парсер.
|
||
2. **Маппинг** (фоновый цикл ядра, отдельный от inbox): задание `done`/`needs_check` + файл `PROCESSING` → проверка ключа
|
||
результата (бакет и префикс `results/{file_id}/{job_id}/`, OQ-70) → чтение S3 вне транзакции → одна транзакция:
|
||
прежние строки файла удаляются, на каждый лист ОСВ создаётся `document` (`osv_account_code` — нормализованный счёт
|
||
шапки, `period_year/quarter`, `is_cumulative` по §1.5, `locator`), вставляются `osv_entry` (§1.3), резолвится
|
||
`counterparty_id` (§3 п. 4), пишутся `recognition_job.group_schema` и `metrics.mapping_warnings`, файл → `PROCESSED`,
|
||
выполняется автозамена. Второй лист того же файла с тем же счётом документа не создаёт (предупреждение, первый
|
||
документ — `NEEDS_CHECK`). Повторяемые сбои (БД, S3) — до `MAPPING_MAX_ATTEMPTS` (3) попыток, затем файл `ERROR`
|
||
(`metrics.mapping_error`); неповторяемые (ключ вне папки, нет объекта, результат другого задания, пустой список
|
||
`documents`) — сразу `ERROR`. **Формы (Срез 3b):** после листов ОСВ, в той же транзакции, каждый `statementDocument`
|
||
`BALANCE`/`INCOME` (Excel и OCR) становится `document` (`osv_account_code = NULL`, `is_cumulative = NULL` — отчёт
|
||
нарастающий, `locator` из результата) + `financial_statement_meta` (шапка: ИНН, дата отчёта, единицы, ОКУД/ОКПО/ОКОПФ/ОКФС,
|
||
адрес; поля `*_original_value` заполняются как распознано и больше не меняются) + `financial_statement_column` +
|
||
`financial_statement_line` (строки с кодом, `(meta_id, line_code)` уникален: повтор кода — остаётся первая строка,
|
||
предупреждение) + `financial_statement_value` — **на каждую пару «строка × колонка» одна запись** (ячейки, которых нет
|
||
в результате, — с пустыми суммой и уверенностью; значение с `column_index` вне колонок формы пропускается с
|
||
предупреждением; повтор `column_index` в колонках результата или повтор пары «строка × колонка» — остаётся первое, пишется
|
||
предупреждение, документ `NEEDS_CHECK`). `checks` результата ядро игнорирует: §7 считает само (§7). **Период формы:** дата = `header.report_date`,
|
||
а если даты нет — **самая поздняя `period_date` колонок** (дата отчёта в OCR часто мусор; колонки надёжнее); `period_year` —
|
||
год даты, `period_quarter` — только если дата — последний день марта, июня, сентября или декабря, иначе квартал пуст и
|
||
предупреждение «дата … не конец квартала»; без даты год и квартал пусты. У колонки с самой поздней датой `is_current = true`.
|
||
`counterparty_check` — как у ОСВ (§9.3). **Повтор формы:** второй `BALANCE`/`INCOME` того же типа в файле не создаётся
|
||
(предупреждение, первый документ — `NEEDS_CHECK`, OQ-77); `DISCLOSURE`/`OTHER` — один документ на тип на файл без данных
|
||
(OQ-23). Форма без строк (нет колонки кодов, OQ-139) создаётся с шапкой и колонками, если они есть в результате, статус
|
||
`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` по порядку строк результата с 0,
|
||
`line_code_original_value = line_code`, `source_ref = null`, `text = string_value`, `needs_review` значений = **НЕ**
|
||
«правлено или уверенность 100 и `is_recognized`» (чистое значение — `false`), `form_variant` — по структуре кодов,
|
||
`file_ordinal` — индекс в `documents[]`, `header_source` — из полей шапки 1.x; нарушения §7 ядро считает один раз
|
||
точкой входа `arm_statements` и пишет в
|
||
`document_issue` (`source = core`, §7 «Ядро»); предупреждения маппинга 1.x по документу (повтор кода, значение вне
|
||
колонок) — `document_issue` `MAPPING_WARNING` `info` (`source = core`; статус при приёме — по D-6, после правки они
|
||
статус не держат); строки `metrics.warnings` 1.x — в `recognition_job.metrics.warnings` структурами `{code:
|
||
"LEGACY_WARNING", severity: "info", message}` уровня файла; `statements_version` задания — `null`, и
|
||
`STATEMENTS_VERSION_MISMATCH` у такого документа не пишется).
|
||
Результат пишется как пришёл, без повторной дедупликации: каждая строка — `financial_statement_line` с `row_index`
|
||
(у строки из результата всегда не `null`: `row_index IS NULL` — признак строки, созданной правкой),
|
||
`source_ref` (`ref`, уникален в форме), `line_status`, `line_code` (`null` у `unreadable` / `missing` / `detail`),
|
||
`line_code_original_value` (= `line_code` результата; правками не меняется), `line_code_text`, `parent_row_index`
|
||
(родитель `detail`), `needs_review`; повтор кода — законный случай, хранятся все
|
||
строки (Р-4; строки без кода хранятся — Р-8, отменяет OQ-36 / OQ-141). Значения — с `source_text` (`values[].text`) и
|
||
`needs_review`; колонки — с `period_kind` и `label_confidence`; шапка — с `form_variant`, `needs_review_fields` и
|
||
`header_source` (`<поле>_text` и `<поле>_confidence` всех полей; `*_original_value` — как прежде: `inn`,
|
||
`report_date_text`, `units_text`; `inn_confidence` результата, `organization_name_original_value` =
|
||
`organization_name`); `checks`
|
||
и предупреждения документа — в `document_issue` (источник `service`; нарушение — `kind = relation`, `code =
|
||
RELATION_FAILED`, `error` или `kind = soft`, `code = RELATION_SOFT`, `info`, с `codes`, `relation`, `expected`,
|
||
`actual`, `column_id`; предупреждение о столбце вне колонок формы — с `column_index`) вместе с проверками ядра уровня
|
||
`info` (`CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`); версия общего кода —
|
||
`recognition_job.statements_version`. **Каждый** документ результата (ОСВ, формы, `DISCLOSURE`, `OTHER`) при любом
|
||
MAJOR получает `file_ordinal` — индекс в `documents[]` (несозданный документ номер не занимает и не сдвигает
|
||
остальные). Предупреждения файла хранятся в `recognition_job.metrics.warnings` структурами (у 1.x — `LEGACY_WARNING`)
|
||
и отдаются в `JobBrief.warnings` объектами (строки старых заданий конвертируются при чтении). Каждый `BALANCE` /
|
||
`INCOME` становится отдельным документом:
|
||
вторая форма того же типа в файле **хранится** (Р-10, Р-15; ни одна из двух форм одного типа и периода не
|
||
автоподтверждается — Р-18); ОСВ по счёту, `DISCLOSURE` и `OTHER` — по-прежнему один документ на тип (и счёт) на файл.
|
||
Все чтения формы «по коду» (межформенная проверка, суммы формы для консолидации и выгрузок, `parent_code`, раскладка
|
||
экрана, правка по `line_code`, метрики `fields_*`) идут через одну чистую функцию **«агрегат формы по коду»**: сумма
|
||
`amount` строк `ok` с кодом по колонке, список `line_id`, справочное имя (DECISION-5, не хранится). Строки `detail`,
|
||
`unreadable`, `missing` в агрегат, КФО и снимок ЕХ не попадают — цена Р-8. Записи `document_issue` удаляются вместе
|
||
с документом при перезагрузке файла.
|
||
**Перед выкладкой ядра v5.19** база очищается от форм ББ/ОФР (Р-24; скрипт ядра `scripts/purge_statements.py`,
|
||
`--dry-run` с отчётом, затем `--apply`; владелец или платформенная команда по его заявке, после резервной копии):
|
||
документы `BALANCE` / `INCOME` с `financial_statement_*`, комментариями и привязками к версиям «в работе»; файлы без
|
||
ОСВ — целиком с заданиями (объектам S3 — `expires_at`); смешанные «ОСВ + формы» — только формы, или целиком по
|
||
решению владельца; формы в верифицированных версиях — отказ без изменений; ОСВ, консолидации и справочники не
|
||
трогаются; системный аудит `FILE_DELETE`. Миграция v5.19 только добавляет колонки и значения по умолчанию (спека
|
||
`arm-core`, «Миграция DBML v5.19», «Очистка форм перед выкладкой»).
|
||
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`
|
||
документом (решение владельца 05.10.2026); документ без года или квартала в замене не участвует. Документы и строки
|
||
заменённых файлов сохраняются, но по умолчанию не показываются. Если новый файл содержит лишь часть документов
|
||
заменяемого, заменяемый уходит в `SUPERSEDED` целиком (OQ-06: замена файлом целиком; частный случай — OQ-115). Для форм
|
||
поведение то же (решение владельца 07.10.2026, OQ-154): исправленный отдельный ОФР заменяет всю книгу «ББ + ОФР» того же
|
||
периода. Чтобы это не происходило молча, документы заменённого файла, ключа которых нет в новом, перечисляются в
|
||
предупреждении `metrics.mapping_warnings` нового файла: тип, счёт, период, был ли `CONFIRMED`, из каких версий сняты
|
||
привязки (реализация — RSHB/arm-core#58).
|
||
Аудит: системное `FILE_REPLACE` (`user_id = NULL`, `object_id` — заменённый файл, `after_value` — новый, в `description`
|
||
перечень документов и `confirmed=true`). **Срез 4:** автозамена снимает привязки (`consolidation_document`) **всех**
|
||
документов заменённого файла из всех версий «в работе» (`CANCELLED` не трогаются); версии, где привязка снята, —
|
||
`FILE_REPLACE.after_value.unbound_version_ids`; новый документ привязывается только после подтверждения (§3 п. 1).
|
||
4. **Подтверждение** (`POST /documents/{id}/confirm`, `DOCUMENT_EDIT`): `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`
|
||
(`confirmed_by/at`, `audit_event(DOCUMENT_CONFIRM)`); повтор — 409 `ALREADY_CONFIRMED`; документ заменённого файла —
|
||
409 `FILE_SUPERSEDED`; прочие статусы — 409 `DOCUMENT_NOT_CONFIRMABLE`. Автоподтверждённый документ (`confirmed_by` NULL) повторно подтвердить нельзя — 409 `ALREADY_CONFIRMED`. Ручной правки строк ОСВ нет (решение владельца
|
||
07.10.2026, OQ-114; возможен пересмотр по качеству парсинга и документов): ошибка в ОСВ исправляется перезагрузкой файла
|
||
(п. 3), поля `is_modified`/`original_values` у `osv_entry` — резерв; корректировки — §8.
|
||
**Форма ББ/ОФР (Срез 3b)** дополнительно: период не определён (`period_year` или `period_quarter` пуст) — 409
|
||
`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`, у которых хотя бы в одной ячейке
|
||
непустая сумма или нераспознанный текст — `amount` или `string_value` не `null`; `detail.lines[] {line_id, row_index,
|
||
line_code_text}`; строки без значений не мешают) → `DUPLICATE_FORM` (в файле уже подтверждена другая форма того же
|
||
типа и периода — та же пара `period_year`, `period_quarter`; `detail.document_id`; между файлами — автозамена п. 3) →
|
||
`CHECKS_NOT_PASSED` (нарушения — записи `document_issue` вида `relation` без акцепта, у любой формы).
|
||
Автоподтверждения `LINES_UNRESOLVED` не касается (`done` таких строк не допускает).
|
||
При подтверждении пишется `financial_statement_meta.reviewed_by/at`. Несовпадение контрагента (`MISMATCH`) и низкая
|
||
уверенность значений подтверждение **не блокируют** (как у ОСВ): защита — статус `NEEDS_CHECK` и подсветка. Мягкие проверки
|
||
(в том числе межформенная) не блокируют. `DISCLOSURE`/`OTHER` — без условий. Привязка к версиям и переходы статуса версии —
|
||
как у ОСВ (§3 п. 1, §9.1): ББ/ОФР за квартал версии (`Q1` — только версии за `Q1`, R-14a Среза 4).
|
||
5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено, если у файла есть документ, подтверждённый пользователем (`confirmed_by` не NULL), или автоподтверждённый документ привязан к версии вне `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`FILE_HAS_CONFIRMED_DOCUMENTS`; OQ-157, уточняет OQ-110); привязки автоподтверждённых документов снимаются, статус версий не меняется, `FILE_DELETE.after_value.unbound_version_ids`; запрещено также, если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить
|
||
можно. Удаляются строки, документы (вместе с данными форм `financial_statement_*` и комментариями документов), задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не
|
||
стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии
|
||
привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в
|
||
версии невозможно; расширять удаление не требуется (уточнено OQ-157: для автоподтверждённых документов — см. выше).
|
||
6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); правка формы и
|
||
акцепт проверок — `RECOGNIZED_DATA_EDIT` (раздел `RECOGNITION`, `object_type = document`, `before/after` изменённых полей и
|
||
ячеек); смены статусов задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55). Не аудируются (Срез 3b):
|
||
скачивание исходного файла, CSV чек-листа, комментарии к документу (`comment` сам хранит автора и время); экспорт сводной
|
||
ОСВ в xlsx, напротив, пишет `audit_event(EXPORT)` (Срез 4). Скачивание первички (это же и просмотр PDF) и CSV чек-листа
|
||
не журналируются — решение владельца 07.10.2026 (OQ-153).
|
||
7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы (`FOR NO KEY UPDATE`) → advisory-lock ГК →
|
||
версии консолидации (`FOR UPDATE`, по возрастанию `id`); автозамена дополнительно берёт advisory-lock по ЮЛ. Подробности —
|
||
`03-architecture.md`.
|
||
8. **Загрузка в консолидацию** (`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`,
|
||
`DOCUMENT_EDIT`; v5.18, решения владельца 09.10.2026, RSHB/arm-ka-docs#90): до приёма — консолидация видима (404
|
||
`NOT_FOUND`), ЮЛ видимо (404), есть версия консолидации «в работе» (409 `CONSOLIDATION_NOT_EDITABLE`), ЮЛ — член
|
||
периметра хотя бы одной такой версии (422 `ENTITY_NOT_IN_PERIMETER`). Принятый файл получает
|
||
`stored_file.consolidation_id`; `FILE_UPLOAD.after_value.consolidation_id`. После приёма хотя бы одного файла, отдельной
|
||
транзакцией, каждая версия консолидации «в работе» с этим ЮЛ в периметре и статусом INITIATED переходит в
|
||
DOCUMENTS_PROCESSING (§9.1; версии блокируются `FOR UPDATE` по возрастанию `id`, статус перечитывается); сбой перехода не
|
||
отменяет принятые файлы. Блокировка версии для загрузки не нужна. Query `consolidation_version_id` у
|
||
`POST /legal-entities/{id}/files` (Срез 4) удалён.
|
||
9. **Правка формы ББ/ОФР** (`PATCH /documents/{id}/statement`, `DOCUMENT_EDIT`, Срез 3b; решения владельца 07.10.2026).
|
||
Правятся **суммы** ячеек (`values[] {line_id, column_id, amount}`) и три поля шапки: `inn` (10 или 12 цифр, иначе 422
|
||
`INVALID_INN`, в том числе `null` — очистить ИНН нельзя; пересчитывается `counterparty_check`), `report_date` (пересчитываются `period_year`, `period_quarter`,
|
||
`reporting_date` по правилу периода п. 2) и `units` (непустая строка до 50 символов). Код и название строки, добавление и
|
||
удаление строк и колонок не правятся (до v2.0; с v2.0 код и статус строки, дата колонки и наименование организации
|
||
правятся — см. ниже, название строки и удаление строк и колонок — по-прежнему нет). **Только до подтверждения:** документ `CONFIRMED` — 409 `DOCUMENT_READONLY`
|
||
(исправление подтверждённой формы — новым файлом через автозамену, п. 3); файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`; не
|
||
форма — 409 `DOCUMENT_NOT_STATEMENT`; ячейка не этого документа — 422 `UNKNOWN_CELL`. Исходное сохраняется: у значения при
|
||
первой правке `original_amount` = прежняя сумма, `is_modified = (amount ≠ original_amount)` (возврат к исходному снимает
|
||
признак), `modified_by/at`; у поля шапки `*_modified = true`, `*_original_value` не меняется. После правки: акцепт проверок
|
||
сбрасывается (§7; прежний акцепт, включая комментарий, остаётся в `before_value.checks_accepted` записи аудита), проверки и статус пересчитываются (§9.3), пишется одно `audit_event(RECOGNIZED_DATA_EDIT)`; запрос
|
||
без изменений — 200 без аудита. Автозамена при правке не запускается (только при маппинге): правка даты, давшая период
|
||
другого файла ЮЛ, привязывает документ к версии лишь при подтверждении. Порядок блокировок — как у подтверждения (файл →
|
||
документ).
|
||
**С v2.0** (Р-6, Р-13; change `unified-statement-normalization`) PATCH дополнительно принимает:
|
||
`header.organization_name` (непустая строка до 1000 символов; `organization_name_modified = true`,
|
||
`organization_name_original_value` не меняется; пересчитывается `counterparty_check`; снимает `NO_ORGANIZATION_NO_INN`;
|
||
аудит — как у ИНН); `header.report_date`, равный текущему, у поля из `needs_review_fields` — подтверждение даты без
|
||
изменения: поле уходит из `needs_review_fields`, снимается `REPORT_DATE_UNCERTAIN`, аудит (у бланка 66н только с
|
||
относительными подписями неуверенная дата иначе не подтверждается ничем, §11.9);
|
||
`values[] {line_id, column_id, confirmed: true | false}` — подтвердить ячейку без изменения (`is_confirmed`,
|
||
`confirmed_by/at`, `needs_review = false`, `is_modified` не ставится) или снять подтверждение (`is_confirmed = false`,
|
||
`needs_review` пересчитывается); `columns[] {column_id, period_date}` — дата колонке без подписи или с неуверенной
|
||
подписью (обязательная дата, `null` — 422 ошибка валидации; пересчитывает `is_current` и период документа; ставит
|
||
`period_kind = absolute`; та же дата у колонки с `COLUMN_DATE_UNCERTAIN` / `COLUMN_WITHOUT_DATE` — подтверждение,
|
||
предупреждение снимается);
|
||
`lines[] {line_id, line_code}` — назначить или сменить код; `lines[] {line_id, line_status: "detail"}` — отметить
|
||
расшифровкой; `lines[] {line_id, confirmed: true | false}` — подтвердить неуверенный код или снять подтверждение.
|
||
Переходы: `unreadable` / `missing` / `detail` → `ok` назначением кода; `unreadable` / `missing` / `ok` → `detail`
|
||
отметкой; `ok` → `ok` сменой кода. Обратного перехода `detail` → `unreadable` / `missing` нет: ошибочная отметка
|
||
исправляется назначением кода, исходное состояние — перезагрузкой файла. `line_status` в PATCH принимает только
|
||
`"detail"`: `"ok"` без кода — 422 `LINE_CODE_REQUIRED` (в `ok` строку переводит только `line_code`). Назначенный
|
||
код — `line_code_confidence = 100`, `line_code_modified`, `needs_review` строки = `false`; подтверждённый код —
|
||
флаг `is_confirmed`, `needs_review` строки = `false`, `line_code_confidence` и `line_code_modified` не меняются,
|
||
снятие подтверждения возвращает `needs_review` по прежнему правилу (Р-34); `line_code_original_value` не меняется (код из источника; у строк без кода — `null`); после правки родитель **всех**
|
||
строк `detail` пересчитывается как ближайшая строка `ok` выше по `row_index` (нет такой — `null`). Код — 4–5 цифр (422 `INVALID_LINE_CODE`), может
|
||
совпадать с существующим (Р-4); незнакомый словарю — `info`. **Строка, созданная правкой** — `row_index IS NULL`
|
||
(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409 `LINE_CODE_TAKEN`; без сумм — она
|
||
удаляется (её `id` убирается из `document_issue.line_ids`), назначенная строка источника занимает её место. Строки
|
||
источника (`row_index` не `null`) с тем же кодом не удаляются и не мешают — повтор кода законен. Элемент без действия
|
||
или с несколькими — 422 `INVALID_LINE_PATCH`; чужая строка — 422 `UNKNOWN_LINE`, чужая колонка — 422 `UNKNOWN_COLUMN`;
|
||
правка суммы по `line_code` при наличии строки `ok` с этим кодом — 422 `UNKNOWN_CELL` (передайте `line_id`). Удаления
|
||
строк источника нет (первичка не мутируется, `detail` — способ вывести строку из сумм). Строки, созданные правкой:
|
||
`row_index = null`, `source_ref = null`, `line_status = ok`, `line_code_original_value = null`. Правка
|
||
`header.report_date` пересчитывает `period_date` колонок с `period_kind = relative:*` общим кодом (`null` — `null`).
|
||
После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или
|
||
подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue`
|
||
видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица
|
||
§11.9; значения строк `detail` — без `needs_review`, Р-23). **Акцепт** сбрасывается правкой данных — суммы, кода или
|
||
статуса строки, даты колонки, поля шапки; подтверждение ячейки или кода (`confirmed`) и подтверждение даты отчёта или
|
||
колонки тем же значением суммы не меняют и акцепт не сбрасывают (решение владельца 18.10.2026, Р-25). Расхождение `statements_version`
|
||
задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH —
|
||
один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям).
|
||
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
|
||
объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует
|
||
видимость); работает и для `SUPERSEDED`; объекта нет в хранилище — 404 `FILE_CONTENT_NOT_FOUND`. Комментарии к документу —
|
||
`POST/GET /documents/{id}/comments` (таблица `comment`, `object_type = 'document'`); добавлять можно к документу в любом
|
||
статусе; правки и удаления нет; при удалении файла (`DELETE /files/{id}`, п. 5) комментарии его документов удаляются вместе с ними
|
||
(связь полиморфная, FK нет — удаляет сервис).
|
||
11. **Рабочий стол консоли** (`GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`,
|
||
`DOCUMENT_VIEW`; 404 на невидимые консолидацию или ЮЛ; `limit`, `offset`, `include_superseded`): файлы ЮЛ с
|
||
`consolidation_id` этой консолидации **плюс** файлы ЮЛ, у которых есть документ, привязанный (`consolidation_document`)
|
||
к любой версии этой консолидации (подхваченные ОСВ за `Q1` и файлы, загруженные без контекста); новые первыми
|
||
(`uploaded_at` desc, `id`); `SUPERSEDED` — только при `include_superseded=true` (скрыт по умолчанию). Карточка файла та же, что в
|
||
`GET /legal-entities/{id}/files` (`FileOut` + `consolidation_id`). Рабочие версии одного периода видят один стол;
|
||
верифицированная версия заморожена привязками и `ex_snapshot` (Срез 7), отдельного «замороженного списка» в панели
|
||
нет. Панель [39] из карточки версии показывает стол консоли; переключатель «Все документы организации» —
|
||
`GET /legal-entities/{id}/files` с подписью консолидации у файла.
|
||
|
||
---
|
||
|
||
## §10. Роли и матрица прав (ТЗ 8.1, DOC-036960) — seed для `role_permission`
|
||
|
||
| Роль (`role.code`) | DIRECTORY | DOCUMENT | CONSOLIDATION | KFO | EX | EXPORT | USERS | AUDIT |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| ADMIN | VIEW, EDIT, DELETE, ADMIN (импорт) | — | — | — | VIEW | — | ADMIN | VIEW |
|
||
| CURATOR_GO (Куратор КП ГО) | VIEW, EDIT (ГК: создание, перезакрепление) | — | VIEW (по закреплённым ГК) | VIEW | VIEW | — | — | — |
|
||
| KA_RF | VIEW (свой РФ), EDIT (состав ГСК своей ГК) | VIEW, EDIT (загрузка, правка, подтверждение) | VIEW, EDIT (INITIATED…CONSOLIDATION_RF) | VIEW, EXPORT | VIEW | EXPORT | — | — |
|
||
| RKA (РКА РФ) | VIEW, EDIT (состав ГСК в рамках корректировки) | VIEW | VIEW, EDIT (VERIFICATION_RF) | VIEW, APPROVE | VIEW | EXPORT | — | — |
|
||
| KA_GO | VIEW, EDIT (состав ГСК, без ограничения по филиалу) | VIEW, EDIT | VIEW, EDIT (всё, что KA_RF + CONSOLIDATION_GO) | VIEW, VERIFY (№/дата УОБ), EXPORT | VIEW | EXPORT | — | — |
|
||
| EXPERT | VIEW | VIEW | VIEW | VIEW | VIEW | — | — | — |
|
||
| CLIENT_MANAGER | VIEW (свои клиенты) | VIEW, EDIT (только загрузка по своей ГК) | VIEW (статус) | VIEW | — | — | — | — |
|
||
|
||
Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят только ГК своего `regional_office`; редактирование версии — только в
|
||
допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости.
|
||
|
||
**Срез 4 (`domain/policy.py:ensure_version_editable`, решения владельца 06.10.2026, OQ-19):** правка версии (замена
|
||
периметра, ручной статус, ручное сопоставление контрагента) разрешена, когда версия «в работе» (INITIATED,
|
||
DOCUMENTS_PROCESSING, DATA_CORRECTION; иначе 409 `VERSION_NOT_EDITABLE`) **и** у пользователя своя активная
|
||
`consolidation_lock` (нет — 409 `LOCK_REQUIRED`, чужая — 409 `LOCKED_BY_OTHER`). Блокировка закрывает все правки версии, но не
|
||
загрузку и подтверждение документов. Перехват чужой блокировки — любому с `CONSOLIDATION_EDIT`, с обязательной причиной;
|
||
блокировка не истекает по времени (OQ-119). Чтение (свод, чек-лист, экспорт, кандидаты) требует `CONSOLIDATION_VIEW`,
|
||
блокировки не нужно, версия любого статуса читается. Видимость версии — видимость её ГК; чужая версия — 404.
|
||
Порядок проверок: право → видимость → статус/блокировка → операция.
|
||
Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI.
|
||
|
||
### 10.1 Видимость справочников (Срез 1, без проверки кода роли)
|
||
|
||
Правило формулируется через данные, а не через роль (инвариант «права — данные»):
|
||
|
||
- Пользователь **головного офиса** (`app_user.regional_office → regional_office.is_head_office = true`) видит все ГК и все ЮЛ.
|
||
- Пользователь **филиала** видит ГК с `company_group.base_regional_office_id = app_user.regional_office_id`, их ЮЛ, ГСК/ГСЗ,
|
||
а также монозаёмщиков своего филиала (`legal_entity.company_group_id IS NULL AND legal_entity.regional_office_id = …`).
|
||
- Это совпадает с §10: KA_RF / RKA / CLIENT_MANAGER — пользователи филиалов, KA_GO / CURATOR_GO / EXPERT / ADMIN — ГО.
|
||
- ЗО (`business_segment`) в Срезе 1 **не фильтрует**: у ГК и ЮЛ нет поля сегмента (DBML v5.14), сегмент есть только у пользователя
|
||
**[открыто, OQ-86]**.
|
||
- Право (`DIRECTORY_VIEW`, `DIRECTORY_EDIT`, …) отвечает «может ли вообще», policy — «какие именно записи»; обе проверки
|
||
обязательны на каждом маршруте справочников. Запись вне видимости → 404 (не 403), чтобы не раскрывать существование.
|
||
|
||
---
|
||
|
||
## §13. Импорт справочников из выгрузки ЕЦП CRM (Срез 1; формат — `docs/source/biq/ecp-export-lagal-entity.xlsx`)
|
||
|
||
Источник: реальная выгрузка ЕЦП CRM (лист «Выписки_01.04.26», 8400 строк, 345 ГК, 53 значения «Базовый РФ»), ТЗ 6.1 (ГК из
|
||
ЕЦП CRM, создание вручную Куратором), макет [38]. Импорт запускает пользователь с правом `DIRECTORY_ADMIN`
|
||
(seed: `ADMIN`); результат — отчёт + `audit_event(DIRECTORY_IMPORT)` + файл в S3.
|
||
|
||
### 13.1 Формат файла
|
||
|
||
- xlsx (первый лист, первая непустая строка — заголовки) или csv (UTF-8, разделитель `;` или `,` — определяется по строке
|
||
заголовков). Заголовки сопоставляются по нормализованному тексту (нижний регистр, без пробелов, `ё` = `е`):
|
||
`ИНН`, `КПП`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`. Порядок колонок не важен,
|
||
лишние колонки игнорируются. Нет любой из `ИНН`, `iD ГК`, `Группа компаний`, `Организация`, `Тип` → файл отвергается целиком
|
||
(`MISSING_COLUMNS`). `Базовый РФ ГК в CRM`, `ГСК`, `ГСЗ`, `КПП` — необязательные (v5.17: отсутствие колонки `КПП` не ошибка; значение — trim, все пробельные символы убираются, регистр букв приводится к верхнему; число в xlsx читается как целое без дробной части, ведущие нули не восстанавливаются — как у ИНН).
|
||
- Значения: trim; `ИНН` — убрать все пробельные символы; `Тип` — без регистра. Пустые строки пропускаются. Число в ячейке
|
||
xlsx читается как целое без дробной части (`3528052718.0` → `3528052718`); **ведущий ноль, потерянный ещё в Excel
|
||
(`0123456789` → `123456789`), не восстанавливается** — строка получит `INVALID_INN` (ИНН нужно задавать текстом или
|
||
использовать csv) **[открыто, OQ-93]**.
|
||
- Технические пределы файла (не правила предметной области; нарушение — ошибка файла целиком, §13.3): размер ≤ 20 МиБ
|
||
(`IMPORT_MAX_BYTES`, ответ 413), не более 100 000 строк листа/записей csv **включая пустые** (`MAX_IMPORT_ROWS`), суммарный
|
||
распакованный размер частей xlsx ≤ 200 МиБ (`MAX_UNCOMPRESSED_BYTES`); допустимы только расширения `xlsx` и `csv`.
|
||
|
||
### 13.2 Строка → решение
|
||
|
||
Строки обрабатываются в порядке файла; **порядок проверок внутри строки**: тип → ИНН → КПП → дубль ИНН в файле → ГК (пустой
|
||
`iD ГК`, конфликт имени) → РФ → ГК (создание/обновление) → ЮЛ → флаги ГСК/ГСЗ. У каждой строки ровно один **итоговый** код
|
||
(шесть кодов пропуска — `UNSUPPORTED_TYPE`, `INVALID_INN`, `INVALID_KPP`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT`, —
|
||
и `LE_CREATED` / `LE_UPDATED` / `LE_EXISTS`) и ноль или более **сопутствующих** событий той же строки (`RF_*`, `GK_*`,
|
||
`GSK_*`/`GSZ_*`, `MEMBER_ADDED`, `UNKNOWN_FLAG`). Счётчик `errors` = число строк с кодом пропуска.
|
||
|
||
| Шаг | Правило | Код в отчёте |
|
||
|---|---|---|
|
||
| Тип | `ЮЛ` / `ИП` → `entity_type`; иное (`нерез.`, `ФЛ`, `нет инн`, пусто) → строка пропускается **[открыто, OQ-87]** | `UNSUPPORTED_TYPE` |
|
||
| ИНН | ЮЛ — ровно 10 цифр, ИП — 12; иначе пропуск (контрольные разряды в Срезе 1 не проверяются **[открыто, OQ-88]**) | `INVALID_INN` |
|
||
| КПП | v5.17, OQ-31: пустой КПП — допустим (у ЮЛ необязателен). Непустой: у ИП — запрещён, у ЮЛ — по формату §13.5 (9 символов: 4 цифры, 2 цифры/латинские A–Z, 3 цифры); нарушение — строка пропускается с сообщением | `INVALID_KPP` |
|
||
| Дубль в файле | второй и далее ИНН в файле пропускается. ИНН считается «встречавшимся» только после успешных проверок типа и формата, поэтому «ФЛ», идущая после ИП с тем же ИНН, получает `UNSUPPORTED_TYPE`, а не `DUPLICATE_IN_FILE` | `DUPLICATE_IN_FILE` |
|
||
| РФ | по `regional_office.name` (точное после trim); нет → **создаётся** `regional_office(name, is_head_office=false)` с предупреждением (один раз на имя в файле); пусто → `base_regional_office_id = NULL`. РФ разрешается один раз на ГК — по первой строке ГК и только после проверок пустого `iD ГК` и конфликта имени (отвергнутая строка филиал не создаёт); у **существующей** ГК при `skip` филиал из файла всё равно создаётся, но ГК не меняется. Значения «ЦКБ» и «Головная организация» в выгрузке — **[открыто, OQ-89]**: до решения создаются как обычные филиалы | `RF_CREATED` |
|
||
| ГК | **пустой `iD ГК`** → строка пропускается (`GK_NAME_CONFLICT`, сообщение «пустой iD ГК»). По `company_group.crm_id = iD ГК`: нет → создать (`code` генерируется `GK-NNNNNN`, `name`, `base_regional_office_id`, `valid_from = now`); есть → при стратегии `update` обновить `name`/`base_regional_office_id` c снимком в `company_group_history` и `audit_event(GROUP_CHANGE / ASSIGNMENT_CHANGE)`, при `skip` — оставить; **`GK_EXISTS` ставится и для неизменной ГК при `update`** (нет различий в имени и РФ — нечего обновлять; это обеспечивает идемпотентность). Разные имена одной ГК в файле (имя первой строки против последующих) → ошибка ГК, строка пропускается. Архивные ГК (`is_archived`) входят в сопоставление и при `update` обновляются **без разархивации** **[открыто, OQ-94]**. `update` пишет снимок `company_group_history` и событие `audit_event` теми же функциями, что ручная правка (PATCH); смена только имени — `GROUP_CHANGE`, смена РФ (в т.ч. вместе с именем) — одно `ASSIGNMENT_CHANGE` | `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS`, `GK_NAME_CONFLICT` |
|
||
| ЮЛ | по `legal_entity.inn`: нет → создать (`entity_type`, `name`, `company_group_id`, `regional_office_id = base RF ГК`, `valid_from = now`); есть в этой же ГК → `update`: обновить `name`/`entity_type` со снимком `legal_entity_history(UPDATE)`, `skip`: оставить; есть **в другой ГК или вне ГК (монозаёмщик)** → строка пропускается, перевод между ГК только явной операцией (§13.4) **[открыто, OQ-90]**; сообщение называет имя ЮЛ, независимо от филиала инициатора — импорт доступен только `DIRECTORY_ADMIN` **[открыто, OQ-95]**. `regional_office_id` нового ЮЛ = действующий РФ ГК. `LE_UPDATED` пишет снимок `legal_entity_history(UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)` как ручная правка; `LE_EXISTS` — и при `skip`, и при `update` без различий | `LE_CREATED`, `LE_UPDATED`, `LE_EXISTS`, `LE_IN_OTHER_GROUP` |
|
||
| ГСК / ГСЗ | флаг = нормализованное `входит в гск` / `входит в гсз`; любое другое непустое значение → предупреждение и «не входит». Если у ГК есть хотя бы один член с флагом — создаётся (если нет) **одна** группа вида `gsk`/`gsz` с `name` = имя ГК и `code` `GSK-NNNNNN`/`GSZ-NNNNNN` **[открыто, OQ-91]**; член добавляется записью `related_group_member(kind, valid_from = now, is_current)`, если текущего членства **этого вида** (в этой или другой группе) нет — существующее членство импорт не меняет и не закрывает. Флаги обрабатываются **и при `skip`** (недостающее членство добавляется у существующих ЮЛ). Группа создаётся по самому факту флага, даже если у ЮЛ уже есть членство того же вида в другой группе (возможна пустая группа). Значение, не равное ожидаемому для колонки (в т.ч. «входит в ГСЗ» в колонке `ГСК`), — `UNKNOWN_FLAG` (предупреждение, не ошибка: «не входит»). Отсутствие флага членство **не закрывает** (импорт не удаляет) | `GSK_CREATED`, `GSZ_CREATED`, `MEMBER_ADDED`, `UNKNOWN_FLAG` |
|
||
|
||
**Перечень кодов строки (17; совпадает с `ImportCode` в `arm-core/api/src/domain/directory_import.py`):**
|
||
|
||
| Код | Роль в строке | Счётчик отчёта |
|
||
|---|---|---|
|
||
| `UNSUPPORTED_TYPE`, `INVALID_INN`, `INVALID_KPP`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT` | итоговый, строка пропущена | `errors` |
|
||
| `LE_CREATED`, `LE_UPDATED`, `LE_EXISTS` | итоговый | `entities_created`, `entities_updated`, `entities_skipped` |
|
||
| `RF_CREATED` | сопутствующий | `offices_created` |
|
||
| `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS` | сопутствующий | `groups_created`, `groups_updated`, `groups_skipped` |
|
||
| `GSK_CREATED`, `GSZ_CREATED` | сопутствующий (создана группа) | — |
|
||
| `MEMBER_ADDED` | сопутствующий | `memberships_created` |
|
||
| `UNKNOWN_FLAG` | сопутствующий (предупреждение) | — |
|
||
|
||
Допущения по неоднозначностям выгрузки — OQ-87…OQ-91 (в коде — `ASSUMPTION(OQ-nn)`); вопросы, появившиеся при реализации, —
|
||
OQ-92 (пустые «Организация»/«Группа компаний» при непустом `iD ГК` импортируются как есть, с пустым именем), OQ-93, OQ-94, OQ-95.
|
||
|
||
### 13.3 Транзакция, отчёт, след
|
||
|
||
- Один импорт — одна транзакция; ошибки строк не прерывают импорт (строка пропускается), прерывают только ошибки файла.
|
||
`dry_run = true` — тот же прогон с откатом, отчёт идентичен; **в API `dry_run` по умолчанию `true`** (запись — явным
|
||
`dry_run=false`). Номера `GK-`/`GSK-`/`GSZ-` из последовательностей PostgreSQL при откате расходуются (дыры в нумерации
|
||
допустимы). Ошибки файла (прерывают импорт до любых записей, ответ 422): `MISSING_COLUMNS` (с перечнем недостающих
|
||
колонок) и `UNSUPPORTED_FILE` (расширение не `xlsx`/`csv`, повреждённый xlsx, csv не в UTF-8, превышены лимиты строк и
|
||
распакованного размера §13.1); размер файла больше 20 МиБ — 413.
|
||
- Стратегия дублей — параметр `on_existing ∈ {skip, update}` (макет: «Пропустить существующие» — `skip` по умолчанию); `update` обновляет у ГК имя и РФ (РФ — только непустым значением из файла), у ЮЛ — имя и тип.
|
||
- Отчёт: счётчики (`rows`, `groups_created/updated/skipped`, `entities_created/updated/skipped`, `memberships_created`,
|
||
`offices_created`, `errors`) и построчный список `{row, inn, code, message}`. Отчёт возвращается в ответе и пишется в
|
||
`audit_event(action = DIRECTORY_IMPORT, section = DIRECTORIES, object_type = 'directory_import', after_value = отчёт,
|
||
description = имя файла, user_id = инициатор)`. Файл сохраняется в S3 как `uploads/imports/{audit_event_id}/{original_name}`
|
||
и регистрируется в `s3_object(bucket_role = uploads, created_by = user:{id})` (`size_bytes`, `sha256`, `content_type` по расширению); имя файла приводится к basename (путь отбрасывается, управляющие и форматирующие символы убираются, длина ≤ 200 символов с сохранением расширения; пустое → `import`). Запись `s3_object` создаётся **до** `put_object` (сбой S3 откатывает импорт); сам объект кладётся до commit транзакции, поэтому при сбое commit в бакете может остаться объект без записи (зачистка — follow-up arm-core). При `dry_run` файл не сохраняется и `DIRECTORY_IMPORT` не пишется. Без подключённого S3 (dev) файл не сохраняется, в лог пишется предупреждение.
|
||
- Помимо сводного `DIRECTORY_IMPORT` импорт при `update` пишет **собственные** события изменения объектов — `GROUP_CHANGE`/`ASSIGNMENT_CHANGE` (ГК), `LEGAL_ENTITY_CHANGE` (ЮЛ) с `description = «импорт ЕЦП»`; создания объектов отдельных событий не порождают (только сводное, снимок в `*_history` при создании не пишется — он появляется при первом изменении).
|
||
- Повторный импорт того же файла идемпотентен при `skip`: ранее принятые строки получают `GK_EXISTS`/`LE_EXISTS`, новых `MEMBER_ADDED` нет; ранее пропущенные (ошибочные) строки остаются ошибками.
|
||
- Лимит тела запроса (20 МиБ на пути импорта) обязан ставить ingress: multipart разбирается до аутентификации (требование к платформе, `arm-core/README.md`, M-1).
|
||
|
||
### 13.4 Ручные операции справочника (вне импорта)
|
||
|
||
- **Перевод ЮЛ в другую ГК** (макет [79-01]): `legal_entity.company_group_id := новая`, снимок в `legal_entity_history(event_type =
|
||
MOVE_TO_GROUP, effective_date = дата перевода ≥ сегодня, OQ-21)`, все текущие членства ЮЛ в ГСК/ГСЗ старой ГК закрываются
|
||
(`valid_to`, `is_current = false`), `audit_event(MEMBERSHIP_CHANGE)`.
|
||
- **Вывод из состава ГК** (макет [80]): событие `CLOSE` (`close_date`, ЮЛ остаётся с `company_group_id` для истории? — нет:
|
||
`company_group_id := NULL`, `close_date := дата`) или `REMOVE_FROM_GROUP` (`company_group_id := NULL`, без `close_date`);
|
||
членства закрываются; `legal_entity_history(event_type, effective_date)`.
|
||
- **ГСК/ГСЗ:** ЮЛ может входить одновременно в одну ГСК и одну ГСЗ (v5.14), но только в группы **своей** ГК; добавление в
|
||
группу другой ГК → ошибка. Удаление группы — только пустой (без текущих членов) **[OQ-34]**.
|
||
- **Алиасы** (§3): `legal_entity_alias(alias, normalized_alias)`; `normalized_alias` уникален в пределах ЮЛ; конфликт
|
||
с алиасом другого ЮЛ (OQ-28/ANL-028) — ошибка с указанием ЮЛ-владельца.
|
||
- **ГК:** перезакрепление за другим филиалом — `audit_event(ASSIGNMENT_CHANGE)` + снимок `company_group_history`; архив —
|
||
`is_archived` (ГК с активными консолидациями архивировать нельзя — проверка появится в Срезе 4).
|
||
|
||
### 13.5 КПП (v5.17, OQ-31, решение владельца 07.10.2026)
|
||
|
||
- Поле `legal_entity.kpp` (и снимок `legal_entity_history.kpp`), nullable. Формат — ровно 9 символов: `^[0-9]{4}[0-9A-Z]{2}[0-9]{3}$` (4 цифры, 2 символа — цифра или латинская A–Z, 3 цифры); иначе `INVALID_KPP` (422 в API). Контрольных разрядов у КПП нет.
|
||
- **ИП:** КПП запрещён — непустой `kpp` при `entity_type = ИП` → `INVALID_KPP` (создание и PATCH; при PATCH проверяется итоговая пара «тип, КПП»: смена типа на `ИП` у ЮЛ с КПП без очистки КПП — `INVALID_KPP`, а смена типа без смены ИНН невозможна и по длине ИНН — `INVALID_INN`). Для ЮЛ КПП необязателен; очистить его (`null`) можно.
|
||
- **Уникальность:** только `inn` (КПП — атрибут). Сопоставление контрагентов ОСВ по ИНН (§3) КПП не использует.
|
||
- **Изменение КПП** — обычный PATCH ЮЛ: снимок `legal_entity_history(event_type = UPDATE)` с прежними значениями и `audit_event(LEGAL_ENTITY_CHANGE)` с `before/after`, содержащими `kpp`.
|
||
- **Импорт:** колонка `КПП` необязательна; для существующего ЮЛ при стратегии `update` непустой КПП из файла обновляет `kpp` (со снимком, как `name`/`entity_type`), пустая ячейка существующий КПП не стирает.
|
||
- **Поиск ЮЛ:** подстрочный поиск `q` по справочнику ЮЛ (имя, ИНН) ищет и по КПП; точный фильтр `inn` не меняется.
|
||
|
||
Все мутации пишут `audit_event` (`GROUP_CHANGE`, `ASSIGNMENT_CHANGE`, `MEMBERSHIP_CHANGE`, `LEGAL_ENTITY_CHANGE` — v5.14, `DIRECTORY_IMPORT`) с `before/after`.
|
||
|
||
---
|
||
|
||
|
||
---
|
||
|
||
## §11. Нормализация форм ББ/ОФР — общая цепочка (scan-gateway и tabular-parser)
|
||
|
||
**Статус: целевое правило** change `unified-statement-normalization` (RSHB/arm-ka-docs#116; дизайн —
|
||
`openspec/changes/archive/2026-10-19-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 оставлен
|
||
заголовком-ссылкой.
|
||
|
||
Источник требований: ТЗ 6.2 («парсинг ОСВ, бухгалтерского баланса и ОФР»), DOC-037734, DOC-221281, FNR01/FNR02; формат —
|
||
Приказ 66н / ФСБУ 4/2023, на практике — скан бланка (ИС Сканирование), выгрузка 1С («Регламентированный отчёт»), шаблон
|
||
банка и вручную набитая таблица.
|
||
|
||
**Принципы.**
|
||
|
||
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 — не отдельный шаг**, а первая стадия типовых разборов текста там, где уже известен смысл ячейки (дата,
|
||
подпись колонки, код, разлепление, сумма).
|
||
|
||
**Где код.** Шаги 2–8, общие разборы и словари — пакет `arm_statements/` репозитория `arm-statements` (Р-1); в
|
||
tabular-parser, scan-gateway и arm-core он лежит копией (раскладывает и проверяет `sync-statements` в docs, прямые правки
|
||
копии запрещены, маркер версии — `service.statements_version`). Golden-тесты пакета — на синтетических и обезличенных
|
||
данных внутри пакета; тесты на `fixtures/` остаются в сервисах. Изменения правил ниже — только через этот параграф и
|
||
словари пакета.
|
||
|
||
### §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`, `locator =
|
||
file:<имя json>;table:<i>`; документа у такой таблицы нет), остальные таблицы разбираются; как и `JSON_FAILED`, оно
|
||
блокирует `RECOGNIZED` всех документов файла (§9.3); ни одной таблицы не разобрано — `error RECOGNITION_FAILED`.
|
||
|
||
**Перенос из старого ядра.** Источник очистки и разборов — модуль `DKB_ARMKA` `api/src/services/ofr_result_conversion/`
|
||
(`utils.py`, `date_utils.py`, `consts.py`; локально — `reference-old-arm/api/src/core/PDFWorker/`); решение владельца
|
||
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`. ОКЕИ — поле `okei` шапки контракта.
|
||
3. **Ячейки**: `text` — как показывает числовой формат ячейки, `value` — число по формату (tp#42, OQ-138 пересмотрен
|
||
15.10.2026): масштабирующие запятые (`#,##0,` → тысячи), число знаков после точки, секции и условия (`[=0]"-"`,
|
||
`[=-646532]"(647)"`; литерал условной секции разбирается как текст), округление half-up, знак — от сырого значения,
|
||
условия сравниваются с точностью 15 значащих цифр; `General` / пустой формат — значение как в ячейке; формат, который не
|
||
удалось разобрать (`%`, экспонента, дробь, даты, `@`, неизвестные знаки вне кавычек, больше четырёх секций, условие не
|
||
по шаблону, результат вне `numeric(18,2)`), — значение как в ячейке и одно `NUMBER_FORMAT_UNPARSED` (`info`) на документ
|
||
с перечнем форматов; xlsb — стили не читаются, `XLSB_NO_FORMATS` (`info`). `ref = <лист>!<A1>` по правилу Excel: имя
|
||
листа берётся в одинарные кавычки, если в нём есть что-то кроме букв, цифр и `_` или оно начинается с цифры, апостроф
|
||
внутри имени удваивается (`Баланс!C27`, `'Бухгалтерский баланс. Лист 1'!C14`; решение владельца 18.10.2026, Р-22 /
|
||
S-15); `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;
|
||
единицы — словарь единиц пакета `statement_units.yaml` (написания «тыс. руб.», «в тыс. рублей», «руб.», «млн руб.» → `тыс.
|
||
руб.` / `руб.` / `млн руб.`); единицы не найдены по тексту, а ОКЕИ есть — по коду ОКЕИ тем же словарём (384 → `тыс. руб.`,
|
||
383 → `руб.`, 385 → `млн руб.`); ОКЕИ — только цифры. Сырой текст и уверенность каждого поля — в контракт
|
||
(`<поле>_text`, `<поле>_confidence`); ядро хранит их в `financial_statement_meta.header_source`.
|
||
|
||
### §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, у которого код в другой колонке). Колонки документа задаёт первый сегмент; колонки
|
||
следующих сегментов новых колонок не создают, а сопоставляются с колонками первого: у сегмента **со своей шапкой**
|
||
(повторная шапка OCR, лист-продолжение Excel со строкой заголовка — «Бухгалтерский баланс. Лист 4» НПЗ, колонки 8–10
|
||
при 16 / 21 / 24 у первого листа) — по `period_date`, если у каждой колонки сегмента дата есть и совпадает ровно с
|
||
одной колонкой первого, иначе по порядку; у сегмента без шапки колонки значений — столбцы правее кода, где в строках с
|
||
кодами числа, по порядку. Значения несут `column_index` колонки первого сегмента. При сопоставлении по порядку другое
|
||
число колонок — `SEGMENT_COLUMNS_MISMATCH` (`error`): значения по позиции, лишние столбцы сегмента в колонки не
|
||
попадают, их номера — в тексте предупреждения (S-56). Столбец кодов не найден ни так, ни так —
|
||
`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_date` (конец периода колонки; во внутренних типах пакета может называться иначе, в
|
||
контракте и БД — `period_date`) и `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_date = null`, вид
|
||
остаётся относительным, ядро досчитает после правки даты отчёта. Подпись OCR с уверенностью ниже 100, разобранная в
|
||
дату, подтверждается, если дата входит в ожидаемые: эффективная дата, она же минус один и минус два года, 31 декабря
|
||
предыдущего и предшествующего ему года; иначе `COLUMN_DATE_UNCERTAIN` (`error`, колонка). Для этого подтверждения
|
||
эффективная дата строится **только из уверенных источников** — дата отчёта с уверенностью 100, иначе самая поздняя
|
||
абсолютная дата колонок с подписью уверенности 100; нет таких — подпись не подтверждена (неуверенное не подтверждает
|
||
само себя, S-64).
|
||
4. **Строка заголовка** — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя
|
||
над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный
|
||
заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль `title` — только строки выше найденной строки
|
||
заголовка (титульный блок «Дата 30 | 9 | 2025»); в сегменте без заголовка роли `title` нет, строки выше первого кода с
|
||
содержимым идут в данные. Заголовка нет ни у одного сегмента → колонки значений первого сегмента — столбцы правее кода,
|
||
где в строках с кодами числа, `label` пустая, `period_date = null`, `period_kind = none`, одно `HEADER_ROW_NOT_FOUND`
|
||
(`error`; прежний синтез шапки шлюза и «рулинг 06.10.2026» заменены этим правилом). Заголовок есть хотя бы у одного
|
||
сегмента — остальные берут его колонки без предупреждения (листы 2–5 ББ НПЗ без шапки остаются `done`).
|
||
5. **Строка нумерации** «1 2 3 4 5» — роль `numbering`, в данные не попадает (AURORA-1517). Строка — нумерация, если в
|
||
ней не меньше трёх непустых ячеек, каждая после замены букв-двойников (§11.3, «1 2 З 4» → 1 2 3 4) — целое число,
|
||
числа слева направо возрастают на 1, начинаются с 1 или 2 и допускают **один пропуск** («1 2 4 5», «2 3 4 5» без
|
||
первой ячейки; в Excel Межениновской в столбце кодов строк нумерации стоит «2»). Решение владельца 18.10.2026 (Р-22,
|
||
S-13): иначе сбой OCR в нумерации превращает её в строку `unreadable` с «суммами» и блокирует подтверждение.
|
||
Запись нумерации в столбце кодов («2», «3») кодом не считается — вне кодовых диапазонов.
|
||
6. **Подпись без периода** (Р-6): период по словарю (абсолютный или относительный) → колонка с датой; подпись из
|
||
словаря исключений («Изменение», «Отклонение», «%», «Темп», «Доля», «Примечание») → пропуск, `COLUMN_SKIPPED` (`info`);
|
||
иначе, если в столбце есть числа в строках с кодами (OCR-мусор «| | г» с уверенностью 17, обрезанная или самодельная
|
||
подпись), — колонка остаётся с подписью как есть, `period_date = null`, `period_kind = none`, `COLUMN_WITHOUT_DATE`
|
||
(`error`, колонка); аналитик ставит дату через PATCH (§9.5 п. 9); иначе пропуск. Колонка без даты не мешает периоду
|
||
формы (эффективная дата), но межформенная проверка и гудвилл «на отчётную дату» по ней не считаются, пока дата не
|
||
проставлена. Уверенность подписи — `label_confidence` (Excel — 100, подписи нет — `null`).
|
||
**Числа правее таблицы формы** (решение владельца 18.10.2026, Р-44; ответ на вопрос Р-31 плана 4; пример — Межениновская 2025-Q3, ББ, формулы
|
||
аналитика `=M45*5/100` в столбцах U–W над пустой шапкой): если среди колонок значений сегмента есть хотя бы одна с
|
||
подписью периода (дата или относительная подпись бланка), то столбец **без текста подписи**, лежащий правее последней
|
||
такой колонки, колонкой сумм не считается: его числа в значения строк не попадают, `COLUMN_WITHOUT_DATE` нет,
|
||
выдаётся одно предупреждение `NUMBERS_OUTSIDE_TABLE` (`info`, документ) с адресами ячеек («числа правее таблицы формы
|
||
не учитываются: V45, W57, …»: в тексте первые 10 адресов, дальше «и ещё N»; Excel — `<лист>!<A1>`, OCR — `row:<n>`).
|
||
Адреса собираются по всем сегментам формы в одно предупреждение; у сегмента без подписанных колонок (лист-продолжение
|
||
без шапки) столбцы без текста подписи сверх числа колонок документа тоже считаются числами правее таблицы (иначе
|
||
`SEGMENT_COLUMNS_MISMATCH`). Подписанный столбец без чисел в строках с кодами (потерянная подпись объединённой ячейки)
|
||
границей таблицы не служит. На статус документа предупреждение не влияет. Не
|
||
меняется: подписанных колонок нет вовсе; столбец без подписи **между** подписанными — `COLUMN_WITHOUT_DATE`; столбец
|
||
правее с **любым текстом подписи**, не разобранным ни как период, ни как исключение (OCR-мусор «| | г», «Проверка»), — `COLUMN_WITHOUT_DATE`, как раньше. Правило общее для Excel и
|
||
OCR и живёт в пакете `arm_statements` (шаг 2), не в адаптере хоста.
|
||
7. **Столбец наименования** — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с
|
||
наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца,
|
||
пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
|
||
8. **Номер колонки** (`columns[].column_index`, `values[].column_index`, `column_index` предупреждений) — номер столбца
|
||
в сетке кандидата шага 1: у OCR — `column_index` ИС как есть (не обязательно подряд: 2, 4, 5), у Excel — номер
|
||
столбца листа с 0 (столбец D — 3). Номер уникален в документе; колонки сегментов без шапки сопоставляются с колонками
|
||
первого сегмента по порядку и несут их номера. Предупреждение о столбце, которого нет среди `columns[]`
|
||
(`COLUMN_SKIPPED`), несёт номер этого столбца в сетке; ядро хранит его в `document_issue.column_index` (колонки формы
|
||
нет — `column_id = null`).
|
||
|
||
### §11.4 Шаг 3 — отбор строк данных и статусы
|
||
|
||
Строка данных — строка сетки с ролью `data`, у которой есть **содержимое**: в ячейке кода есть хотя бы одна цифра
|
||
(разобранный код или мусор вроде «44 30\nI |» aep_6), или хотя бы одна ячейка значений даёт сумму (`amount` не `null`),
|
||
или непуста с уверенностью ниже 100 (признак потерянной суммы: «[190 :», «" :» в aep_3), или её текст — «цифра под видом
|
||
буквы» (после замены двойников без условия соседства есть цифра и нет других букв: `'I'` с уверенностью 100 в строке 38
|
||
aep_6). Строка только с текстом наименования — не строка данных (заголовок раздела); текст из настоящих букв в колонке
|
||
значений при уверенности 100 (подписи под формой Excel: «Руководитель … Косарев») — тоже нет, осознанное исключение
|
||
(S-72). Строка `unreadable` без значений («44 30\nI |» при пустых суммах)
|
||
— строка данных: документ `needs_check` (не все строки `ok` / `detail`), но подтверждение она не блокирует
|
||
(`LINES_UNRESOLVED` — только строки со значениями, §9.5 п. 4). У строки: `row_index` (номер в документе с
|
||
0, по порядку сетки после разлепления), `ref` (уникален в документе: OCR `row:<row_index ИС>`, разлепленные
|
||
`row:<i>#<k>` с k от 1; Excel — адрес ячейки столбца кодов строки `<лист>!<A1>`, без столбца кодов — первой непустой
|
||
ячейки), статус, ячейки кода и наименования, ячейки значений по колонкам сегмента, `parent_ref`. **Статусы:**
|
||
|
||
- `ok` — код разобран;
|
||
- `unreadable` — ячейка кода содержит хотя бы одну цифру, но кодом не разобралась («ИЧ,250», «[190 :»); ячейка без
|
||
единой цифры («}», «:», «изо :» — двойники вне соседства с цифрой цифрами не считаются, S-61) считается пустой;
|
||
- `detail` (Р-8) — строка без кода с содержимым, над которой есть строка `ok` (в том же или в предыдущих сегментах —
|
||
родитель продолжается через разрыв страницы): расшифровка («в том числе: …», «из них …», «Оборудование к установке»,
|
||
«Товар (41.01)»); родитель — ближайшая строка `ok` выше (`unreadable` / `missing` между ними пропускаются). Сервисы её
|
||
отдают, ядро хранит; в проверки §7, агрегаты, `needs_review` и `done` она не входит (у её значений `needs_review =
|
||
false`, Р-23); экран по умолчанию не показывает; после правок ядро пересчитывает родителей всех строк `detail`;
|
||
- `missing` — строка без кода с содержимым, над которой во всём документе нет строки `ok` («Выручка» 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)`**, один для обоих, по стадиям (S-57): (а) прочерк (`-`, `–`, `—`, `−`
|
||
без цифр) и пусто → `amount = null`, `is_recognized = true` (OQ-148); (б) крайние `|` удаляются (до замены
|
||
двойников), затем двойники вплотную к цифре (§11.3); (в) **ядро числа** — от первого из «цифра, `(`, `-`, `−`, `+`»
|
||
до последнего из «цифра, `)`, `}`, `]`»; всё вне ядра — краевой мусор (`:`, `=`, `■`, `•`, кавычки, `{`, `[` …),
|
||
выбрасывается с `is_recognized = false` («: : 2856983 = ■» → 2856983, «| : 122477» → 122477); (г) в ядре допустимы
|
||
только цифры, пробелы (NBSP, тонкие), `,`, `.`, знак в начале и одна пара скобок вокруг всего числа (`(` первым, `)`,
|
||
`}` или `]` последним — `}` / `]` исправление, OQ-147: «(7744536}» → −7744536, `is_recognized = false`; `{` и `[`
|
||
открывающей скобкой не считаются); иначе — буква в ядре, скобка без пары или не на краю, знак не в начале, другой
|
||
символ между цифрами — `amount = null`, `string_value` = текст, `is_recognized = false` («211(1 |», «(Ю4)», «{38378)»);
|
||
(д) цифр нет — `amount = null`, `string_value` = текст. Разделители:
|
||
- `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): в 2025 году формы сдают и на
|
||
бланке 66н (Межениновская, 3 кв. 2025), и выбор только по дате не подтвердил бы её код 1120 (в перечне `fsbu4` его нет). Прежний выбор редакции шлюза для имён (по дате, по кодам, «редакция не
|
||
определена» / «не заполнена» → `needs_check`) отменён.
|
||
|
||
### §11.7 Шаг 6 — тип формы, вариант, документ
|
||
|
||
1. **Тип** — большинство кодов строк `ok` в отрезке ББ → `BALANCE`, ОФР → `INCOME`, прочих форм и пояснений →
|
||
`DISCLOSURE`; совпало с `type_hint` или `type_hint = null` — тип по кодам; противоречит — тип по кодам и `TYPE_CONFLICT`
|
||
(`error`); кодов нет — `type_hint`. Поиск столбца кодов (шаг 2 п. 1) выполняется **всегда**, в том числе при
|
||
`type_hint = DISCLOSURE`: подпись «Пояснения» или имя листа «Расшифровка» над таблицей с кодами 1110–1700 не должны
|
||
превратить баланс в пустое пояснение — большинство кодов в отрезке ББ или ОФР даёт тип по кодам и `TYPE_CONFLICT`
|
||
(`error`). Без столбца кодов: `type_hint` есть → документ без строк (`DISCLOSURE` — `done`, иначе `needs_check` с
|
||
`CODE_COLUMN_NOT_FOUND` `error`); `type_hint` нет → у OCR таблица ИС с содержимым (хотя бы одна ячейка с цифрой
|
||
или «цифрой под видом буквы») — предупреждение **файла** `TABLE_LOST` (`error`, `locator = file:<имя json>;table:<i>`),
|
||
таблица только из текста — `TABLE_SKIPPED` (`info`, файл, S-71); у Excel — лист «прочее», предупреждение **файла**
|
||
`SHEET_OTHER` (`info`, `locator = sheet:<имя>`); документа во всех случаях нет. `DISCLOSURE` (по `type_hint` без
|
||
кодов форм или по кодам прочих форм) — шаги 3–7 не выполняются, `columns` и `lines` — пустые массивы (ядро строки
|
||
пояснений не хранит), `checks` нет, документ `done`.
|
||
2. **Вариант формы** `form_variant` — только по структуре кодов всех строк `ok` (ядро определяет его заново при каждом
|
||
пересчёте после правки; ОКУД 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, Р-21). Проверка §7 — арбитр, уверенность — подсказка там, где
|
||
проверки нет. Правила ниже — для значений строк `ok`, `unreadable`, `missing`; у значений строк `detail` `needs_review =
|
||
false` всегда (Р-23, решение владельца 18.10.2026): кода у них нет, покрыть их нельзя, в §7, агрегаты и статус они не
|
||
входят, сомнение видно в подсказке ячейки (`text`, уверенность).
|
||
|
||
- Значение **нуждается в покрытии**, если `confidence < 100` (Р-5: у ИС уверенность дискретная — 100, ~80, 71, 67, 60,
|
||
50, 25, 0; в группе 70–89 есть настоящие ошибки, например «11 30» с уверенностью 80) или `is_recognized = false`
|
||
(грязный текст, замена двойников, разлепленная строка). Чистые значения в покрытии не нуждаются.
|
||
- Значение **покрыто**, если `amount` значения не `null` и в его колонке есть соотношение с участием его кода, которое
|
||
посчитано и сошлось: **жёсткое** — в пределах допуска ±1 или округления 0,001 %; **мягкое** — только точно, в пределах
|
||
допуска ±1, без «округления» (Р-21, решение владельца 18.10.2026). Мягким покрываются строки, у которых жёстких
|
||
соотношений нет: 2400, 2410, 2411, 2412, 2460 (ОФР полного набора; `2400` упрощённого — тоже мягкое, S-5), 2510, 2520,
|
||
2530, 2500 (`2500 = 2400 + 2510 + 2520 + 2530`), пятизначные подстроки («сумма подстрок = родитель»).
|
||
- **Знак** члена `abs_terms`, нуждающегося в покрытии: подтверждён, если форма знака совпадает с формой строгого
|
||
большинства **остальных** членов `abs_terms` той же колонки с непустым `amount`, `is_recognized = true` и без правки
|
||
аналитика (сама ячейка не голосует); форма знака — по результату разбора `parse_amount`: «скобки» (пара, в том числе с
|
||
`}` / `]`), «минус», «без знака»; голосующих нет или голоса поровну — не подтверждён (S-66; то же правило — при
|
||
покрытии мягким соотношением, например 2410 в `2400 = 2300 − 2410 + 2420`). «(7744536}» подтверждается, если 2210 и
|
||
2220 той же колонки в скобках; сомнительное «38378» без знака при остальных в скобках — подсвечивается; «{38378)»
|
||
разбором даёт `null` (§11.5) и подсвечивается как непокрытое.
|
||
- `needs_review = true`, если значение нуждается в покрытии и не покрыто или его знак не подтверждён; а также у всех
|
||
ячеек жёсткого соотношения, не сошедшегося вне допуска и округления, независимо от уверенности. Иначе `false`: сырой
|
||
текст и уверенность остаются в подсказке ячейки. Мягкое соотношение, не сошедшееся точно, подсветки не даёт.
|
||
|
||
Не покрыто проверками и остаётся на уверенности: строки 29xx (прибыль на акцию), 2420 и 2530 вне сошедшихся мягких
|
||
соотношений, детализация без строки-родителя, `DISCLOSURE`, колонки, где код не участвует ни в одном посчитанном
|
||
соотношении. Риск двух взаимно компенсирующих ошибок в одном соотношении принимается. Закрывает AURORA-1524 без ущерба
|
||
для 1519.
|
||
|
||
**Код строки** (Р-5, Р-12, Р-16, Р-20). Код неуверенный — `line_code_confidence < 100` или заменён двойник; структурная
|
||
проверка — **только для неуверенных кодов** (у Excel и чистых OCR-кодов расхождение с перечнем — `info` ядра
|
||
`CODE_NOT_IN_EDITION`, не `needs_check`: пятизначные подстроки 1С и нестандартные коды клиента не блокируют
|
||
автоподтверждение). В пакете — **упорядоченный перечень кодов** каждой формы по редакциям (66н, ФСБУ 4) и вариантам, без
|
||
наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня —
|
||
`contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`):
|
||
объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215,
|
||
1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; код без образца в перечень не входит (кандидаты — OQ-168;
|
||
временное правило утверждено владельцем 18.10.2026, Р-26: неуверенный такой код на скане — `needs_review` строки),
|
||
упрощённые варианты — по бланку (OQ-166). **Законные повторы кода** (Р-20, решение владельца 18.10.2026) — часть перечня
|
||
(`repeats`): у `fsbu4` — 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощённой
|
||
системе налогообложения» (печатная форма 1С, AURORA-1431), у `66n` — раздел III некоммерческой организации после раздела
|
||
III (aep_3). Алгоритм: четырёхзначные коды строк `ok` документа — последовательность (коды, которых нет в перечне
|
||
редакции, в её последовательность не входят); для **каждой** редакции перечня (66н и ФСБУ 4 — всегда обе: Межениновская
|
||
за 3 кв. 2025 сдана на бланке 66н) — **лучшая** подпоследовательность, согласованная с порядком перечня варианта формы
|
||
документа (повтор допустим столько раз, сколько код стоит в перечне): сначала наибольшее число уверенных кодов (якоря;
|
||
противоречащие по порядку якоря все войти не могут — входит наибольшее согласованное множество), затем наибольшая
|
||
длина; редакция — с лучшей (при равенстве — по эффективной дате: `fsbu4` после 2024-12-31, OQ-145); неуверенный код
|
||
подтверждён, только если входит во все лучшие подпоследовательности выбранной редакции (S-65). Вариант формы — по всем
|
||
строкам `ok` (§11.7); ядро пересчитывает вариант и проверку после правки кода. Пятизначный код
|
||
подтверждён, если подтверждён его родитель (§7) и подстрока стоит после него. Повтор кода, которого **нет в перечне**
|
||
(или копий больше, чем в перечне), у которого хотя бы одна копия неуверенна, — `needs_review` у неуверенной копии (Р-16);
|
||
повтор из перечня подтверждается структурой, как любой код. Неподтверждённый неуверенный код — `needs_review` строки,
|
||
документ `needs_check`. Подмену неуверенного кода на незапечатанного соседа того же раздела (1110 → 1120) структура не
|
||
ловит — это закрывает ядро: для строк с неуверенным кодом условие `RECOGNIZED` включает совпадение распознанного
|
||
наименования со справочным (§9.3).
|
||
|
||
**Совпадение наименований** (ядро; S-4 с оговорками ревью, решение владельца 18.10.2026): обе строки — нижний регистр,
|
||
`ё` → `е`, всё кроме букв и цифр → пробел, пробелы схлопываются, крайние обрезаются (`strip`); совпало, если строки
|
||
равны, или более короткая (≥ 5 символов) входит в более длинную, или `difflib.SequenceMatcher` ≥ 0,8 — с **любым**
|
||
справочным именем кода в любой редакции словаря ядра (у кода может быть несколько имён: синонимы и законные повторы —
|
||
Р-20). Исключение: если хотя бы одно из сравниваемых имён начинается с «итого по разделу», сравниваются **номера
|
||
раздела** — начальные буквы `i` / `v` / `x` первого слова после фразы (кириллические «і», «х» приводятся к латинским,
|
||
хвост слова отбрасывается: «Итого по разделу V1» Счастливочной и «Итого по разделу V /» РБ → V); совпало ⇔ номера равны;
|
||
номер не выделен — только равенство строк (S-60). Без исключения «итого по разделу i» входило бы в «итого по разделу ii»
|
||
— подмена 1100 ↔ 1200 ↔ 1300 не ловилась бы именем. В словарь имён ядра `fsbu4` в PR перехода добавляются вторые имена
|
||
законных повторов 1220, 1520, 2410 и коды 1105 «Гудвил», 1130, 1140, 1530, 2460 (спека `arm-core`).
|
||
Кода нет в словаре или наименование пусто — не совпало; несовпадение — `NAME_MISMATCH` (`info`) у строки. По
|
||
`fixtures/ocr` коды часто приходят с уверенностью 50 и 25 — требовать 100 нельзя.
|
||
|
||
**Поля шапки.** Дата отчёта с уверенностью ниже 100 подтверждена, если совпала с самой поздней абсолютной датой колонок
|
||
с подписью уверенности 100 (неуверенная подпись дату отчёта не подтверждает, S-64);
|
||
иначе `report_date` попадает в `needs_review_fields` и `REPORT_DATE_UNCERTAIN` (`error`) — неверно прочитанный год давал
|
||
бы неверный период и автоподтверждение. **У бланка 66н только с относительными подписями** («На отчётную дату», …)
|
||
абсолютных дат колонок нет, и неуверенная дата отчёта структурой не подтверждается никогда — такой документ всегда
|
||
`needs_check` с `REPORT_DATE_UNCERTAIN`; аналитик сверяет дату с бланком и подтверждает её (PATCH `header.report_date`
|
||
тем же значением или исправленным, §9.5 п. 9). ИНН и организация с уверенностью ниже 100 — `info` (их сверяет ядро: ИНН
|
||
— `MISMATCH` с ЮЛ, организация — по алиасам), в `needs_review_fields` не попадают (`needs_review_fields` — поля, которые
|
||
держат документ в `needs_check`; подсветка ИНН и организации на экране — по предупреждениям с `field`; S-10, решение
|
||
владельца 18.10.2026); единицы и прочие поля на статус не влияют.
|
||
|
||
**Предупреждение** — структура `{code, severity, message, line_refs?, column_index?, field?}`; уровень файла
|
||
(`metrics.warnings`) — ещё `locator?`. Место — `locator` документа (у предупреждения файла — его `locator`), не текст.
|
||
`line_refs` — только `ref` строк из `lines[]` (о строках ролей `title` / `header` / `numbering` — в `message`);
|
||
`column_index` — номер столбца сетки (§11.3 п. 8). Принцип: `error` — только то, что меняет данные или их полноту;
|
||
`error` → `needs_check`, `info` на статус не влияет. Предупреждение уровня файла с `error` блокирует `RECOGNIZED` всех
|
||
документов файла (§9.3).
|
||
|
||
| Код | Уровень | Когда | Чем снимается в ядре |
|
||
|---|---|---|---|
|
||
| `JSON_FAILED` | error, файл | json набора ИС ошибочен при успешном соседе | перезагрузка |
|
||
| `TABLE_PARSE_ERROR` | error, файл | исключение при разборе таблицы ИС (`locator` таблицы; документа нет) | перезагрузка |
|
||
| `TABLE_LOST` | error, файл | таблица ИС с содержимым (ячейка с цифрой или «цифрой под видом буквы») без столбца кодов и без типа | перезагрузка |
|
||
| `TABLE_SKIPPED` | info, файл | таблица ИС только из текста без столбца кодов и без типа (документа нет) | — |
|
||
| `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` | — |
|
||
| `XLS_NO_CODEPAGE` | info, файл | книга xls без кодовой страницы — кириллица могла прочитаться неверно (формы и ОСВ книги) | — |
|
||
| `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 «прочее» без типа и кодов (`locator = sheet:<имя>`; документа нет) | — |
|
||
| `ROW_SPLIT` | info | разлеплена склеенная строка | — |
|
||
| `NUMBERS_OUTSIDE_TABLE` | info | числа правее последней подписанной колонки значений не учтены (шаг 2, п. 6); адреса ячеек — в тексте | — |
|
||
| `OSV_*` | info | предупреждения листа ОСВ (§1) | — |
|
||
| `CODE_NOT_IN_DICTIONARY`, `CODE_NOT_IN_EDITION`, `NAME_MISMATCH`, `STATEMENTS_VERSION_MISMATCH` | info, ядро | проверки ядра (§9.3, §9.5) | пересчёт |
|
||
| `RESULT_STATUS_INCONSISTENT` | info, ядро | `document.status = done` из результата противоречит строкам, `needs_review`, нарушениям или предупреждениям документа — ядро считает его `needs_check` (§9.3) | пересчёт после правки |
|
||
| `MAPPING_WARNING` | info, ядро | предупреждение маппера 1.x по документу (повтор кода, значение вне колонок) | — |
|
||
| `LEGACY_WARNING` | info, файл | строка `metrics.warnings` результата 1.x (`recognition_job.metrics`) | — |
|
||
| `RELATION_FAILED` / `RELATION_SOFT` | error / info | код записи `document_issue` вида `relation` / `soft` — нарушение §7 из `checks` (или пересчёта ядра) с `codes`, `relation`, `expected`, `actual` | пересчёт после правки; `relation` — ещё акцепт |
|
||
|
||
Назначение кода и отметка `detail` снимают нарушения, пересчитываемые §7. Новый код предупреждения вводится словарём
|
||
пакета и строкой этой таблицы; в контракте `code` — строка, не перечисление.
|
||
|
||
**Статус документа** — одна функция общего кода: `done` ⇔ раскладка найдена ∧ (организация или ИНН) ∧ период определён
|
||
(дата отчёта или самая поздняя абсолютная дата колонок) ∧ все строки `ok` или `detail` ∧ нет значений, строк и полей
|
||
шапки с `needs_review` (значения строк `detail` не учитываются, Р-23) ∧ нет жёстких нарушений §7 вне допуска и
|
||
округления ∧ нет предупреждений документа уровня `error`; иначе `needs_check`. `DISCLOSURE` — `done` без условий о
|
||
раскладке, строках и проверках. Квартал периода,
|
||
сверка ИНН и наименований — знание ядра (§9.3), в `done` не входят. У ОСВ `document.status = done`, когда контрольные
|
||
суммы §1.4 листа сошлись (§1.6, S-22), её предупреждения — `info`.
|
||
|
||
**Интерфейс функции статуса** (K-14) — один для сервиса и ядра, на входе только данные, без типов источника:
|
||
`document_type`; `layout_found` (сервис — столбец кодов найден хотя бы у одного сегмента; ядро — у формы есть хотя бы
|
||
одна строка из источника, `row_index` не `null`); `has_organization_or_inn`; `period_known` (дата отчёта или абсолютная
|
||
дата хотя бы одной колонки — у ядра `document.reporting_date` не `null`); строки `{line_status, needs_review}`; значения
|
||
`{line_status строки, needs_review}` (значения строк `detail` функция не учитывает); число жёстких нарушений вне допуска и округления; `accepted` (акцепт — жёсткие нарушения не
|
||
учитываются); уровни предупреждений документа (у ядра — записи `document_issue` вида `warning` с `severity = error`).
|
||
`needs_review` строк и значений на входе уже учитывает подтверждения: у ядра правленое (`is_modified`,
|
||
`line_code_modified`) или подтверждённое (`is_confirmed`) — `false`. Сервис собирает вход из `FormCandidate` и
|
||
результата шагов 2–7, ядро — из строк БД (после правки — точкой входа пакета для пересчёта по сохранённым данным,
|
||
спека `statement-normalization`; при приёме результата — для проверки согласованности `done`, §9.3); так один код даёт
|
||
один статус.
|
||
|
||
**Ожидание на реальных сканах** (`fixtures/ocr`, оценки ревью спеки 18.10.2026, K-4 и L-1): после Р-20 (повторы 1220 /
|
||
1520 / 2410 в перечне `fsbu4` и синонимы в словаре имён), Р-21 (мягкие соотношения ОФР) и Р-23 (значения `detail` не
|
||
держат статус) законные повторы, строки 24xx / 25xx и ячейки расшифровок документ не держат. По прогону ревьюера ББ и
|
||
ОФР РБ («Бухгалтерская отчетность 1 кв.25») и Счастливочной («БО 1 кв 2025») могут стать `done`: все их неуверенные
|
||
значения строк с кодом покрыты сошедшимися соотношениями, неуверенные коды подтверждаются перечнем `fsbu4`, а держали
|
||
их только ячейки расшифровок. `needs_check` остаётся там, где есть что смотреть: строки `unreadable` / `missing` с
|
||
суммами (aep_3), несошедшиеся соотношения и мусор в значениях (aep_6: «(Ю4)» в 2350), неуверенная дата отчёта.
|
||
Golden-эталоны `fixtures/ocr/expected_normalize/` пересчитываются в PR scan-gateway (шаг 5 выкладки), фактическая доля
|
||
`done` — в его описании.
|
||
|
||
**Статус конверта:** `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` (OQ-148 закрыт решением владельца 18.10.2026, Р-22 / S-7) |
|
||
| `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); коды
|
||
перечня без образца в fixtures (OQ-168);
|
||
несколько периодов в одном файле ИС (OQ-167, допущение владельца); переключатель «как в файле» на экране (Р-14).
|
||
|
||
Что это даёт ядру: один маппинг `statementDocument → document + financial_statement_* + document_issue` для PDF и Excel;
|
||
экран «Проверка распознавания» показывает статусы строк и предупреждения у места.
|
||
|
||
---
|
||
|
||
## §12. ББ и ОФР из Excel — см. §11
|
||
|
||
Правила разбора форм из 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.
|