§1.4: строки разреза «Период» сверяются цепочкой (обороты — Σ, сальдо начала — первый период, конца — последний, непрерывность между периодами); развёрнутый родитель над свёрнутым субсчётом сверяется по нетто только при цепочке нетто-строк (узкий вариант, решение владельца 28.10.2026), риск RR-5. §1.3: уточнение иерархии по отступам — подуровень счёта на том же отступе, безымянная строка закрытия, вложенность по суммам у родителя с нарушением с тремя защитами; риск RR-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
295 KiB
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/ — это примеры двух организаций, а не спецификация формата. Парсер обязан:
- Найти лист(ы) с ОСВ по содержимому: ячейка в первых 30 строках листа (
SHEET_MARKER_ROWS: шапка ведомости всегда над таблицей), матчащаяоборотно[- ]сальдовая ведомость(без учёта регистра, «ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколькоdocument. Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать. - Найти заголовок таблицы по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…»,
«Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит»,
«Дт/Кт», «Д-т/К-т»). Обязательны группы «обороты» и «сальдо на конец»; отсутствовать может только «сальдо на
начало» (иначе заголовок не найден →
INVALID_FORMAT, §1.6). Заголовок может занимать 1–4 строки с merged-ячейками; группировки могут быть как под «Счет» в первой колонке, так и строкой «Субконто», так и перечислением через запятую (вариант 2). - Определить колонки сумм по тексту, а не по позиции: для каждой из трёх групп («начало», «оборот», «конец»)
найти подзаголовки «Дебет»/«Кредит» в следующей строке под ней (с учётом merged-диапазонов). Допустимы ОСВ без
«начала» (только обороты и конец), с одной свёрнутой колонкой сальдо (знак = сторона), с дополнительными
колонками («Количество», «Валюта», «ИНН», «КПП», «Код»). Неизвестные колонки — сохранять в
extraстроки, не терять. - Определить колонку текста строки — первая непустая слева от сумм (обычно A, но не обязательно); ИНН — колонка с
заголовком
ИННили колонка, где ≥ 80 % непустых значений — ровно 10/12 цифр (число из 9/11 цифр при этом выборе нулём не дополняется, иначе числовой КПП сошёл бы за ИНН — R-24). - Определить иерархию строк по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) —
по отступу (ведущие пробелы /
alignment.indent), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется вmetrics.warningsкакsheet=<имя листа> hierarchy=<outline|indent|account_pattern|sums|trivial>. Строка «Итого» в каскаде не участвует (всегда уровень 0); при меньше чем двух строках данных (пустая ОСВ) —trivial. - Шапку документа (организация, счёт, период, «выводимые данные») искать регулярками по всем ячейкам выше заголовка
таблицы; отсутствие любого поля — не ошибка, а
null+ warning (ядро попросит аналитика уточнить при проверке). - Самопроверка результата (контрольные суммы §1.4) — единственный способ понять, что структура угадана верно.
Если тождества не сходятся — не «подгонять», а вернуть
needs_checkс диагностикой, какой шаг не уверен. - Правила парсера — данные, а не код: словари синонимов заголовков, группировок, периодов лежат в
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.
Плоская форма 1С по одному счёту (реализовано, tabular-parser #59, AURORA-1661, решения владельца 16.10 и 19.10.2026). Пример — выгрузка «Оборотно-сальдовая ведомость по счету: 76.Н.1»:
r2 Оборотно-сальдовая ведомость по счету: 76.Н.1 ← код счёта — и через двоеточие
r3 Контрагенты ← строка вида субконто (у файла-письма — «Организации»)
r4 за 2025 г.
r5 ООО "Товарное хозяйство" ← организация (или хвост «План счетов <организация>»)
r9 Субконто | Сальдо на начало периода | Обороты за период | Сальдо на конец периода
r11 Август - Агро ООО | … ← сразу контрагенты, строки-счёта нет
- Код счёта после «по счету» читается и через двоеточие («по счету: 76.Н.1»). Счёт шапки важнее счёта строки таблицы: фолбэк «код первой строки-счёта» (выше, OQ-134) работает, только если в шапке кода нет.
- Строка шапки между названием ведомости и периодом — строка вида субконто, а не организация. Её элементы
(через запятую) — виды субконто: известный вид (
osv_groups.yaml, явная записьmapping: «Контрагенты», «Организации» — синонимCOUNTERPARTY, «Договоры» …) становится группировкой схемы послеACCOUNT, если схема варианта 1 — однаACCOUNT(«Субконто»); неизвестный вид («Материалы») в схему не попадает. - Организация — по порядку: хвост «План счетов <организация>» (
header_markers.chart_of_accounts) → первая не-метка шапки вне строки вида субконто → хвост названия ведомости.
Шапка выгрузки «ОСВ по счёту … по контрагентам» (AURORA-1714, решение владельца 27.10.2026; tabular-parser #71). Пример — выгрузка 1С «ОСВ по счету 60 по контрагентам» (в файле нет слов «по счету»):
r1 MBC2009 | Оборотно-сальдовая ведомость
r2 15 июля 2025(18:40) | За период | 01.01.25..30.06.25 ← метка и значение в разных ячейках
r3 | Счет Поставщика | 60* ← метка и код счёта в разных ячейках
r4 | Счет Клиента | ← метка без значения
r6 Субконто | Начальное сальдо | …
r8 | | Поставщики ← строка вида субконто внутри таблицы
- Счёт шапки. Метки
Счет Поставщика/Счет Клиента(header_markers.account, данные) — как «по счету»; код — в той же ячейке после метки либо, если там кода нет, в следующей ячейке строки. «60*» — все субсчета счёта: код «60» (знак*кодом не является). Метка без значения пропускается; первая метка со значением определяет счёт шапки. - Период. Метка
За период(header_markers.period) со значением в соседней ячейке строки; ячейки метки и значения организацией не считаются. Диапазон дат01.01.25..30.06.25(разделитель..— наравне с пробелом/«по»;periods.yaml) разбирается как «с 1 января по конец квартала»; год из двух цифр — 20yy (год конца диапазона). Ожидаемо для примера: 2025, Q2, нарастающий (§1.5); границы 01.01.2025–30.06.2025. - Вид субконто в таблице. Если первая строка данных состоит из одной текстовой ячейки вне колонки текста (в колонке
суммы), а текст — известный словарю групп вид субконто (
osv_groups.yaml: «Поставщики» — синонимCOUNTERPARTY, как «Контрагенты»), строка — не данные: она не получает суммы, предупреждение «нечисловое значение в колонке сумм» не выдаётся, а вид субконто добавляется вgroup_schemaпосле ACCOUNT так же, как строка вида субконто шапки плоской формы выше (схема из одного ACCOUNT). Текст в колонке текста («Поставщики» как название контрагента) этим правилом не затрагивается.
Нормализация значений шапки в типы:
| Текст в шапке | 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; метка колонки
«Показатели» — ниже; по умолчанию — из шапки, а если шапка вид данных не называет — BU с предупреждением).
«Вал» без точки — не метка валюты: так называется материал (счёт 10); валютная строка — «Вал.» с точкой или код валюты из словаря (реализовано, tabular-parser #47, AURORA-1663). «Кол-во» — строка количества, как «Кол.» и «Количество» (там же); то же с единицей измерения после запятой — «Кол-во, га», «Кол-во, кг» (tabular-parser #66, AURORA-1709).
Строки «Вал.», «Кол.», «Кол-во», «В валюте» и трёхбуквенный код валюты («USD») — строки-показатели: не уровень
группировки, а вид сумм родителя: group_type = OTHER, data_type = CURRENCY | QUANTITY, позицию в group_schema не
занимают, не наследуют counterparty_name/counterparty_inn (контрагента потребитель находит по родителю),
account_code_raw наследуют от ветки; их суммы остаются в шести полях, но в проверки родителя и «Итого» не входят
(§1.4). Родитель строки-показателя — предыдущая строка данных (ближайшая строка выше, которая сама не показатель);
уровень — на один глубже родителя; в каскад иерархии строки-показатели не передаются, так что не ломают дерево строк
БУ, даже если стоят на одном outline-уровне с ним. Строка «Валюта <код>» («Валюта USD») — не показатель: обычная
строка аналитики (OTHER) с рублёвыми суммами; код валюты отдельно не хранится (решение владельца 19.10.2026,
AURORA-1661; прикладной сценарий валютных строк — аналитика AURORA-1670).
Колонка «Показатели» (решение владельца 19.10.2026, AURORA-1613). 1С печатает вид суммы строки отдельной колонкой
между колонкой текста и суммами (заголовок «Показатели», часто с переносом «Показа-тели»; словарь
osv_headers.yaml — service_columns.indicator): на каждую строку аналитики — строка «БУ», под ней «НУ», «Кол.»,
«Вал.», иногда «ПР»/«ВР» (постоянные и временные разницы); текст строк-показателей в колонке текста пуст или повторяет
объединённую ячейку строки БУ. Метка читается по словарю data_types.yaml — indicator_column: «БУ» — обычная строка с
data_type = BU; «НУ» → NU, «Кол.» и «Кол. доп.» (дополнительное количество, сч. 11; AURORA-1709) → QUANTITY,
«Вал.» → CURRENCY — строки-показатели (как выше); «ПР», «ВР» и метка не
из словаря — строки-показатели с data_type = null (в контракте нет значений для разниц) и, для метки не из словаря,
предупреждением. row_value строки-показателя — метка. Строки НУ хранятся как пришли (OQ-45), собственных проверок
«дети = родитель» и «Итого» у них нет (прикладной сценарий НУ — аналитика AURORA-1669); тождество строки (начало +
обороты = конец) проверяется у всех строк, как и раньше. Итоговые строки «Итого» с меткой показателя («НУ», «Кол.»)
сразу за основной «Итого» — тоже TOTAL, в подвал не уходят и в сверку «Итого = корень» не входят. Колонка
«Показатели» остаётся в extra строки под своим заголовком.
Классификация строки (п. 6–10 «Парсинг ОСВ»):
- Строка — счёт, если текст матчится
^\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. - Не-счёт: позиция в ветке = количество не-счётных предков + 1 → соответствующая группировка из
group_schema(после последнего ACCOUNT). Увеличение уровня — ребёнок, равный — сосед, уменьшение — конец группы. - После COUNTERPARTY дочерние продолжают классифицироваться по схеме (CONTRACT, затем OTHER).
- Строка «Итого» (level 0, последняя) — контрольная сумма всего документа; не сохранять как
osv_entry(OQ-01 закрыт). Парсер отдаёт её вrowsкакgroup_type = TOTAL(level 0, без родителя); ядро (Срез 2c) при маппинге строку TOTAL отбрасывает (osv_entryеё не содержит,TOTAL— не значение хранимогоgroup_type); вmetrics.rowsона не считается. - Все остальные строки сохраняются (п. 11 «Парсинг ОСВ») с
parent_entry_idпоparent_row_number(OQ-01 закрыт): дерево строится по номеру родителя, строка с несуществующим родителем сохраняется сparent_entry_id = NULLи даёт предупреждение маппинга (документNEEDS_CHECK). Суммы иdata_type(включаяNU) пишутся как в результате.
Плоский список под счётом шапки (реализовано, tabular-parser #59, AURORA-1661). Если в шапке есть код счёта, а в теле
нет ни одной строки-счёта (п. 1), каскад иерархии (§1.0 п. 5) после outline, indent, account_pattern и до sums
пробует стратегию header_account: все строки таблицы — уровень 0, предупреждение OSV_HIERARCHY_BY_HEADER_ACCOUNT
(«плоский список под счётом шапки»). Условие — определение плоской формы 1С; при наличии outline или отступов побеждают
они (счёт шапки при этом всё равно наследуют строки). Счёт шапки — счёт корня ветки: строки до первой строки-счёта
(и все строки плоского списка) получают account_code_raw шапки, а не null.
Контрагенты расчётных счетов без названия вида (решение владельца 16.10.2026; риск OQ-158). Если схема группировок —
одна ACCOUNT (вид субконто в шапке не назван), строка на позиции 1 (первая не-счётная) под счётом из списка
counterparty_accounts (osv_groups.yaml, данные) — COUNTERPARTY без имени группировки, ИНН — по общим правилам (колонка,
строка ИНН, хвост названия). Список: 58, 60, 62, 66, 67, 75, 76, 80 (без 71 и 73 — подотчётные и персонал, R-9); запись
76 покрывает 76, 76.2, 76.Н.1, но не 760. Глубже позиции 1 — OTHER; именованные группировки схемы («Учредители»,
«Работники организаций») правило не переопределяет. Предупреждение OSV_ROWS (по одному на различный код счёта):
«вид субконто в шапке не назван: строки аналитики счёта <код> приняты за контрагентов по плану счетов — проверьте».
Риск: физлица и подотчётные на расчётных счетах без названия вида попадут в «Осталось N» сопоставления (OQ-158).
Блоки субсчетов с кодами контрагентов (AURORA-1714, решение владельца 27.10.2026; tabular-parser #71). Выгрузка 1С «ОСВ по счету 60 по контрагентам» (плоский лист без outline и отступов, счёт — в шапке, см. §1.1) строит таблицу блоками:
60.111 ← субсчёт, ячейки сумм пусты
П005482 | 0 | 108000 | 108000 | 0 | 0 | 0 ← код контрагента, суммы есть
ВЕСТ БИР ООО ← название контрагента, сумм нет
ДП87825;З-139/21-1 | 0 | 108000 | … ← договор контрагента, суммы есть
ИТОГ по 60.111 | … суммы блока … ← итог субсчёта
…
ИТОГ | … суммы листа … ← итог листа
- Строка-счёт с тремя цифрами в сегменте. Код вида
60.111,60.243— строка-счёт (ACCOUNT, п. 1 выше), только если он начинается со счёта шапки (60.xxxпри шапке60; сегмент — 1–3 цифры). Без счёта шапки, а также для кода, не начинающегося со счёта шапки, шаблон п. 1 прежний (сегмент — 1–2 цифры или заглавные буквы). Голый числовой код без точки, не совпадающий со счётом шапки («441», «199»), при известном счёте шапки счётом не считается: это текст строки аналитики (код контрагента, номер договора). - «ИТОГ по <субсчёт>» (
subaccount_sums, данные) — суммы субсчёта: строка-счёт с таким же кодом, у которой нет ни одной суммы, получает шесть сумм из своей строки «ИТОГ по …»; сама строка «ИТОГ по …» отдельной строкой вrowsне выдаётся (контракт не меняется). Субсчёт сверяется «сумма детей = родитель» как обычный родитель (§1.4). Наличие хотя бы одной такой пары включает п. 3–4 для листа. - Код с суммами → название без сумм → договоры. Внутри блока субсчёта (между строкой-счётом и следующей строкой-счётом
или итогом):
- строка с суммами, за которой следует строка без единой суммы — контрагент:
COUNTERPARTY(вид субконто — «Контрагенты» или его синоним из шапки/таблицы, §1.1),row_value— код как в файле; - строка без сумм, идущая сразу за такой строкой, — название этого контрагента: склеивается с ним (как строка ИНН
с наименованием, OQ-163): в
rowsне попадает, текст — вcounterparty_name;counterparty_inn— по общим правилам (ИНН в файле нет — пустой ИНН штатен); - остальные строки с суммами до следующего контрагента, субсчёта или итога — договоры этого контрагента:
CONTRACT(§1.3, п. 3: после COUNTERPARTY дочерние продолжают классифицироваться по схеме; вид договоров здесь не назван в шапке, поэтому тип задаёт сама раскладка), на уровень глубже контрагента. Суммы контрагента = Σ его договоров. Иерархия листа: субсчёт → контрагент → договор (стратегия каскадаcode_blocks,OSV_HIERARCHY_BY_CODE_BLOCKS).
- строка с суммами, за которой следует строка без единой суммы — контрагент:
- «ИТОГ» — итог листа. Строка с текстом ровно «ИТОГ» (
total_row_exact, наравне с «Итого») — TOTAL; таблица кончается на ней, а не на первой «ИТОГ по …» (та не начинается с «Итого» и итогом листа не является). Сверка «Итого = Σ строк уровня 0» (§1.4) идёт по субсчетам.
Иерархия по отступам: подуровень счёта, строка закрытия, вложенная аналитика (AURORA-1858, tabular-parser #78).
ОСВ 1С без outline («Субконто», вид аналитики в шапке не назван) задаёт уровни только отступом: 0 у счетов, 1 у
аналитики. Отступов не хватает в трёх случаях, стратегия indent их уточняет:
- Подуровень счёта на том же отступе.
68.01.1стоит на одном отступе с68.01. Счёт, код которого продолжает код более раннего счёта того же отступа в том же блоке (68.01.1→68.01), становится его ребёнком, вместе со своей аналитикой. Только если все такие пары листа на одном отступе, раскладка однозначна и сумма детей перекладываемого родителя после перекладки сходится; иначе лист остаётся по отступам (защищает плоские списки60,60.01,60.02). - Безымянная строка закрытия. Строка без текста и кода сразу за строкой счёта, с оборотами, равными оборотам счёта (1С печатает её под 90.09 и 91.09), — ребёнок счёта. Пустую ячейку отступа читатель не хранит, поэтому без правила строка попадала на уровень 0 и удваивала сумму корней.
- Вложенность аналитики по суммам (решение владельца 28.10.2026 после ревью). Если аналитика родителя на одном
отступе, а вложенность видна только по суммам (91.01: «Прочие внереализационные» = две строки ниже), то только у
родителя с нарушением «сумма детей = родитель» строка, равная сумме следующих соседей, жадно становится их
родителем (как стратегия
sums). Перестройка принимается, только если после неё родитель сходится, иначе — откат. Защиты — правило применяется, только если одновременно:- в схеме шапки после счёта нет названных видов аналитики («Субконто» без вида), то есть вложение не меняет
group_typeи имя группы строки (иначе контрагент мог бы стать «договором» соседа); - все дети родителя стоят на одном отступе (если отступ различает уровни, ему верим);
- ни вкладываемая строка, ни её новый родитель не несут ИНН.
Вложение меняет дерево в результате (
level,parent_row_number), а не только проверку. Родитель без нарушения не перестраивается; равные суммы соседей (50 + 50 = 100) сами по себе вложенности не дают. Остаточный риск: - RR-6 — на листе «Субконто» без вида дубль строки аналитики с той же суммой, что у соседа выше, неотличим от
«группа → единственная статья» (на 91.01: «Проценты к получению» = «по векселям») и принимается за вложенность:
ошибка «лишняя строка» в такой форме не ловится.
Правила 1 и 3 опираются на сходимость, но это не «подгонка» (§1.0 п. 7): дерево меняется только в узких случаях, где
структура подтверждается и кодом/формой выгрузки, и суммами; реальное расхождение в остальных случаях даёт
needs_check.
- в схеме шапки после счёта нет названных видов аналитики («Субконто» без вида), то есть вложение не меняет
Нормализация кода счёта (ядро, 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): у 90, 91 и 99 строки аналитики несут только обороты, сальдо закрывается на субсчёт закрытия (91.09), поэтому тождество строк на них не проверяется; список счетов — данные вrules/*.yamltabular-parser (состав 90, 91, 99; счёт 90 добавлен решением владельца 19.10.2026: 4 файла контура с колонкой «Показатели» оставалисьneeds_checkна счёте 90 (667d1ee1, 7983e91b, 3e74526e, ec075a82) — у родителей 90.xx.y сальдо на конец есть, у аналитики его нет; tabular-parser#58, O-1). Проверки «сумма детей = родитель» и «Итого» на этих счетах сохраняются в таком виде (R-3 PR arm-tabular-parser#26, подтверждено владельцем 08.10.2026): обороты родителя сверяются с суммой детей всегда; сальдо — только у родителя с дочерними субсчетами (91 → 91.01/91.02/91.09); у субсчёта, под которым одна аналитика, сальдо с детьми не сверяется (у аналитики оно пусто, у субсчёта — равно оборотам). Счёт строки сопоставляется со списком по ближайшему счёту ветки: запись91покрывает91,91.01,91.09, но не910; строка «Итого» проверяется как обычно. -
По каждому родителю: суммы родителя = Σ детей того же
data_type. Строки-показатели (§1.3: «Вал.», «Кол.», код валюты, «В валюте», метки колонки «Показатели» кроме «БУ») в суммирование не входят и сами родителями не проверяются. Обороты сверяются по колонкам (дебет с дебетом, кредит с кредитом). Сальдо (начало, конец) — по колонкам либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по неттодебет − кредит(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).
Развёрнутый родитель над свёрнутым субсчётом (AURORA-1863, решение владельца 28.10.2026, tabular-parser #78). 1С считает развёрнутое сальдо счёта по нижнему уровню аналитики, а субсчёт может показать свёрнутым: счёт 76 — 47,1 млн Дт / 12,9 млн Кт, его субсчёт 76.07 — 8,2 млн Кт при аналитике 4,6 млн Дт / 12,8 млн Кт. По колонкам счёт с субсчетами не сходится, по нетто сходится до копейки. Сальдо развёрнутого родителя сверяется по нетто, только если среди его прямых детей есть «нетто-ребёнок» — ребёнок, который сам сошёлся со своими детьми лишь по нетто (по колонкам — нет). Признак идёт вверх только по непрерывной цепочке таких строк: строка, сошедшаяся по колонкам, его не передаёт, так что слепая зона не растёт до корня. Обороты — по колонкам всегда. Развёрнутый родитель без нетто-ребёнка сверяется по колонкам, как раньше: «нетто всегда» (широкий вариант) отклонён — он прячет любую ошибку, сохраняющую нетто. Так в банке первичных документов остаются
needs_check14 файлов одной организации, где счёт 68 (и 71) расходится с субсчетами на одинаковую сумму по Дт и Кт (0,05; 137,00; 5 939,76) без свёрнутого ребёнка: отличить схлопнутую пару от потерянной по данным нельзя. Остаточный риск:- RR-5 — у развёрнутого родителя с нетто-ребёнком колонки сальдо не информативны: ошибка, сохраняющая нетто родителя (одинаковый сдвиг Дт и Кт, перестановка сторон), не ловится, как у свёрнутого родителя (RR-1).
Строки разреза «Период» (AURORA-1823, tabular-parser #78). В выгрузке 1С с группировкой «Период» в шапке под контрагентом идут строки «Обороты за <месяц> <год>» (или «… кв. <год>»), у каждой своё сальдо на начало и конец периода; сальдо конца периода — сальдо начала следующего. Группировка распознаётся по имени в схеме шапки (словарь
period_groupsвosv_groups.yaml; тип группы остаётсяOTHER, контракт не меняется). Если все дети родителя — строки-периоды, родитель сверяется цепочкой: обороты = Σ оборотов периодов (по колонкам), сальдо начала = сальдо начала первого периода, сальдо конца = сальдо конца последнего (в порядке строк; по колонкам либо по нетто, как выше), и цепочка непрерывна: сальдо конца каждого периода = сальдо начала следующего (пропущенные месяцы разрыва не дают — 1С не печатает месяц без движения, сальдо переходит дальше). Сумма сальдо периодов не считается. У родителя со смешанными детьми (периоды и обычные строки) суммы считаются как прежде. -
Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0); если корней уровня 0 несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03, OQ-133, PR #13) — «Итого» == сумме строк уровня 0 (показатели без суммы и сама «Итого» не входят; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя выше, R-21); при одном корне сверка прежняя по колонкам. Единственный корень и свёрнутая «Итого» (AURORA-1708, tabular-parser #73, arm-ka-docs #170): у счёта с развёрнутым сальдо 1С показывает корень развёрнутым (Дт и Кт), первую «Итого» — свёрнутой, а «Итого развернутое» равной корню до копейки. «Итого» сверяется с корнем как родитель с ребёнком (R-21): обороты по колонкам, сальдо начала и конца по нетто
дебет − кредит, если сальдо «Итого» свёрнуто; если «Итого развернутое» совпадает с корнем по колонкам, основная «Итого» сверяется с корнем по оборотам (колонки) и нетто сальдо. Реальное расхождение нетто или оборотов остаётся нарушением; развёрнутая (обе стороны) «Итого», не равная корню по колонкам, нетто-сверкой не спасается. Плоский список под счётом шапки (§1.3,header_account) — тоже случай «корня ACCOUNT нет»: «Итого» == сумме строк уровня 0 (прежняя сверка пустой/нулевой «Итого» — только когда строк уровня 0 нет). Вторая итоговая строка («Итого развернутое») — тожеTOTAL, в любом порядке относительно «Итого»: и сразу за ней, и перед ней (плоская форма 1С, tabular-parser #59); сама в сверку «Итого = корень» не входит (только подтверждает нетто-сверку при единственном корне, AURORA-1708 выше); тождество строки на ней не проверяется, только если обороты пусты и на листе есть другая «Итого» (перед «Итого» в 1С — без оборотов, тождество не проверяется; после «Итого» — с оборотами, проверяется как обычная строка; tabular-parser #59, R-16, R-19) — сверяется основная «Итого» (если на листе только «Итого развернутое», она единственная итоговая и сверяется как «Итого»); строка сумм без подписи в конце таблицы принимается за «Итого» с предупреждением. Таблица кончается на первой «Итого» (на корневом уровне оформления; плюс «Итого развернутое» сразу за ней): всё ниже (подпись «Ответственный:», блоки, дописанные аналитиком, в том числе строки, начинающиеся с «Итого») — вне документа, в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).
Диапазон дат с точками и год из двух цифр (AURORA-1714, решение владельца 27.10.2026). «За период | 01.01.25..30.06.25»
(метка в одной ячейке, диапазон в соседней, §1.1) разбирается как «01.01–30.06» по таблице выше: Q2, нарастающий;
разделитель .. допустим наравне с пробелом и «по». Год из двух цифр в дате (.25) — 20yy; берётся год даты конца
диапазона. Год из четырёх цифр, если есть в тексте, имеет приоритет.
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.
Имена частей xlsx сравниваются без учёта регистра, как в Excel: часть, которая отличается от ссылки [Content_Types].xml
или *.rels только регистром (xl/SharedStrings.xml при ссылке на sharedStrings.xml — так пишут некоторые выгрузки),
читается под именем из ссылки; без этого книга не читается (UNREADABLE), а лист с таким именем пропадает молча. Две
части под одну ссылку не угадываются. Реализовано, tabular-parser #66, AURORA-1707.
С контракта 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 — квартал версии:
- есть привязанный документ с
is_cumulative = trueиperiod_quarter = n— он один, строки берутся как есть; - иначе нужны привязанные квартальные документы за каждый квартал
Q1..Qn; любого нет — пробел (Gap(missing_quarters)), строк пары в своде нет, пробел показывается в сводке (gaps[]) и в листе «Пробелы» экспорта; - нарастающий и квартальные одновременно — берётся нарастающий (он полнее по определению);
- 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!).
- Для каждой компании периметра:
documentтипа OSV, входящие вconsolidation_documentверсии (явная привязка, а не «последние подтверждённые»). Документ подходит версии, если ЮЛ — член периметра,period_year= год версии, и: нарастающий (ББ/ОФР — всегда) —period_quarter= квартал версии; квартальный —period_quarter≤ квартал версии; ОСВ заQ1подходит версии любого квартала года (§2 п. 4, OQ-124). Привязываются толькоCONFIRMEDдокументы незаменённых файлов. Точки привязки: подтверждение документа (ко всем версиям «в работе»), создание версии и добавление ЮЛ в периметр (подхват: на ключ «тип, счёт, квартал, признак нарастающего» — документ с самым позднимconfirmed_at); при привязке документ с тем же ключом у того же ЮЛ заменяется. Точки отвязки: автозамена файла (§9.5 п. 3), удаление ЮЛ из периметра, уточнение периода или счёта подтверждённой ОСВ (§9.5 п. 12, v5.20). ВерсииCANCELLEDпривязок не меняют. Выбор источников и склейка кварталов — §2. - Строки
osv_entryэтих документов сgroup_type = COUNTERPARTY;data_type IN (BU, SUM). - Строки не агрегируются: сто строк по 60.01 у компании — сто строк в своде.
counterparty_name/innпоказываются как в файле. - Резолв контрагента (при маппинге результата — Срез 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) не ищется. - Признак контрагента: ГСК / ГК / Внешний (см.
04-data-model-guide.md). - Количество строк свода = количеству отобранных
osv_entryдля нарастающей ОСВ; для квартальных документов строки склеиваются по ключу (§2),entry_idsстроки свода перечисляет источники. - Дорезолв на чтение. Строка без
counterparty_idрезолвится на лету тем жеCounterpartyIndex; результат не сохраняется (производное, DECISION-5): в ответеmatch_stored = false,match_kind— шаг резолва. Сохранённое значениеosv_entry.counterparty_idприоритетнее; при расхождении между кварталами берётся значение самого позднего квартала (расхождение — вwarningsстроки). - Способ сопоставления
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 символов (иначе 422QUERY_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, а у «Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу.
- Пары участников без встречных записей (решение владельца 27.10.2026, AURORA-1746, arm-core #195). Для участников
периметра А ≠ Б (
consolidation_memberверсии) пара (А, Б) входит в список, если в своде есть хотя бы одна строка ЮЛ А сcounterparty_id = Б(сохранённым или дорезолвленным на чтение, п. 7) и нет ни одной строки ЮЛ Б сcounterparty_id = А. А —holder(записи есть), Б —missing(встречных записей нет). Пара, где записей нет ни у А, ни у Б, не входит; строки с контрагентом — самим собой не учитываются; счета, суммы иdata_typeна признак не влияют. Это чистая функция от строк свода и периметра, не хранится (DECISION-5). Порядок — по имени Б, затем по имени А. Поля пары:holder_id/name,missing_id/name,holder_rows(строки А с контрагентом Б),missing_unmapped_rows(строки Б сunmapped = true),missing_rows_total(все строки Б в своде; 0 — ОСВ не привязаны или по ним пробелы). Назначение — показать аналитику перед шахматкой места, где в строках без ИНН может прятаться участник периметра; переход к шахматке при непустом списке не блокируется, несопоставленные строки по-прежнему считаются «Внешний» (п. 5; AURORA-1388, AURORA-1865). Закрывается ручным сопоставлением (см. выше). Строки Б, у которых ИНН в файле есть, но его нет в справочнике, сопоставления не требуют и вmissing_unmapped_rowsне входят — отдельная задача AURORA-1866.
§4. Правила элиминации — пары счетов (Прил. 3 ТЗ, DOC-223044, FNR03)
Хранятся как данные: elimination_rule (+ elimination_rule_account), версионируются; версия фиксируется в
consolidation_version.elimination_rules_version. Для версии консолидации берётся версия правил, действующая на дату
составления консолидации (создания версии), а не на её отчётный период; фиксируется при создании версии и дальше не
меняется (OQ-174, решение владельца 21.10.2026). С DBML v5.23 поле обязательно (решение владельца 23.10.2026, AURORA-1536):
версия консолидации без действующих правил не создаётся, у версий, созданных раньше, номер проставлен миграцией по дате
создания. 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; OQ-07, OQ-173 — решения владельца 19.10, 21.10 и 23.10.2026)
Границы: только closing_debit / closing_credit; только компании периметра версии; правила и счета сторон — из §4
(версия правил — consolidation_version.elimination_rules_version), только правила ББ (statement_type = balance,
measure = closing; правило с другим показателем — ошибка INVALID_RULE); строки group_type = COUNTERPARTY,
data_type ∈ (BU, SUM). Единица расчёта — тройка (правило r, компания A, компания B). Правила считаются для каждой
упорядоченной пары компаний — и (A, B), и (B, A); поэтому зеркальные правила № 9–16 Прил. 3 в seed не нужны (№ 14 для
(A, B) — это № 6 для (B, A)).
Два уровня: сумма элиминации по тройке — агрегат, идёт в КФО; упаковка — какие строки ОСВ с какими сопоставлены (детализация, строки без пары). Способ упаковки сумму элиминации не меняет (решение владельца 19.10.2026).
1. Сумма элиминации по тройке — каждое правило отдельно. Каскад по остатку в порядке rule_no отменён (решение
владельца 21.10.2026 по созвону с заказчиком 19.10.2026, OQ-173): счета правил пересекаются (дебет № 5–8 общий; кредит
67.03 — № 3 и маска 67 у № 7, 66.03 — № 4 и маска 66 у № 8), и строка такого счёта пока входит в Σ каждого подходящего
правила. Двойной учёт снимает разнесение строк по правилам и ручной перенос между правилами (AURORA-1751); до него
сумма элиминации по паре компаний может превышать сумму стороны — это известное ограничение, не дефект.
ΣA = Σ строк A с counterparty_id = B по счетам стороны 1 правила r (с исключениями is_excluded), сумма стороны side_1
ΣB = Σ строк B с counterparty_id = A по счетам стороны 2 правила r, противоположная сторона
deviation_amount = |ΣA − ΣB|; deviation_percent = deviation_amount / min(ΣA, ΣB) * 100
mode = min: расхождение ≤ 5 % → элиминация_r = min(ΣA, ΣB); > 5 % → подсветка, элиминация_r = 0, ждёт корректировки
mode = manual: элиминация_r = 0 — только корректировка / гудвилл (§8)
Знак (решение владельца 23.10.2026): суммы сторон — со знаком, как в шахматке §6 (одно правило и одна пара дают
одинаковые ΣA и ΣB на обоих экранах). Отрицательное сальдо строки (§1: возможно) — встречная позиция, например аванс или
переплата: у A по B строки 150 и −50 дают ΣA = 100, а не 200. Если min(ΣA, ΣB) ≤ 0 (одна сторона пуста или ушла в
минус), процент не считается (null), элиминация 0, тройка подсвечивается при любом расхождении — разбирает аналитик
(в шахматке тот же случай — OQ-175).
2. Упаковка строк в тройке — группами 1:1, 1:N, N:1 (правило охватывает несколько счетов, поэтому одной строке на одной стороне могут соответствовать несколько строк на другой). N:M нет. Выбирается минимальная упаковка — группа с меньшим числом строк: если сумму закрывают и «1 Дт ↔ 2 Кт», и «1 Дт ↔ 3 Кт», берётся 1:2. Каждое правило упаковывается независимо (каскада нет): строка может войти в пары разных правил, пока её не разнесли (AURORA-1751).
вход правила r: строки a стороны 1 (A → B) и b стороны 2 (B → A) по счетам правила r с суммой > 0
строки с суммой < 0 в упаковку не идут → «без пары», причина NEGATIVE_AMOUNT (разбирает аналитик)
строки с суммой 0 → «без пары», причина ZERO_AMOUNT
для k = 2 … 6 (число строк группы: 1:1, затем 1:2 и 2:1, … до 1:5 и 5:1; N ≤ 5):
кандидаты размера k из ещё не сопоставленных строк, «закрывающие» сумму:
точные (Σ одной стороны == Σ другой), затем с расхождением ≤ 5 % к меньшей — по возрастанию ПРОЦЕНТА расхождения
(решение владельца 23.10.2026)
группа без соперников → пара, её строки выбывают
две и более группы с тем же k и тем же процентом расхождения, делящие строку → не сопоставляем, на ручное (AMBIGUOUS)
строки, не вошедшие ни в одну группу → «без пары» (NO_MATCH)
результат группы: rule, A, B, osv_entry.id обеих сторон, Σ A, Σ B, deviation_amount, deviation_percent
Результат не хранится (DECISION-5) — возвращается как структура для шахматки, корректировок и КФО. Ручное решение по
паре — correction с естественным ключом (rule_id, legal_entity_id, counterparty_id).
3. Выдача. Два запроса (контракт — dev/frontend-api-guide.md): список пар версии — все тройки «правило ББ ×
упорядоченная пара компаний периметра», где хотя бы одна сторона не ноль, с ΣA, ΣB, расхождением, процентом, суммой
элиминации, статусом (сопоставлено автоматически / с расхождением > 5 % / без встречной стороны / ручное правило) и
osv_entry.id строк обеих сторон; сводная ОСВ строится один раз на запрос. Детализация тройки — упаковка п. 2:
точные группы, группы с расхождением ≤ 5 %, строки без пары с причиной.
Алгоритм — по ТЗ (решение владельца 08.10.2026, OQ-155): пары, min, порог 5 %, а не полная элиминация каждой
стороны, как в эталонном Excel. Пример на фикстурах (Q3 2024, правило № 6): у Счастливочной на «Русский бисквит» 62.01
«БП Производство» 106 311 116,46, 62.01 «БП Аренда» 1 401 330, 76.06 350 000, 76.АВ 2 366 666,69; у РБ — одна строка
60.01 106 311 116,46. Упаковка: точная пара 1:1 (106 311 116,46), остальные три строки Счастливочной — без пары.
Сумма элиминации: ΣA = 110 429 113,15, ΣB = 106 311 116,46, расхождение 3,87 % ≤ 5 % → min = 106 311 116,46. Строки 76.06
и 76.АВ входят и в правило № 5, если у РБ есть 66.04/67.04 на Счастливочную, — двойной учёт до AURORA-1751.
§6. Шахматка и результирующая таблица (DOC-221277)
Вход: версия + набор дебетовых счетов (kind ∈ A, AP) и кредитовых (kind ∈ P, AP); предустановки: «58.03 vs
67.03 + 66.03», «60 + 62 + 76 vs 60 + 62 + 76», «все счета» — только балансовые счета, без забалансовых 001–012
(is_off_balance = true; решение владельца 16.10.2026, AURORA-1651: 009 «Обеспечения выданные» давал ложное
расхождение); при ручном выборе забалансовые счета допускаются. Дополнительно пресетом может быть правило ББ из §4
(statement_type = balance): дебетовые и кредитовые счета берутся из сторон правила (с исключениями is_excluded),
показатель — measure правила (у всех правил ББ seed v1 — closing); каскада §5 нет — это просмотр сумм по счетам правила, не сумма элиминации (решение владельца
21.10.2026, arm-core #141). Правила ОФР пресетом не являются (ОФР в §4 открыт).
Шахматка — суммы первички: корректировки (§8) в ячейках не учитываются, список «строк без пары» шахматка не даёт —
строки ОСВ ячейки — AURORA-1653 (osv_entry_ids_* ячейки), строки без пары — упаковка §5 п. 2 (AURORA-1536).
Строки/столбцы — компании периметра; диагональ пуста.
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 (null при min = 0)
one_sided = ровно одна из amount_a, amount_b равна 0 (знак ненулевой не важен)
подсветка = variance_pct > 5 % ИЛИ one_sided
ячейка: amount_a / amount_b / (variance)
Пара без встречной стороны (one_sided) — по суммам, а не по наличию строк ОСВ: строка с нулевым сальдо (одни обороты)
стороной не считается; «0 против 0» не подсвечивается (AURORA-1651). Подсветка одинакова в API, в xlsx и на экране.
Процент при отрицательной меньшей стороне — OQ-175; счёт 000 в «все счета» — OQ-176.
Фильтры строк — как в §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 (Σ сумм элиминации троек, §5 п. 1; пересчёт в тыс. руб.) → 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;
автоповтор публикации без ack (OUTBOX_MAX_REPUBLISH) — то же задание, attempt не меняется, счётчик —
metrics.republish_count, после исчерпания — dlq (NO_ACK); ручной повтор по кнопке пользователя — новое задание
с attempt + 1, прежнее остаётся в истории (§9.5 п. 13). Note DBML у recognition_job («replay = сброс в queued с
attempt+1») устарела — исправить в следующей версии DBML. max_attempts: OCR 1, парсер 3; reply_deadline OCR 20 мин (решение владельца 09.10.2026; note DBML «5 мин» — OQ-159), парсер 5 мин.
9.5 Первичные документы: загрузка, маппинг, замена, удаление, подтверждение (Срез 2c)
- Загрузка (
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. Принятый файл: объект S3uploads/{file_id}/{job_id}/{имя},stored_file(UPLOADED, version_no = 1),recognition_job(queued),audit_event(FILE_UPLOAD). Тип документа и период при загрузке не запрашиваются: их определяет парсер. - Маппинг (фоновый цикл ядра, отдельный от 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): после листов ОСВ, в той же транзакции, каждыйstatementDocumentBALANCE/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 (changeunified-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_issueMAPPING_WARNINGinfo(source = core; статус при приёме — по D-6, после правки они статус не держат); строкиmetrics.warnings1.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», «Очистка форм перед выкладкой»). - Автозамена. После успешного маппинга другие файлы того же ЮЛ (не
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). Чужая организация в шапке (AURORA-1644, решение владельца 21.10.2026, уточнено 27.10.2026). Пара «новый документ — прежний» сcounterparty_check = MISMATCH(§9.3) у любого из двух в замене не участвует: скан другой организации, загруженный в карточку ЮЛ, не вытесняет ни документ организации карточки, ни другой чужой скан, и сам ими не вытесняется.UNKNOWN(пустая шапка) несовпадением не считается — замена как прежде. Файл с документом того же ключа, оставленный только из-заMISMATCH, — предупреждение «автозамена не выполнена» вmetrics.mapping_warningsнового файла (имя и версия оставленного файла, его документы этого ключа). Цена: исправленный скан с искажённым OCR ИНН шапки ляжет рядом с прежним, прежний удаляется вручную; файл, заменённый по другому документу, уходит вSUPERSEDEDцеликом (OQ-115), даже если в нём есть документMISMATCH. Организация документа по ИНН шапки вместо карточки загрузки и единая область видимости панели, чек-листа и автозамены — AURORA-1752. Аудит: системное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). - Подтверждение (
POST /documents/{id}/confirm,DOCUMENT_EDIT):RECOGNIZED/NEEDS_CHECK→CONFIRMED(confirmed_by/at,audit_event(DOCUMENT_CONFIRM)); повтор — 409ALREADY_CONFIRMED; документ заменённого файла — 409FILE_SUPERSEDED; прочие статусы — 409DOCUMENT_NOT_CONFIRMABLE. Автоподтверждённый документ (confirmed_byNULL) повторно подтвердить нельзя — 409ALREADY_CONFIRMED. Ручной правки строк ОСВ нет (решение владельца 07.10.2026, OQ-114; возможен пересмотр по качеству парсинга и документов): ошибка в ОСВ исправляется перезагрузкой файла (п. 3), поляis_modified/original_valuesуosv_entry— резерв; корректировки — §8. Форма ББ/ОФР (Срез 3b) дополнительно: период не определён (period_yearилиperiod_quarterпуст) — 409PERIOD_REQUIRED(поправьте дату отчёта, п. 9); непройденные неакцептованные жёсткие проверки §7 — 409CHECKS_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). - Удаление (
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: для автоподтверждённых документов — см. выше). - Аудит.
FILE_UPLOAD,FILE_REPLACE,FILE_DELETE,FILE_REPROCESS(п. 13, v5.21),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). - Порядок блокировок (против взаимоблокировок): задание → файл → документы (
FOR NO KEY UPDATE) → advisory-lock ГК → версии консолидации (FOR UPDATE, по возрастаниюid); автозамена дополнительно берёт advisory-lock по ЮЛ. Подробности —03-architecture.md. - Загрузка в консолидацию (
POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files,DOCUMENT_EDIT; v5.18, решения владельца 09.10.2026, RSHB/arm-ka-docs#90): до приёма — консолидация видима (404NOT_FOUND), ЮЛ видимо (404), есть версия консолидации «в работе» (409CONSOLIDATION_NOT_EDITABLE), ЮЛ — член периметра хотя бы одной такой версии (422ENTITY_NOT_IN_PERIMETER). Принятый файл получаетstored_file.consolidation_id;FILE_UPLOAD.after_value.consolidation_id. После приёма хотя бы одного файла, отдельной транзакцией, каждая версия консолидации «в работе» с этим ЮЛ в периметре и статусом INITIATED переходит в DOCUMENTS_PROCESSING (§9.1; версии блокируютсяFOR UPDATEпо возрастаниюid, статус перечитывается); сбой перехода не отменяет принятые файлы. Блокировка версии для загрузки не нужна. Queryconsolidation_version_idуPOST /legal-entities/{id}/files(Срез 4) удалён. - Правка формы ББ/ОФР (
PATCH /documents/{id}/statement,DOCUMENT_EDIT, Срез 3b; решения владельца 07.10.2026). Правятся суммы ячеек (values[] {line_id, column_id, amount}) и три поля шапки:inn(10 или 12 цифр, иначе 422INVALID_INN, в том числеnull— очистить ИНН нельзя; пересчитываетсяcounterparty_check),report_date(пересчитываютсяperiod_year,period_quarter,reporting_dateпо правилу периода п. 2) иunits(непустая строка до 50 символов). Код и название строки, добавление и удаление строк и колонок не правятся (до v2.0; с v2.0 код и статус строки, дата колонки и наименование организации правятся — см. ниже, название строки и удаление строк и колонок — по-прежнему нет). Только до подтверждения: документCONFIRMED— 409DOCUMENT_READONLY(исправление подтверждённой формы — новым файлом через автозамену, п. 3); файлSUPERSEDED— 409FILE_SUPERSEDED; не форма — 409DOCUMENT_NOT_STATEMENT; ячейка не этого документа — 422UNKNOWN_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; changeunified-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"без кода — 422LINE_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 цифр (422INVALID_LINE_CODE), может совпадать с существующим (Р-4); незнакомый словарю —info. Строка, созданная правкой —row_index IS NULL(шаблонная): если строка с назначаемым кодом — такая и у неё есть суммы, — 409LINE_CODE_TAKEN; без сумм — она удаляется (еёidубирается изdocument_issue.line_ids), назначенная строка источника занимает её место. Строки источника (row_indexнеnull) с тем же кодом не удаляются и не мешают — повтор кода законен. Элемент без действия или с несколькими — 422INVALID_LINE_PATCH; чужая строка — 422UNKNOWN_LINE, чужая колонка — 422UNKNOWN_COLUMN; правка суммы поline_codeпри наличии строкиokс этим кодом — 422UNKNOWN_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задания и копии ядра —infoSTATEMENTS_VERSION_MISMATCH(у заданий 1.x, где версии нет, — не пишется). Один PATCH — один пересчёт и одна записьRECOGNIZED_DATA_EDIT(before/afterпо кодам, статусам, родителям, подтверждениям). - Исходный файл и комментарии (Срез 3b).
GET /files/{id}/content(DOCUMENT_VIEW, policy по ЮЛ) отдаёт исходный объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует видимость); работает и дляSUPERSEDED; объекта нет в хранилище — 404FILE_CONTENT_NOT_FOUND. Комментарии к документу —POST/GET /documents/{id}/comments(таблицаcomment,object_type = 'document'); добавлять можно к документу в любом статусе; правки и удаления нет; при удалении файла (DELETE /files/{id}, п. 5) комментарии его документов удаляются вместе с ними (связь полиморфная, FK нет — удаляет сервис). - Рабочий стол консоли (
GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files,DOCUMENT_VIEW; 404 на невидимые консолидацию или ЮЛ;limit,offset,include_superseded): файлы ЮЛ сconsolidation_idэтой консолидации плюс файлы ЮЛ, у которых есть документ, привязанный (consolidation_document) к любой версии этой консолидации (подхваченные ОСВ заQ1и файлы, загруженные без контекста); новые первыми (uploaded_atdesc,id);SUPERSEDED— только приinclude_superseded=true(скрыт по умолчанию). Карточка файла та же, что вGET /legal-entities/{id}/files(FileOut+consolidation_id). Рабочие версии одного периода видят один стол; верифицированная версия заморожена привязками иex_snapshot(Срез 7), отдельного «замороженного списка» в панели нет. Панель [39] из карточки версии показывает стол консоли; переключатель «Все документы организации» —GET /legal-entities/{id}/filesс подписью консолидации у файла. Чек-лист версии — по столу консоли (AURORA-1646, решение владельца 21.10.2026). Статусы ячеек «Требуется проверка» / «Распознан» (неподтверждённые документы) и «Обработка» / «Ошибка загрузки» (состояние файлов ЮЛ) чек-лист берёт только из файлов стола этой консолидации; «Загружен» — по привязанным документам, как прежде. Документы ЮЛ из других консолидаций и из карточки ЮЛ, которых нет на панели, ячейку не меняют: без документов — «Отсутствует». Цена: неподтверждённый документ, загруженный из карточки ЮЛ, в чек-листе не виден, пока его не подтвердят (тогда он привязывается и попадает на стол). Единая область видимости панели, чек-листа и привязки целиком — AURORA-1752 (после аналитики AURORA-1378). - Уточнение периода и счёта ОСВ (
PATCH /documents/{id}/osv-header,DOCUMENT_EDIT, policy по ЮЛ; v5.20, решения владельца 21.10.2026, OQ-161 вариант 2, AURORA-1442). Аналитик исправляет период или счёт документа ОСВ, распознанные неверно. Тело — любые из полейperiod_year,period_quarter,is_cumulative,osv_account_code; отсутствующее поле не меняется. Проверки, по порядку: документ не ОСВ — 409DOCUMENT_NOT_OSV; файлSUPERSEDED— 409FILE_SUPERSEDED;period_year— четыре цифры 19xx/20xx,period_quarter—Q1…Q4,is_cumulative—true/false; если в теле есть хоть одно поле периода, после правки все три не пусты и уQ1is_cumulative = true(§1.5), иначе 422INVALID_PERIOD(правка одного счёта период без квартала не трогает);osv_account_code— непустой, нормализуется как при маппинге (§1,60.1 → 60.01), пустой — 422INVALID_ACCOUNT_CODE; счёт уже есть у другой ОСВ того же файла — 409DUPLICATE_ACCOUNT(detail.document_id; одна ОСВ по счёту на файл, §9.5 п. 2); документ привязан к версии не «в работе» и неCANCELLED— 409VERSION_NOT_EDITABLE(detail.version_ids; версия послеVERIFIED— read-only). Блокировка версии (consolidation_lock) не нужна — как для подтверждения. Правка на месте, исходное рядом (первичка не мутируется): новые значения пишутся в те же поляdocument; при первой правке прежние значения всех четырёх полей копируются вdocument.original_valuesи дальше не меняются;is_modified= текущие четыре поля отличаются отoriginal_values(возврат к исходному снимает признак);modified_by/at. Копияdocumentне создаётся (file_ordinal— номер документа в результате, не номер редакции). Строкиosv_entryне меняются: ихaccount_code,account_id,account_root— свои коды субсчетов, счёт документа — только слот чек-листа и привязки. Подтверждённый документ (CONFIRMED, в том числе автоподтверждённый): правка сбрасывает подтверждение —status = NEEDS_CHECK,confirmed_by/at = NULL; привязки документа (consolidation_document) снимаются из всех версий «в работе» (CANCELLEDне трогаются), статус версий не меняется (как при удалении, п. 5). Документ подтверждается заново вручную (п. 4) и тогда привязывается по новому периоду и счёту (§3 п. 1); автоподтверждения после правки нет (только при маппинге). Неподтверждённый документ статус сохраняет. Пересчитывается то, что выводится из периода и счёта: подходящесть версиям, чек-лист, колонка «ОСВ» (чистые функции, отдельно не хранятся);period_checkиcounterparty_checkне меняются (§9.3). Автозамена при правке не запускается (только при маппинге, п. 3): документ с ключом, как у другого файла ЮЛ, тот файл не заменяет; при привязке документ с тем же ключом в версии заменяется (§3 п. 1). Ключ документа после правки — исправленные период и счёт: при маппинге следующего файла ЮЛ автозамена (п. 3) сравнивает уже их, а не распознанные значения. Перезагрузка файла создаёт документы заново из результата — уточнения прежних документов не переносятся. Аудит — одноaudit_event(RECOGNIZED_DATA_EDIT)(object_type = document): вbefore/afterизменённые поля, при сброшенном подтверждении —status,confirmed_by,confirmed_at, вafter_value.unbound_version_ids— версии, из которых снята привязка. Запрос без изменений — 200 без аудита. Порядок блокировок — п. 7 (файл → документ → версии по возрастаниюid). Ответ — карточка документа сis_modifiedиoriginal_values. Временный флагCONSOLIDATION_PERIOD_FROM_VERSION(OQ-161) остаётся, пока правка периода не появится на экране, и снимается отдельной задачей. - Повторная обработка файла (решения владельца 19.10.2026 и 22.10.2026, AURORA-1442; аудит — v5.21). Разрешена, только
если текущее задание файла (
current_recognition_job_id) — табличного парсера (pipeline = tabular) и завершилось сбоем обмена:timeout(error_code = TIMEOUT) илиdlq(NO_ACK). OCR (ИС Сканирование) не входит. От такого задания нет ни документов, ни подтверждений, ни правок — переносить и сбрасывать нечего. Ядро создаёт новоеrecognition_jobсattempt + 1(уникальность(file_id, attempt)), делает его текущим и публикует через outbox; файл снова уходит в обработку (§9.2). Прежнее задание остаётся в истории. Предел —max_attemptsзадания (у парсера 3): на нём — 409MAX_ATTEMPTS_REACHED; не подходит под условие — 409FILE_NOT_RETRYABLE. Права —DOCUMENT_EDITи policy по ЮЛ. Ответ — 202 и карточка файла с новым заданием. Файл, загруженный в консолидацию (stored_file.consolidation_id), повторяется, только если у этой консолидации есть версия «в работе» с ЮЛ файла в периметре, иначе 409VERSION_NOT_EDITABLE; файл без контекста — без этого условия. Пакет версии (POST /consolidations/versions/{id}/files/retry) — то же одним запросом. Версия должна быть «в работе» (послеVERIFIEDиCANCELLED— 409VERSION_NOT_EDITABLE); блокировка версии (consolidation_lock) не нужна. Рассматриваются файлы вERROR, загруженные в консолидацию этой версии (stored_file.consolidation_id), ЮЛ которых в периметре версии; файлы без контекста и файлы других статусов в пакет не входят и не блокируются. Ответ — 202{accepted[] {file_id, job_id, attempt}, skipped[] {file_id, code}}: вskipped— рассмотренные файлы, которые повторить нельзя, с кодомFILE_NOT_RETRYABLEилиMAX_ATTEMPTS_REACHED; пустой пакет — 202 с пустыми списками. Статус версии и периметр перепроверяются под блокировкой. Аудит —audit_event(FILE_REPROCESS)на каждый файл, отправленный на повтор (разделPRIMARY_DOCUMENTS,object_type = stored_file,object_id— файл,user_id— инициатор):before_value— прежнее задание (job_id,attempt,status,error_code),after_value— новое (job_id,attempt); при обработке пакета — ещёconsolidation_idвafter_value. Файлы пакета, не подошедшие под условие, события не пишут.FILE_UPLOADне пишется: файл не загружается заново, событие загрузки означало бы вторую загрузку, которой не было. Смены статусов нового задания и файла в обмене аудит не пишут, как и при загрузке (п. 6, OQ-55).
§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— тот же прогон с откатом, отчёт идентичен; в APIdry_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 (адаптеры «json ИС → кандидат», «лист Excel → кандидат»). Шаги 2–8 — общий код с общими словарями. Единственное место, где общий код смотрит на источник, — разбор запятой и точки в сумме (шаг 4).
- Ничего не выбрасывать молча. Строка, колонка или ячейка, которую не удалось истолковать, идёт дальше со
статусом, а не превращается в счётчик; положение в источнике (
ref), порядок строк (row_index) и сырой текст ячейки (text) доезжают до базы и экрана. Подтвердить документ, не разобрав строки с суммами без кода, нельзя (Р-13). - Качество — структурно и с привязкой к месту: уверенность ячейки (от источника),
is_recognized(разбор числа), статус строки, нарушения §7 по ячейкам, предупреждения с уровнем и привязкой. Уверенность,is_recognizedи §7 сервис сводит в один признакneeds_reviewу значения, строки и поля шапки и из них выводит статус документа. Ядро при загрузке ничего из этого не пересчитывает. - Один словарь — одно место: проверки §7 (полный и упрощённый наборы), периоды, заголовки форм, подписи колонок, упорядоченные перечни кодов форм по редакциям и вариантам (без наименований), диапазоны кодов — в общем коде сервисов; соответствие «код → наименование» — только в ядре (Р-3).
- Очистка 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).
- Классификация листов: лист ОСВ (признак ведомости в первых 30 строках, §1) → форма → продолжение → прочее. Форма —
заголовок («Бухгалтерский баланс», «Отчет о финансовых результатах», «…о прибылях и убытках») или ОКУД в любой строке до
таблицы; титул «Бухгалтерская отчетность» — не тип. Продолжение — лист без заголовка, но с маркером «Форма … с.2», или
с подписью «Код» и периодами правее, или с колонкой кодов, или с одним-двумя кодами сразу после листа формы. Прочее —
SHEET_OTHER(info), если у листа нет ни типа, ни кодов. - Блоки форм на листе: строки с заголовком или ОКУД — кандидаты; без кодов между ними блоки сливаются, заголовок
после кодов — новая форма. Шапка своя у каждого блока в его границах: организация (после метки «Организация»,
иначе текст с признаком ОПФ строкой выше метки), ИНН 10/12 цифр у метки «ИНН» (ИНН аудитора не берётся: строки после
метки «аудитор» исключены), ОКПО, ОКОПФ, ОКФС, ОКЕИ, адрес, единицы, дата (подзаголовок формы или тройка «число | месяц
| год» у метки «Дата»; для ОФР — конец периода). Шапка другого листа не используется. Найденное поле —
Cellсconfidence = 100, не найденное —null. ОКЕИ — полеokeiшапки контракта. - Ячейки:
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. - Склейка листов-продолжений — до раскладки. Строки продолжения дописываются в конец сетки открытой формы по
маркеру «Форма … с.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). - Пятизначные подстроки (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), первые четыре — в кодовом
диапазоне. Многострочная ячейка — код, только если каждая её часть код.
-
Столбец кодов. (а) Столбец с подписью из словаря («код», «коды», «код строки», «код показателя» — после нормализации регистра и пунктуации) над первой строкой с кодом, если под подписью доля кодов среди непустых ≥ 0,6 или кодов ≥ 3; несколько кандидатов — с наибольшим числом кодов; берётся последняя такая подпись над первой строкой с кодом. Защита обязательна: в выгрузках 1С под «Коды» титульного блока стоят ОКУД, дата, ОКПО, ИНН, ОКОПФ (12267, 12300 — «коды баланса» по первым цифрам), ОКЕИ — один код из девяти, доля 0,11. Документ из двух строк остаётся документом (два из двух, доля 1,0). (б) Подписи нет — столбец с наибольшим числом ячеек-кодов, кодов ≥ 3, доля ≥ 0,6. Нет —
CODE_COLUMN_NOT_FOUND; сопоставление по наименованиям строк не делается (OQ-139). -
Сегменты. Новый сегмент открывает строка-шапка (в ней есть подпись кода из словаря, или не менее двух её непустых ячеек совпадают после нормализации с ячейками шапки первого сегмента) или граница листа из
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). -
Дата колонки — один словарь периодов
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). - абсолютные подписи →
-
Строка заголовка — последняя над первой строкой с кодом, где в столбце кодов стоит подпись кода; иначе последняя над первой строкой с кодом с двумя и более подписями-периодами правее кода (или одной, не голым годом); двухстрочный заголовок («На» + «20__ г.», перенос внутри ячейки) склеивается. Роль
title— только строки выше найденной строки заголовка (титульный блок «Дата 30 | 9 | 2025»); в сегменте без заголовка ролиtitleнет, строки выше первого кода с содержимым идут в данные. Заголовка нет ни у одного сегмента → колонки значений первого сегмента — столбцы правее кода, где в строках с кодами числа,labelпустая,period_date = null,period_kind = none, одноHEADER_ROW_NOT_FOUND(error; прежний синтез шапки шлюза и «рулинг 06.10.2026» заменены этим правилом). Заголовок есть хотя бы у одного сегмента — остальные берут его колонки без предупреждения (листы 2–5 ББ НПЗ без шапки остаютсяdone). -
Строка нумерации «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») кодом не считается — вне кодовых диапазонов. Строка сразу под строкой заголовка сегмента (первой или повторной) проверяется мягче: ячейка со сбоем OCR даёт своё единственное целое («• ■ 1» → 1, «3гола» → 3, «гола\n4» → 4), ячейка без цифр («/» вместо 1, «V» вместо 4) — пропуск, ячейка с двумя целыми — не нумерация; дальше те же условия (три числа, шаг +1, начало 1 или 2, один пропуск), так что строка с кодом строки формы нумерацией не станет. В остальных строках — только строгая проверка. Повторная шапка, у которой под каждой подписью склеен номер столбца («Коды\n2», «На 30 июня 2026 года\n3»), распознаётся по подписям без этого хвоста. Решение владельца 22.10.2026 (ретест AURORA-1517: «Светлый путь», «Будамшинское»; arm-statements#8). -
Подпись без периода (Р-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), не в адаптере хоста. -
Столбец наименования — подпись, начинающаяся с «наименование», в строке заголовка; иначе столбец левее кода с наибольшим числом текстов, кроме «Пояснения» (пустые или ≤ 3 символов); наименование строки — ячейка этого столбца, пуста — ближайшая текстовая ячейка левее кода, кроме «Пояснения».
-
Номер колонки (
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 и текст непуст и не прочерк.
valueесть (Excel): число →amountс квантованием до 2 знаков half-up, не больше 16 целых цифр (иначеamount = null,string_value= текст,is_recognized = false,VALUE_OUT_OF_RANGEinfo); дата или логическое →amount = null,string_value= текст,is_recognized = false.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).
- Пробел внутри числа допустим только как разделитель тысяч (группы ровно по три цифры); иначе
amount— «лучшая догадка» (цифры подряд, не длиннее 16 цифр, иначеnull),is_recognized = false, сырой текст — вtext(«42831 1» → 428311; AURORA-1511, 1519). Многострочное значение в строке с однострочным кодом →amount = null,string_value= текст,is_recognized = false. Excel-ячейки «число текстом» идут тем же путём. is_recognized— один смысл у обоих: текст разобран как число без выброшенных и исправленных символов (замена двойника,}/]вместо скобки — исправление).confidence— отдельно, от источника (у Excel — 100); прежнее «is_recognized=confidence == 100и ничего не выброшено» шлюза отменено.- Пустая ячейка (Р-2): позиции нет, текст пуст или прочерк →
amount = null,string_value = null,is_recognized = true,confidenceячейки (позиции нет — 100); собственной подсветки нет. Непрочитанная ячейка ИС (пустой текст, уверенность 0) нуждается в покрытии по §11.9. - Единицы: «тыс. руб.» и ≥ 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 — тип формы, вариант, документ
- Тип — большинство кодов строк
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_FOUNDerror);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. - Вариант формы
form_variant— только по структуре кодов всех строкok(ядро определяет его заново при каждом пересчёте после правки; ОКУД 0710096 — код КНД комплекта, на бланках 0710001 / 0710002, признаком не служит): ББ — есть 1600 и нет ни 1100, ни 1200 →simplified; ОФР — есть 2400 или 2110 и нет ни 2100, ни 2200, ни 2300 →simplified; иначеfull; уDISCLOSURE/OTHER—null. Шаг 7 и перечень кодов берут набор варианта. Образец упрощённой формы — в fixtures при первой возможности (OQ-166). 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.