## MODIFIED Requirements ### Requirement: Шапка документа и схема группировок Парсер SHALL извлечь из области выше заголовка таблицы: `organization_name` (хвост метки «План счетов <организация>»; иначе первая непустая текстовая строка, не являющаяся меткой шапки и не лежащая между строкой названия ведомости и строкой периода; иначе хвост названия ведомости; trim), `account_code_raw` (после «по счету»/«по счёту», в том числе через двоеточие «по счету: 76.Н.1», как в файле: «60», «58.03»; код счёта шапки важнее первой строки-счёта таблицы — она используется, только если в шапке кода нет, OQ-134), `period_text` (после «за» до «г.»/конца), `data_type` («Выводимые данные: БУ/НУ/сумма»), `currency` («Валюта …: RUB/руб.» → `RUB`), `header_variant` (1 — группировки строками первой колонки блока заголовка; 2 — перечисление «субконто: …»). `group_schema` SHALL начинаться с ACCOUNT и далее перечислять группировки в порядке вложенности по словарю `rules/osv_groups.yaml`; ячейка `ИНН` в строке группировки → `has_inn = true`. Ячейки между строкой названия ведомости (с меткой «по счету») и строкой периода, если название выше периода, — **строка вида субконто** (плоская форма 1С: «Контрагенты», «Организации», «Материалы»): она MUST NOT становиться `organization_name`; если схема варианта 1 состоит из одной ACCOUNT («Субконто»), элементы этой строки (через запятую), которые словарь `rules/osv_groups.yaml` узнаёт явной записью `mapping` (не `default`), SHALL добавляться в схему после ACCOUNT; неузнанные элементы в схему MUST NOT попадать. Элемент вида `<группировка>.ИНН` (суффикс после последней точки — по словарю `service_columns.inn`, префикс — имя группировки `COUNTERPARTY` этой же шапки; вариант 2 — элемент списка «субконто: …», вариант 1 — строка блока заголовка) MUST NOT попадать в `group_schema`: группировка-владелец получает `has_inn = true`, а элемент служит **подсказкой** для строк ИНН (требование «Строки ИНН рядом с контрагентом»): стоит после владельца — ИНН под наименованием, перед владельцем — над. `period_quarter`/`is_cumulative` SHALL вычисляться только по тексту по `rules/periods.yaml` (§1.5, `ASSUMPTION(OQ-02)`), `period_year` — четыре цифры года. Отсутствие любого поля шапки, кроме обязательных по контракту, — `null` и предупреждение; отсутствие `organization_name`/`period_text` — пустая строка и предупреждение (контракт требует строку). #### Scenario: Вариант 1 с тремя строками группировок - **WHEN** разбирается `schastl/2024-Q3/…62 за 3 квартал 2024 г..xls` - **THEN** `group_schema = [ACCOUNT, DEPARTMENT, COUNTERPARTY(has_inn=true)]`, `header_variant = 1`, `account_code_raw = "62"`, `period_text = "3 квартал 2024 г."`, `period_quarter = "Q3"`, `is_cumulative = false`, `data_type = "BU"`, `currency = "RUB"` #### Scenario: ОСВ по субсчёту и годовой период - **WHEN** разбираются `rb/2024-Q3/сч 583_3 кв 2024.xlsx` и `rb/2024-FY/ОСВ_66 за 2024.xls` - **THEN** у первого `account_code_raw = "58.03"`; у второго `period_quarter = "Q4"`, `is_cumulative = true` #### Scenario: Вариант 2 - **WHEN** разбирается синтетическая ОСВ с шапкой «Период: 1 квартал 2026 г.», «Детализация по субсчетам, субконто: Контрагенты, Договоры», заголовком «Субконто» - **THEN** `header_variant = 2`, `group_schema = [ACCOUNT, COUNTERPARTY, CONTRACT]`, `period_quarter = "Q1"` #### Scenario: Реквизит ИНН после группировки - **WHEN** разбирается `osv-dkb/novopetrovskaya/2025-Q3/60_ 9мес.2025г.xls` (шапка «субконто: Контрагенты, Контрагенты.ИНН») - **THEN** `group_schema = [ACCOUNT, COUNTERPARTY(has_inn=true)]`, подсказка «ИНН под наименованием» #### Scenario: Реквизит ИНН перед группировкой - **WHEN** в синтетической ОСВ шапка «субконто: Контрагенты.ИНН, Контрагенты» - **THEN** `group_schema = [ACCOUNT, COUNTERPARTY(has_inn=true)]`, подсказка «ИНН над наименованием» #### Scenario: Плоская форма — счёт с двоеточием, вид субконто, организация - **WHEN** разбирается синтетическая ОСВ `s40` (шапка «Оборотно-сальдовая ведомость по счету: 76.Н.1», строка «Контрагенты», «за 2025 г.», строка «ООО "Товарное хозяйство"», колонка «Субконто», сразу контрагенты) - **THEN** `account_code_raw = "76.Н.1"` без предупреждения «код счёта … взят из первой строки-счёта», `organization_name = "ООО \"Товарное хозяйство\""`, `group_schema = [ACCOUNT(Субконто), COUNTERPARTY(Контрагенты)]` #### Scenario: Организация из «План счетов» - **WHEN** разбирается синтетическая ОСВ `s41` (шапка «…по счету: 60», строка «Организации», «за 2 Квартал 2026 г. План счетов ООО Хреновская Нива») - **THEN** `organization_name = "ООО Хреновская Нива"`, `account_code_raw = "60"`, `group_schema = [ACCOUNT(Субконто), COUNTERPARTY(Организации)]` (множественное «Организации» — синоним COUNTERPARTY в словаре; единственное «Организация» — собственная организация учёта, R-17) #### Scenario: Счёт шапки важнее строки таблицы - **WHEN** разбирается синтетическая ОСВ `s42` (шапка «…по счету: 76», первая строка таблицы «76.2») - **THEN** `account_code_raw = "76"` ### Requirement: Иерархия строк — каскад стратегий Парсер SHALL определять `level` строк данных первой подходящей стратегией из списка: outline level Excel; отступ ячейки или ведущие пробелы; шаблон кода счёта и жирность; плоский список под счётом шапки (`header_account`); равенство «сумма детей = родитель». Стратегия `header_account` (все строки — уровень 0) применима, только если в шапке есть код счёта и среди строк таблицы нет ни одной строки-счёта (плоская форма 1С). Стратегия подходит, если даёт дерево, где у каждой строки есть предок с меньшим уровнем (кроме уровня 0). Строка «Итого» стратегиям MUST NOT передаваться: она всегда имеет уровень 0 и не имеет родителя (R-14). Если строк данных (кроме «Итого») меньше двух (пустая ОСВ или одна строка) — стратегия `trivial`: дерево из корней (R-13). Выбранная стратегия SHALL быть записана в `warnings[]` документа листа предупреждением `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`severity = info`); в `metrics.warnings` строк `sheet=<имя> hierarchy=…` нет. `parent_row_number` SHALL указывать на ближайшую предыдущую строку с меньшим уровнем. #### Scenario: Outline level (реальные файлы) - **WHEN** разбирается `rb/2024-FY/ОСВ_66 за 2024.xls` (и любой другой реальный файл со строками данных) - **THEN** предупреждение документа `OSV_HIERARCHY_BY_OUTLINE`, строки `66` (0) → `66.01` (1) → контрагент (2); у «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» `parent_row_number` — строка `66.03` #### Scenario: Без outline level - **WHEN** разбирается синтетическая ОСВ без группировок Excel, с отступами ячеек (`s02`) - **THEN** `OSV_HIERARCHY_BY_INDENT`, дерево 0/1/2 и суммы совпадают с деревом той же ведомости, где уровни видны только по кодам счетов и жирности (`s03`) #### Scenario: Без outline и отступов - **WHEN** разбирается синтетическая ОСВ, где уровни видны только по кодам счетов и жирности (`s03`) - **THEN** `OSV_HIERARCHY_BY_ACCOUNT_PATTERN`, инварианты §1.4 выполнены, «Итого» не вложена в последний счёт #### Scenario: Пустая ОСВ — тривиальное дерево - **WHEN** в ОСВ нет строк данных кроме «Итого» - **THEN** `OSV_HIERARCHY_BY_TRIVIAL`, «Итого» — уровень 0 без родителя #### Scenario: Плоский список под счётом шапки - **WHEN** разбирается синтетическая ОСВ `s40` либо `s41` (без outline, отступов и строк-счетов; сразу контрагенты) - **THEN** вместо `INVALID_FORMAT` — `OSV_HIERARCHY_BY_HEADER_ACCOUNT`, все строки данных уровня 0 без родителя, `account_code_raw` каждой строки равен коду счёта шапки ### Requirement: Классификация строк Классификация SHALL выполняться над деревом, из которого уже убраны склеенные строки ИНН (требование «Строки ИНН рядом с контрагентом»). Каждая строка данных SHALL получить `group_type`: ACCOUNT — текст матчит `ACCOUNT_RE` (регистр учитывается, IGNORECASE нет): две-три цифры и до двух сегментов субсчёта через точку, где сегмент — `\d{1,2}` **или** 1–3 заглавные буквы кириллицы/латиницы («76.АВ», «76.ВА», «76.ОТ», R-16), далее опционально запятая и название: `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` с флагами `re.ASCII | re.DOTALL` (после запятой допустим любой пробел или его отсутствие, R-18; цифры — только 0–9; название после запятой может занимать несколько строк ячейки — M-1); `account_code_raw` — код как в файле, без названия после запятой. Не-счёт — группировка из `group_schema` по позиции (число не-счётных предков после последнего ACCOUNT + 1), при выходе за схему — OTHER, кроме **расчётных счетов**: если позиция 1 (первая не-счётная) выходит за схему, а счёт ветки — из списка `counterparty_accounts` `rules/osv_groups.yaml` (начальный состав 58, 60, 62, 66, 67, 75, 76, 80; запись `76` покрывает `76`, `76.2`, `76.Н.1`, но не `760`), строка SHALL получить COUNTERPARTY без имени группировки, а документ — предупреждение `OSV_ROWS` «вид субконто в шапке не назван: строки аналитики счёта <код> приняты за контрагентов по плану счетов — проверьте» (один раз на счёт); счёт ветки строк до первой строки-счёта — код счёта шапки; строка ИНН без пары — OTHER; строка «Итого» и вторые итоговые строки («Итого развернутое») в итоговом блоке сразу перед ней или сразу за ней, включая строки-показатели итоговых строк, — TOTAL; строки-показатели входят в блок перед «Итого», только если выше них стоит вторая итоговая строка. Для COUNTERPARTY `counterparty_name` SHALL быть текстом строки без ИНН в конце и разделителя перед ним (`row_value` — исходный текст; OQ-100), а `counterparty_inn` — первым найденным из: 1. колонки ИНН: цифры текстовой ячейки («ИНН 352819549855» → «352819549855», R-15), склеенная «ИНН/КПП» (`inn_kpp_cell`) → ИНН; результат только при 10 или 12 цифрах, иначе `null`; число из 9/11 цифр (ИНН регионов 01–09 с потерянным ведущим нулём) дополняется нулём слева (R-24); 2. строки ИНН, склеенной с этой строкой (требование «Строки ИНН рядом с контрагентом»); 3. конца текста названия: 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН» (`counterparty_inn_suffix`; «Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)»). Контрольная сумма ИНН из колонки и из названия MUST NOT проверяться. Строки-показатели (требование «Строки-показатели и колонка «Показатели»») SHALL получать `group_type = OTHER`, `data_type` по метке, MUST NOT занимать позицию в `group_schema` и MUST NOT наследовать `counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки родителя, R-19); их суммы остаются в шести полях. Строка с меткой «БУ» в колонке «Показатели» получает `data_type = BU`. Остальные строки получают `data_type` шапки, а если шапка вид данных не называет — `BU` и одно предупреждение `OSV_HEADER` на лист «вид данных в шапке не указан — строки без метки приняты как БУ» (`header.data_type` остаётся `null`). Все строки, включая TOTAL, SHALL присутствовать в `rows` (`ASSUMPTION(OQ-01)`: хранить ли «Итого», решает ядро); склеенные строки ИНН в `rows` MUST NOT попадать. #### Scenario: Подразделение, контрагент с ИНН, физлицо - **WHEN** разбирается `schastl/2024-Q3/…62 за 3 квартал 2024 г..xls` - **THEN** «БП Аренда» → DEPARTMENT; «МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО» → COUNTERPARTY с `counterparty_inn` из 10 цифр; «Ведомость в счет з/платы» → COUNTERPARTY с `counterparty_inn = null` #### Scenario: ИНН в конце названия - **WHEN** разбирается синтетическая ОСВ `s17` (ИНН в тексте названия с разными разделителями) - **THEN** у контрагентов `counterparty_name` без ИНН, `counterparty_inn` — ИНН из текста, `row_value` — исходный текст #### Scenario: «ИНН/КПП» в одной ячейке колонки ИНН - **WHEN** разбирается синтетическая ОСВ `s19` - **THEN** `counterparty_inn` — 10 цифр до «/» #### Scenario: Название субсчёта в строке счёта - **WHEN** в синтетической ОСВ строка «60.01, Расчеты с поставщиками» - **THEN** `group_type = ACCOUNT`, `account_code_raw = "60.01"`, `row_value` — исходный текст #### Scenario: Буквенный субсчёт - **WHEN** в ОСВ 76 строка «76.АВ» (реальные файлы РБ и Счастливочной) либо «60.АВ, …» (синтетика) - **THEN** `group_type = ACCOUNT`, строка наследуется детьми как счёт ветки; «76.ав» (строчные) счётом не считается #### Scenario: Строки Вал./Кол. - **WHEN** в синтетической ОСВ под контрагентом строки «USD» и «Кол.» - **THEN** `group_type = OTHER`, `data_type = CURRENCY` и `QUANTITY`, `counterparty_name`/`counterparty_inn` равны `null`, `account_code_raw` совпадает с родителем, строки исключены из проверки «сумма детей = родитель», статус `done` #### Scenario: Шапка без вида данных - **WHEN** разбирается синтетическая ОСВ `s36` (шапка без «Выводимые данные», колонки «Показатели» нет) - **THEN** `header.data_type = null`, у всех строк данных и «Итого» `data_type = BU`, в `warnings` одно `OSV_HEADER` «вид данных в шапке не указан — строки без метки приняты как БУ», статус `done` #### Scenario: «Валюта <код>» — обычная строка - **WHEN** в синтетической ОСВ `s35` под контрагентом строка «Валюта USD» с рублёвыми суммами - **THEN** `group_type = OTHER`, `data_type` шапки, строка участвует в сверке «сумма детей = родитель» #### Scenario: Контрагенты расчётного счёта без названия вида - **WHEN** разбирается синтетическая ОСВ `s43` (лист как ОСВОКТ-24 «60 сч.»: колонка «Субконто», строка «60.01», под ней контрагенты с отступом) либо `s41` - **THEN** контрагенты — COUNTERPARTY без `group_name`, `counterparty_name` — текст строки; в `warnings[]` — `OSV_ROWS` «вид субконто в шапке не назван…»; на нерасчётном счёте (синтетика `s44`, счёт 10) те же строки — OTHER без предупреждения ### Requirement: Контрольные суммы определяют статус Для каждого документа парсер SHALL проверить: по каждой строке `opening_debit − opening_credit + turnover_debit − turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по каждому родителю суммы = Σ детей с тем же `data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто, а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка «Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0) либо сумма строк уровня 0; основная «Итого» — первая строка TOTAL без признака показателя, текст которой не «Итого развернутое» (`total_row_secondary`), в каком бы порядке они ни стояли; итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если корневой ACCOUNT нет, а строки уровня 0 есть (плоский список), «Итого» SHALL сверяться с их суммой (те же допуски R-21); если строк данных нет (пустая ОСВ, R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в `warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root, balance_identity_violations, parent_sum_violations}`. Результат проверки SHALL записываться в документ листа (контракт v2.0): `status` листа — `done`, когда контрольные суммы §1.4 сошлись (и строки ИНН без нарушений), иначе `needs_check` (прежние условия §1.6, применённые к листу); любое нарушение → `status = needs_check` документа и запись причины в `warnings[]` документа. Предупреждения листа SHALL быть структурами `{code, severity, message}` с `severity = info` и кодом с префиксом `OSV_` (`OSV_HIERARCHY_BY_<СТРАТЕГИЯ>`, `OSV_HEADER`, `OSV_ROWS`, `OSV_INN_ROWS`, `OSV_NO_DATA_ROWS`, `OSV_BALANCE_IDENTITY`, `OSV_PARENT_SUM`, `OSV_TOTAL`, `OSV_XLSB_NO_OUTLINE`); строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Парсер MUST NOT подгонять структуру под сходимость. Вторая итоговая строка (`total_row_secondary`, «Итого развернутое») SHALL NOT участвовать в проверке тождества строки, только если у неё обе колонки оборотов за период пусты и на листе есть другая строка TOTAL: перед «Итого» она несёт одни развёрнутые сальдо (R-16, уточнён R-19). Вторая итоговая строка с оборотами (в 1С она бывает и после «Итого», с оборотами) и единственная итоговая строка листа проверяются тождеством как обычные; основная «Итого» проверяется как прежде. Исключение — **счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 90, 91 и 99 строки аналитики несут только обороты, сальдо закрыто на субсчёт закрытия (91.09). Список счетов — данные `rules/osv_checks.yaml` (`accounts_without_analytic_balance`, состав `90`, `91`, `99`; счёт 90 добавлен решением владельца 19.10.2026). Строка относится к такому счёту, если код ближайшего счёта её ветки (`account_code_raw`) совпадает с записью списка либо является её субсчётом (`91`, `91.01`, но не `910`). Для таких строк тождество строки SHALL NOT проверяться; сверка «сумма детей = родитель» сохраняется: обороты — всегда по колонкам, сальдо — только у родителя с дочерними субсчетами (корень → субсчета), а у родителя с одной лишь аналитикой сальдо не сверяется (сальдо аналитики пусто по определению); «Итого» сверяется как обычно. Остаточные риски (известные слепые зоны, не ловятся проверками §1.4): - RR-1 — у свёрнутого родителя сверяется только нетто сальдо: распределение сальдо детей между дебетом и кредитом (перестановка сторон при совпавшем нетто) не проверяется. - RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя, у которого в файле потеряна одна сторона, неотличимо от нетто-показа 1С и проходит, если нетто совпало. - RR-3 — нетто допускается на любом уровне дерева (счёт, субсчёт, контрагент, договор), а не только на уровне «счёт → субсчета», где его показывает 1С: ошибка распределения сальдо между договорами под свёрнутым контрагентом не ловится. - RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто строки, ловится лишь сверкой с родителем по колонкам, а под свёрнутым родителем — ничем (см. слепую зону ниже). - Слепая зона: обе стороны сальдо (на начало или на конец) развёрнутого ребёнка увеличены на одно и то же X, а его непосредственный родитель свёрнут — не ловится ничем: достаточно свёрнутого прямого родителя (сверка с ним — по нетто, а нетто ребёнка не изменилось; значения родителя и выше не затронуты; тождество строки и «Итого = корень» сохраняются). - Родитель с сальдо (0, 0) при детях (X Дт, X Кт) принимается: нулевое сальдо — свёрнутое, нетто детей X − X = 0 (закреплено тестом как осознанное поведение R-21). - Вне R-21: текст вида «12.ООО» (2–3 цифры, точка, до трёх заглавных букв) принимается за счёт (R-16) и меняет `group_type` строк ветки; суммы и проверки это не затрагивает. #### Scenario: Все реальные файлы сходятся - **WHEN** разбирается каждый файл `fixtures/osv/**` - **THEN** `status = done`, `checks.total_row_matches_root = true`, оба списка нарушений пусты #### Scenario: Испорченная сумма - **WHEN** в синтетической ОСВ у одного контрагента `closing_debit` увеличен на 1 - **THEN** `status = needs_check` документа, `balance_identity_violations` содержит его `row_number`, `parent_sum_violations` — `row_number` родителя, в `warnings[]` документа — `OSV_BALANCE_IDENTITY` / `OSV_PARENT_SUM` (`info`) с номерами строк #### Scenario: Сальдо родителя свёрнуто нетто - **WHEN** у родителя сальдо показано одной стороной (нетто), а у детей — обеими сторонами, и нетто детей равно нетто родителя - **THEN** нарушения «сумма детей = родитель» по сальдо нет; обороты при этом сверяются по колонкам, а неверное нетто или расхождение оборотов — нарушение #### Scenario: Счёт 91 — аналитика только с оборотами - **WHEN** разбирается ОСВ по счёту 91: строки аналитики 91.01, 91.02, 91.09 несут только обороты, сальдо закрыто на субсчёт закрытия (`fixtures/osv-dkb/tsimlyansky/2025-Q3/91_ 9мес.2025.xls`) - **THEN** `status = done`, оба списка нарушений пусты; обороты аналитики при этом сходятся с оборотами субсчёта, а расхождение оборотов, сальдо субсчетов с корнем или «Итого» осталось бы нарушением #### Scenario: «Итого» не найдена - **WHEN** в таблице нет строки с текстом из словаря «Итого» - **THEN** `total_row_matches_root = false`, `status = needs_check`, в `warnings[]` — `OSV_TOTAL` (`info`) «строка «Итого» не найдена» #### Scenario: Пустая ОСВ - **WHEN** разбираются реальные пустые ОСВ (в фикстурах две: `schastl/2025-Q2/…58 за 2 квартал 2025 г..xlsx` и `…66 за 2 квартал 2025 г..xlsx`: заголовок и сразу «Итого») либо любой файл, где кроме «Итого» строк данных нет - **THEN** `status = done` документа, `rows` содержит только строку TOTAL, `metrics.rows = 0` (строки данных без TOTAL), `hierarchy=trivial`, `checks.total_row_matches_root = true` (корня нет, суммы TOTAL пусты), предупреждение `OSV_NO_DATA_ROWS` «нет строк данных» #### Scenario: Предупреждение ОСВ - **WHEN** на листе ОСВ есть строка без родителя - **THEN** у документа ОСВ предупреждение `{code: "OSV_…", severity: "info", message}`, статус документа по §1.6; в `metrics.warnings` конверта строки `sheet=…` нет #### Scenario: «Итого развернутое» перед «Итого» - **WHEN** разбирается синтетическая ОСВ `s42` (субсчета 76.2, 76.5, 76.Н.1 на уровне 0, затем «Итого развернутое» и «Итого», «Итого» — свёрнутое нетто) - **THEN** обе итоговые строки — TOTAL уровня 0 без родителя, «Итого развернутое» не ребёнок «76.Н.1»; `total_row_matches_root = true` (сверка по «Итого»), нарушений нет, `status = done` #### Scenario: «Итого развернутое» без оборотов - **WHEN** у «Итого развернутое» заполнены только сальдо (обороты пусты), а у основной «Итого» тождество строки выполняется - **THEN** `balance_identity_violations` не содержит «Итого развернутое», `status = done`; при нарушении тождества у основной «Итого» её `row_number` в списке остаётся #### Scenario: «Итого развернутое» с оборотами - **WHEN** «Итого развернутое» стоит после «Итого», у неё заполнены обороты, и сальдо на конец не сходится с началом и оборотами - **THEN** её `row_number` попадает в `balance_identity_violations`; при сходящемся тождестве нарушения нет #### Scenario: Показатели второй итоговой строки без оборотов - **WHEN** колонка «Показатели», «Итого развернутое» без оборотов (до или после «Итого») и под ней строка «НУ» без оборотов, на листе есть основная «Итого» - **THEN** строка «НУ» блока пропускает тождество строки так же, как «Итого развернутое»; `balance_identity_violations` пуст, `status = done`; строка «НУ» под последней строкой данных в итоговый блок не входит #### Scenario: «Итого» плоского списка - **WHEN** разбирается синтетическая ОСВ `s40` («Итого» = сумма контрагентов, сальдо «Итого» свёрнуто) - **THEN** `total_row_matches_root = true`, `status = done`; если сумма одного контрагента испорчена — `false`, `OSV_TOTAL`, `needs_check`