period_groups в osv_groups.yaml, признак is_period у ClassifiedRow; родитель, все дети которого периоды: обороты = Σ, начало = начало первого, конец = конец последнего. НКЗ лист 58 убран из манифеста ожидаемых needs_check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 KiB
osv-parsing Specification
Purpose
Разбор оборотно-сальдовых ведомостей из XLS/XLSX/XLSB по 05-domain-rules.md §1: структура определяется по содержимому,
результат — osvDocument контракта v1.1, достоверность подтверждается контрольными суммами, словари — данные.
Requirements
Requirement: Листы ОСВ находятся по содержимому
Парсер SHALL считать листом ОСВ любой лист, где ячейка в первых 30 строках листа (SHEET_MARKER_ROWS: шапка ведомости
всегда над таблицей) матчит признак ОСВ из rules/osv_headers.yaml (без учёта регистра, «ё/е», переносов строк и
дефисов). Лист с признаками формы ББ/ОФР или её продолжения уходит в конвейер форм (спека statement-parsing); прочие листы
SHALL пропускаться с предупреждением файла SHEET_OTHER (severity = info, locator = sheet:<имя>) в
metrics.warnings. Книга без единого листа ОСВ и без форм → status = error, error.code = INVALID_DOCUMENT
(«в книге нет листов ОСВ или форм»). Несколько листов ОСВ → несколько documents с locator = sheet:<имя>.
Имя листа MUST NOT использоваться для поиска.
Scenario: Один лист — одна ОСВ (все реальные фикстуры)
- WHEN разбирается любой файл из
fixtures/osv/** - THEN
documentsсодержит ровно одинosvDocumentсlocator = "sheet:Лист_1", статусdone
Scenario: Два листа ОСВ и посторонний лист
- WHEN разбирается синтетический файл с листами «ОСВ 60», «Справка», «ОСВ 62»
- THEN
documentsсодержит два документа в порядке листов, вmetrics.warningsестьSHEET_OTHERсlocator = "sheet:Справка"
Scenario: Нет ОСВ
- WHEN разбирается книга с одним листом произвольной таблицы
- THEN
status = error,error.code = INVALID_DOCUMENT, сообщение называет число просмотренных листов
Requirement: Заголовок таблицы и колонки сумм определяются по тексту
Парсер SHALL найти блок заголовка таблицы: 1–4 подряд идущих строки, в которых (с учётом объединённых ячеек) встречаются
группы «сальдо на начало», «обороты за период», «сальдо на конец» по синонимам словаря, и под каждой группой подзаголовки
«Дебет»/«Кредит» по синонимам. Группы «обороты» и «сальдо на конец» обязательны: строка, где их нет, заголовком не
считается (R-12). Допустимы: отсутствие группы «начало» (сальдо на начало null), одна колонка в группе без
Дебет/Кредит (свёрнутое сальдо: положительное — дебет, отрицательное — кредит, предупреждение), дополнительные колонки
(«Количество», «Валюта», «ИНН», «КПП», «Код»). Колонка текста строки — первая непустая слева от сумм по строкам данных;
колонка ИНН — по заголовку ИНН или колонка, где ≥ 80 % непустых значений — ровно 10 или 12 цифр (число из 9/11 цифр
при этом выборе MUST NOT дополняться нулём: иначе числовая колонка КПП сошла бы за колонку ИНН, R-24). Правая граница таблицы —
последняя колонка «Кредит»; всё правее MUST NOT попадать в суммы. Колонки внутри таблицы, не сопоставленные с шестью
суммами и ИНН, SHALL попадать в rows[].extra по заголовку. Заголовок не найден или в найденной строке групп нет «оборотов» либо «сальдо
на конец» → status = error, error.code = INVALID_FORMAT с названием ненайденной группы, листом и шагом. В columns SHALL быть записана диагностика (буква
колонки) для каждой из шести сумм и ИНН.
Scenario: Плавающие колонки двух организаций
- WHEN разбираются
rb/2025-Q1/ОСВ сч 60 за 1 кв 2025 год.xlsxиschastl/2024-Q3/…62 за 3 квартал 2024 г..xls - THEN
columnsпервого содержитopening_debit = "E",closing_credit = "Q", второго —opening_debit = "G",closing_credit = "S",inn = "E"; суммы корневой строки совпадают со строкой «Итого»
Scenario: Мусор правее таблицы
- WHEN разбирается
schastl/2024-Q3/…62 за 3 квартал 2024 г..xls(числа в колонках T/U) - THEN эти значения не входят ни в одну из шести сумм, инварианты §1.4 выполнены
Scenario: ОСВ без сальдо на начало и со свёрнутым сальдо
- WHEN разбирается синтетическая ОСВ с группами только «Обороты» и «Сальдо на конец», где у «конца» одна колонка
- THEN
opening_* = nullу всех строк, отрицательное свёрнутое сальдо попадает вclosing_credit, вwarningsесть запись о свёрнутом сальдо, статусdone
Scenario: Неизвестная колонка внутри таблицы
- WHEN в синтетической ОСВ между «ИНН» и суммами есть колонка «КПП»
- THEN у строк контрагентов
extra["КПП"]содержит значение, суммы не сдвинуты
Requirement: Шапка документа и схема группировок
Парсер SHALL извлечь из области выше заголовка таблицы: organization_name (хвост метки «План счетов <организация>»; иначе первая непустая текстовая строка, не являющаяся меткой шапки и не лежащая между строкой названия ведомости и строкой периода; иначе хвост названия ведомости; trim),
account_code_raw (после «по счету»/«по счёту» либо в соседней ячейке строки после метки «Счет Поставщика»/«Счет Клиента» («60*» -> «60»; метка без значения пропускается), в том числе через двоеточие «по счету: 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 разбирается синтетическая ОСВ
s45(выгрузка 1С «ОСВ по счёту 60 по контрагентам»: шапка «Счет Поставщика» | «60*» в соседних ячейках, «Счет Клиента» без значения, слов «по счету» нет; в первой колонке голые числовые коды 441, 431, 199) - THEN
account_code_raw = "60"; голый числовой код, не совпадающий со счётом шапки, счётом не считается (это код контрагента); строки —COUNTERPARTYпо плану счетов,done
Scenario: Выгрузка «по контрагентам»: блоки субсчетов с кодами контрагентов
- WHEN разбирается синтетическая ОСВ
s46(шапка «За период | 01.01.25..30.06.25», «Счет Поставщика | 60*», вид субконто «Поставщики» в колонке суммы первой строки данных; субсчета 60.111/60.112 без сумм; код контрагента с суммами, строка названия без сумм, договоры с суммами; «ИТОГ по 60.111»; «ИТОГ») - THEN период 2025, Q2, нарастающий (метка «За период» со значением в соседней ячейке, разделитель «..», год из двух
цифр);
group_schema = [ACCOUNT, COUNTERPARTY(Поставщики)];hierarchy=code_blocks(субсчёт → контрагент → договор); субсчета —ACCOUNTс суммами из «ИТОГ по …» (строк «ИТОГ по …», названий и «Поставщики» вrowsнет); коды —COUNTERPARTYсcounterparty_name= строка названия,counterparty_inn = null; договоры (в том числе «441») —CONTRACT; «ИТОГ» —TOTAL, таблица кончается на нём;done. Субсчёт с тремя цифрами в сегменте — строка-счёт, только если код начинается со счёта шапки; голый числовой код, не совпадающий со счётом шапки, счётом не считается.
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 — первым найденным из:
- колонки ИНН: цифры текстовой ячейки («ИНН 352819549855» → «352819549855», R-15), склеенная «ИНН/КПП» (
inn_kpp_cell) → ИНН; результат только при 10 или 12 цифрах, иначеnull; число из 9/11 цифр (ИНН регионов 01–09 с потерянным ведущим нулём) дополняется нулём слева (R-24); - строки ИНН, склеенной с этой строкой (требование «Строки ИНН рядом с контрагентом»);
- конца текста названия: 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): хранить ли «Итого», решает ядро); склеенные строки ИНН вrowsMUST 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: Числа — Decimal, пусто — null
Суммы SHALL приводиться к Decimal с двумя знаками: числовые ячейки — через строковое представление; текстовые —
после удаления пробелов (включая неразрывные и узкие), замены запятой на точку, (x) → -x; «-», «—», «–», пустая ячейка →
null (не 0). Нечисловой текст в колонке сумм → null и предупреждение с row_number. float в результате MUST NOT
использоваться; в JSON суммы — строки ^-?\d+(\.\d{1,2})?$.
Scenario: Текстовые числа
- WHEN в синтетической ОСВ суммы записаны как «1 234 567,89», «(1 234,00)», «—»
- THEN получены
1234567.89,-1234.00,null
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), в каком бы порядке они ни стояли; итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если «Итого» свёрнута (не больше одной ненулевой стороны сальдо), а корневая строка развёрнута (в 1С так бывает у счёта с развёрнутым сальдо; «Итого развернутое» равна корню), «Итого» SHALL сверяться с единственным корнем так же, как с родителем и детьми (R-21): обороты по колонкам, сальдо по нетто «дебет − кредит» (AURORA-1708, #73); расхождение по нетто или оборотам остаётся нарушением. Если корневой 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: Развёрнутый родитель над свёрнутым в нетто ребёнком
- WHEN сальдо родителя развёрнуто (обе стороны), а среди его прямых детей есть «нетто-ребёнок» — ребёнок, прошедший сверку со своими детьми только по нетто, либо имеющий такого потомка (признак передаётся вверх по дереву), и нетто детей равно нетто родителя
- THEN нарушения по сальдо нет (1С схлопнула Дт/Кт субсчёта в нетто, а в родителе они остались развёрнутыми, AURORA-1863); обороты сверяются по колонкам, неверное нетто или расхождение оборотов — нарушение; развёрнутый родитель без нетто-ребёнка по-прежнему сверяется по колонкам (расхождение даже на 0,05 при равном нетто — нарушение)
Scenario: Периоды под контрагентом — цепочка сальдо
- WHEN все дети родителя — строки группировки «Период» (
period_groupsсловаряosv_groups.yaml; в выгрузке 1С «Субсчета + Контрагенты» это помесячные строки «Обороты за <месяц>»; тип группы остаётсяOTHER, у строки признакis_period) - THEN обороты родителя сверяются с суммой оборотов периодов, сальдо начала — с сальдо начала первого периода, сальдо конца — с сальдо конца последнего (в порядке строк; сальдо сверяется по колонкам либо по нетто, как обычно), а не с суммой сальдо месяцев (AURORA-1823); расхождение в обороте, начале первого или конце последнего периода — нарушение; у родителя со смешанными детьми (периоды и обычные строки) суммы считаются как прежде
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
total_row_matches_root = true, предупрежденияOSV_TOTALнет; при расхождении нетто сальдо или оборотовtotal_row_matches_root = false(AURORA-1708, #73)
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
Requirement: Форматы, лимиты, время
Парсер SHALL читать XLSX (openpyxl, полное чтение), XLS (xlrd, formatting_info), XLSB (pyxlsb; outline level недоступен —
каскад начинается с отступа). Имена частей xlsx SHALL сравниваться без учёта регистра, как в Excel (AURORA-1707): часть,
отличающаяся от ссылки [Content_Types].xml (Override PartName) или *.rels (Target внутренней связи) только
регистром, читается под именем из ссылки; две части под одну ссылку не угадываются. Несоответствие file_type сигнатуре файла → попытка по сигнатуре и предупреждение файла FILE_TYPE_MISMATCH (severity = info) в metrics.warnings.
Файл больше PARSE_MAX_BYTES (по умолчанию 50 МиБ) → error UNSUPPORTED_FORMAT с текстом про лимит. До вызова
библиотеки чтения SHALL действовать лимиты чтения (R-23) → error UNSUPPORTED_FORMAT с текстом про лимит: сумма
объявленных несжатых размеров записей zip (xlsx/xlsb) не больше ZIP_UNCOMPRESSED_LIMIT_FACTOR × PARSE_MAX_BYTES (10 ×);
суммарная площадь объединённых ячеек книги не больше MERGED_AREA_LIMIT (200 000 ячеек). Для xlsx — до openpyxl
потоковым разбором xml.parsers.expat всех частей архива (R-25, I-1б: каждая некаталожная запись в любом каталоге,
без отбора по имени или началу содержимого; ошибка разбора части не *.xml лишь заканчивает её скан; элемент с локальным именем mergeCell в любом пространстве имён; только атрибут ref без
префикса; ссылки на символы раскрываются; площадь — по строке ref, без перебора ячеек; отказ сразу при превышении);
для любого формата — после чтения, по диапазонам, которые индекс листа раскладывает по ячейкам (площадь не больше
grid.MAX_INDEXED_AREA; merged на весь столбец не считается); число остальных (крупных) диапазонов листа — не больше
LARGE_MERGED_COUNT_LIMIT (1000). Книга не открывается (в том числе битый XML части
*.xml, ref объединения, который openpyxl не разберёт) → UNREADABLE (лог WARNING);
превышение PARSE_TIMEOUT_SECONDS (по умолчанию 120, проверка между листами, в циклах по всему листу — поиск заголовка
таблицы, дополнительные колонки, строки, стратегии иерархии; чтение книги библиотекой таймаут не прерывает) → TIMEOUT;
непредвиденное исключение → RECOGNITION_FAILED с одной строкой причины, стек — в лог уровня ERROR. Не ограничены (остаточный
риск, решение платформы — лимит памяти контейнера, defusedxml): объём самих ячеек, sharedStrings и стилей в
пределах лимита распакованного zip, XML-бомбы entity expansion (кроме защиты самого expat ≥ 2.4). Любой файл
fixtures/osv/** SHALL разбираться быстрее 5 с.
Scenario: XLSB
- WHEN разбирается синтетический xlsb той же ОСВ, что
s02(s13, собран тестовым писателем BIFF12, а не Excel — O-7) - THEN результат совпадает с xlsx-оригиналом во всём, кроме
locatorи предупреждения документаOSV_XLSB_NO_OUTLINE(«xlsb: outline level недоступен»,info);hierarchy=indent
Scenario: Расширение не совпадает с содержимым
- WHEN запрос говорит
XLSX, а байты — BIFF8 - THEN файл прочитан как xls, в
metrics.warnings—FILE_TYPE_MISMATCH(info), статус по содержимому
Scenario: Имя части xlsx в другом регистре
- WHEN таблица строк лежит в архиве как
xl/SharedStrings.xml, а ссылки ведут наxl/sharedStrings.xml(или листSheet1.xmlпри ссылке наsheet1.xml) - THEN книга читается так же, как с именами из ссылок; не
UNREADABLEи без потери листа
Scenario: xls без кодовой страницы
- WHEN разбирается xls без записи CODEPAGE (синтетика,
codepage=None) - THEN строки прочитаны как cp1251, в
metrics.warnings— предупреждение файлаXLS_NO_CODEPAGE(info, безlocator; код пакета,OSV_-кода для него нет); статус по содержимому
Scenario: Превышен лимит
- WHEN передан файл в 51 МиБ
- THEN
error UNSUPPORTED_FORMAT, книга не открывается
Scenario: Бомба объединённых ячеек
- WHEN передан xlsx в несколько килобайт с
<mergeCell ref="A1:Z200000"/>(5,2 млн ячеек) - THEN
error UNSUPPORTED_FORMAT(«лимит объединённых ячеек») быстрее 1 с, openpyxl не вызывается
Scenario: Обходы скана объединённых ячеек
- WHEN тот же диапазон спрятан: лист вне
xl/worksheets/или с расширением не.xml(цель связи вworkbook.xml.rels),refзаписан ссылками на символы (A1:Z200000,A1:…), передrefстоит атрибут с префиксом (a:ref="A1"), в значении другого атрибута есть>, элемент записан с префиксом (x:mergeCell) - THEN
error UNSUPPORTED_FORMAT(«лимит объединённых ячеек») быстрее 1 с, openpyxl не вызывается; тот же вариант с малым диапазоном читается, и объединение видно в листе
Scenario: Объединение на весь столбец в xls
- WHEN в книге xls есть законный merged-диапазон на весь столбец (больше
grid.MAX_INDEXED_AREA) - THEN книга читается: после-чтения проверка его не считает (индекс листа его по ячейкам не раскладывает)
Scenario: Распакованный zip больше лимита
- WHEN zip книги распаковывается больше чем в 10 ×
PARSE_MAX_BYTES - THEN
error UNSUPPORTED_FORMAT(«распакованный размер … больше лимита»), книга не открывается
Requirement: Словари — данные, не код
Признаки листов, синонимы групп колонок и подзаголовков, колонки ИНН, строки «Итого», маркеров шапки (rules/osv_headers.yaml:
sheet_markers, statement_markers, groups, debit, credit, service_columns, total_row, header_markers),
группировок (rules/osv_groups.yaml: mapping, default), периодов (rules/periods.yaml: rules, year, end),
типов данных и кодов валют (rules/data_types.yaml: header, row, currency_codes, currency_aliases)
SHALL загружаться из YAML и валидироваться pydantic-моделью при старте (ошибка словаря — падение старта с указанием файла и
записи). В src/parser/ (кроме grid.py и readers/) MUST NOT быть строковых литералов букв колонок, адресов ячеек,
имён листов и сравнений с номерами строк — проверяется тестом по AST. Каждая запись распознавания словаря (паттерн) SHALL
срабатывать хотя бы на одной фикстуре или синтетике (тест покрытия словарей, R-22); перечень кодов валют
currency_codes (ISO 4217) — список значений, а не синонимы, и в покрытие не входит.
Scenario: Новый синоним без релиза кода
- WHEN в
osv_headers.yamlдобавлен синоним «Нач. остаток» и дана синтетическая ОСВ с таким заголовком - THEN разбор проходит без изменений в
src/; без этого синонима заголовок «Нач. остаток» не распознаётся (колонки начала уходят вextra, результатneeds_check)
Scenario: Литерал координаты
- WHEN в
detect.pyпоявляется строка"E9" - THEN тест-линтер падает с указанием файла и строки
Scenario: Мёртвая запись словаря
- WHEN в словарь добавлена запись, не срабатывающая ни на одном реальном или синтетическом файле
- THEN тест покрытия падает с указанием файла, ключа и паттерна
Requirement: Золотые снимки и property-тесты
Для каждого файла fixtures/osv/** SHALL существовать снимок fixtures/golden/expected_osv_parse/<зеркальный путь>.json
(= файл результата без job_id, file_id, service.version, service.statements_version, metrics.duration_ms); тест сравнивает разбор со снимком,
обновление — только явной командой (make golden-update) с просмотром diff. Property-тест по всем файлам SHALL проверять
инварианты независимо от снимков (статус ≠ error, контрольные суммы, корень = счёт шапки, период распознан). Без
каталога fixtures оба теста пропускаются с причиной (ARM_KA_FIXTURES).
Scenario: Регрессия
- WHEN правка парсера меняет
levelодной строки в любом реальном файле - THEN golden-тест падает с diff по этому файлу
Scenario: Новый файл без снимка
- WHEN в
fixtures/osv/добавлен файл без снимка - THEN property-тест его разбирает, golden-тест сообщает «нет снимка: выполните make golden-update»
Requirement: Строки ИНН рядом с контрагентом
Между построением иерархии и классификацией парсер SHALL найти строки ИНН и сопоставить каждую с её строкой-наименованием в любой из восьми раскладок: ИНН над / под наименованием × в дереве (родитель или ребёнок наименования) / не в дереве (сосед того же родителя) × с суммами / без сумм.
Строка ИНН — не счёт, не «Итого», не строка Вал./Кол., подпись которой целиком — ИНН по словарю inn_only_row
(10 или 12 цифр числом или текстом, «ИНН 7701234567», «ИНН: 7701234567 / 770101001»); числовая ячейка из 9/11 цифр
дополняется нулём слева (R-24, #34). Без подсказки <группировка>.ИНН в шапке ИНН MUST пройти контрольную сумму ИНН
(алгоритм ФНС); строка из 10/12 цифр с неверной контрольной суммой рядом с наименованием MUST NOT считаться строкой ИНН:
она классифицируется как обычно, в warnings — «строка N: похожа на ИНН X, но контрольная сумма не сходится — строка
оставлена как есть», документ — needs_check. С подсказкой контрольная сумма не проверяется, а пустая подпись строки-ребёнка наименования — строка ИНН
«ИНН не указан» (склейка без ИНН, без needs_check, R-1 #32). Наименование — строка не счёт, не «Итого», не Вал./Кол.,
не строка ИНН. С подсказкой «под» единственный ребёнок наименования с теми же суммами и непустой подписью, которая не
ИНН, — строка ИНН с нераспознанным ИНН (без пары, «ИНН не распознан»); такой ребёнок бывает только у наименования, которое
по схеме — COUNTERPARTY (договоры под ним остаются договорами, design #22); при другой позиции по схеме (подразделение и
т. д.) ребёнок — подлинное наименование; если позиции по схеме нет, ребёнком MUST быть только строка без детей (лист,
design #14).
Пара строки ИНН (связь в дереве важнее соседства):
- у строки ИНН есть дети → пара — её единственный ребёнок-наименование; несколько детей — пары нет. Если в
group_schemaбольше одной группировки, ни одна строка листа (кроме Вал./Кол.) не стоит глубже схемы и в шапке нет подсказки<группировка>.ИНН, строка ИНН не склеивается и без предупреждения классифицируется по схеме (design #16); - родитель строки ИНН — наименование → пара — родитель; несколько строк ИНН у одного наименования — пары нет. Пара — родитель только при подсказке «под» или если родитель по схеме — COUNTERPARTY (или позиция вне схемы); иначе у строки ИНН с детьми действует правило «есть дети», а без детей она — сосед (следующий пункт). Строка ИНН с детьми под наименованием при подсказке «над» и родителе-контрагенте по схеме остаётся у родителя (дети — договоры), а противоречие подсказке подсвечивается (design #23);
- иначе — непосредственный сосед того же родителя: при суммах — сосед с теми же суммами (совпали оба — по направлению); без сумм — сосед по направлению листа (над → следующий, под → предыдущий).
Если у строки ИНН есть суммы, все шесть MUST совпасть с суммами пары (пустая ячейка = 0), иначе пары нет. Направление
листа — подсказка шапки (порядок <группировка>.ИНН), если она есть; иначе сведения пар соседей по суммам и однозначных
блоков детей одного родителя из несопоставленных строк ИНН и наименований (начинается строкой ИНН — над; кончается строкой
ИНН — под; начинается и кончается — блок сломан, его строки ИНН без пары) берутся вместе; если они противоречат друг другу
или их нет — направление неизвестно, строки ИНН без сумм, которым оно нужно, остаются без пары. Две строки ИНН с одной парой, прошедшие проверку сумм, — обе без пары (design #26).
Склейка: строка ИНН MUST NOT попадать в rows, её ИНН — наименованию (источник 2 требования «Классификация строк»),
её дети переходят к наименованию, при «над, в дереве» наименование с поддеревом поднимается на её место; в warnings —
«строка N: ИНН … отнесён к контрагенту в строке M». ИНН колонки ИНН у наименования, отличный от ИНН строки, остаётся,
в warnings — расхождение, документ — needs_check.
Противоречие подсказке шапки: если пара найдена, но фактическая раскладка противоречит направлению подсказки
<группировка>.ИНН (подсказка «над», а строка ИНН — ребёнок наименования или сосед ниже него по суммам; подсказка «под»,
а строка ИНН — родитель наименования или сосед выше него по суммам), строка ИНН MUST склеиваться как обычно, а в
warnings MUST быть отдельное предупреждение «строка N: ИНН … стоит <под/над> наименованием в строке M, а шапка
(«<группировка>.ИНН») указывает <над/под> — проверьте»; документ — needs_check. Для соседей без сумм направление берётся
из подсказки, конфликта нет. Раскладка, совпадающая с подсказкой (в том числе «под, в дереве» в 1С), предупреждения не даёт.
Страховка после классификации: ИНН склеен со строкой, которая по схеме не стала COUNTERPARTY, — в warnings «строка N:
ИНН … отнесён к строке M, но она не контрагент — проверьте», документ — needs_check (design #24).
Без пары: строка ИНН остаётся в rows с group_type = OTHER (в свод ядра не попадает), в warnings — «строка N:
ИНН … без пары — <причина>», документ — needs_check. Структура под склейку MUST NOT подгоняться.
Scenario: Восемь раскладок без подсказки
- WHEN разбираются синтетические ОСВ
s20…s27(по одной на раскладку; ИНН с верной контрольной суммой; шапка безКонтрагенты.ИНН) - THEN у каждого контрагента
counterparty_inn— ИНН его строки ИНН, строк ИНН вrowsнет, контрагентов столько же, сколько наименований, контрольные суммы сходятся, статусdone
Scenario: ИНН под наименованием не уходит следующему контрагенту
- WHEN в синтетической ОСВ
s27(ИНН под наименованием, не в дереве, без сумм) идут «ООО Альфа», ИНН Альфы, «ООО Бета», ИНН Беты - THEN ИНН Альфы — у «ООО Альфа», ИНН Беты — у «ООО Бета»
Scenario: ИНН над наименованием (OQ-130)
- WHEN разбирается синтетическая ОСВ
s18(строка ИНН без сумм над наименованием, ИНН с верной контрольной суммой) - THEN ИНН — у контрагента под строкой, строк ИНН в
rowsнет, статусdone
Scenario: Реквизит «Контрагенты.ИНН» (OQ-162)
- WHEN разбирается
osv-dkb/novopetrovskaya/2025-Q3/60_ 9мес.2025г.xls - THEN результат совпадает с golden до изменения: 713 из 716 контрагентов с ИНН (включая ИНН с неверной контрольной
суммой), 3 физлица без ИНН, статус
done
Scenario: Неверная контрольная сумма без подсказки
- WHEN в синтетической ОСВ без подсказки над наименованием строка «7701234567» (контрольная сумма не сходится)
- THEN строка не склеивается, в
warnings— «похожа на ИНН, контрольная сумма не сходится», статусneeds_check
Scenario: Опечатка в ИНН среди верных (s28)
- WHEN разбирается синтетическая ОСВ
s28(раскладкаs20, но у ООО Альфа строка ИНН «7701234567» с неверной контрольной суммой; шапка без подсказки) - THEN строка «7701234567» остаётся обычной строкой, у ООО Альфа
counterparty_innнет, вwarnings— «строка N похожа на ИНН 7701234567, но контрольная сумма не сходится», остальные контрагенты получают свои ИНН (направление «над» — по блоку 60.02), статусneeds_check
Scenario: Раскладка противоречит подсказке шапки
- WHEN в шапке
Контрагенты.ИНН(«над»), а строка ИНН без детей стоит под родителем-контрагентом (или наоборот: подсказка «под», строка ИНН — родитель наименования) - THEN строка ИНН склеена с этим наименованием, в
warnings— предупреждение «стоит под (над) наименованием … а шапка … указывает над (под) — проверьте», статусneeds_check
Scenario: Ведущий ноль у числовой строки ИНН
- WHEN строка ИНН без подсказки — числовая ячейка
123456788(9 цифр;0123456788проходит контрольную сумму) - THEN
counterparty_inn = "0123456788"
Scenario: Неоднозначное направление
- WHEN под счётом блок «наименование, ИНН без сумм, наименование» и других сведений о направлении нет
- THEN строка ИНН без пары,
group_type = OTHER, предупреждение «направление неизвестно», статусneeds_check
Scenario: Несколько детей у строки ИНН
- WHEN у строки ИНН в дереве два ребёнка-наименования
- THEN строка ИНН без пары,
group_type = OTHER, статусneeds_check
Requirement: Строки-показатели и колонка «Показатели»
Строка-показатель — строка, которая несёт вид сумм предыдущей строки данных, а не уровень аналитики. Парсер SHALL считать строку показателем, если:
- в таблице есть колонка «Показатели» (заголовок блока по словарю
osv_headers.yaml—service_columns.indicator, «Показатели», «Показа-тели»), ячейка строки в ней непуста и метка не «БУ»: «НУ» →data_type = NU, «Кол.», «Кол. доп.» → QUANTITY, «Вал.» → CURRENCY (словарьdata_types.yaml—indicator_column); «ПР», «ВР» (indicator_column_untyped) →data_type = null; метка не из словарей →data_type = nullи предупреждениеOSV_ROWS: одно на каждую неизвестную метку, а не на каждую строку — «метка «X» в колонке «Показатели» не распознана — 2 128 строк (11, 14, …) не участвуют в проверках» (показаны первые 20 номеров; для единственной строки текст прежний: «строка N: метка «X» в колонке «Показатели» не распознана — строка не участвует в проверках»); так же группируются прочие построчные предупрежденияOSV_ROWSс одинаковым текстом («строка N: <текст>» → «<текст> — строк: K (…)»; AURORA-1724); - иначе — текст строки матчит
data_types.yaml—row(«Вал.», «Кол.», «Кол-во», «Кол-во, <единица>», «В валюте» → CURRENCY/QUANTITY) или равен коду валюты изcurrency_codes.
Строка «Валюта <код>» («Валюта USD») MUST NOT считаться показателем: это строка аналитики с рублёвыми суммами; код валюты
не хранится. Колонка «Показатели» MUST NOT быть колонкой текста строки (как колонка ИНН) и SHALL оставаться в extra
строки под своим заголовком. row_value строки с меткой колонки — сама метка.
Строки-показатели MUST NOT передаваться в каскад иерархии. Родитель строки-показателя — ближайшая строка выше, которая
сама не показатель; уровень — на один глубже родителя; показатель без строки данных выше — уровень 0 без родителя.
Строки-показатели MUST NOT входить в сверку «сумма детей = родитель» (ни как дети, ни как родители) и в сверку «Итого»;
тождество строки (начало + обороты = конец) проверяется у них, как у всех строк. Строки NU хранятся как пришли
(ASSUMPTION(OQ-45)); собственных проверок по NU нет (AURORA-1669).
Итоговые строки, следующие сразу за основной «Итого» и помеченные в колонке «Показатели» меткой показателя, SHALL
оставаться в таблице как TOTAL (data_type по метке) и MUST NOT уходить в подвал или давать предупреждение «ниже
таблицы есть числа»; сверка «Итого = корень» берёт первую итоговую строку, которая не показатель.
Scenario: Колонка «Показатели» БУ/НУ
- WHEN разбирается синтетическая ОСВ
s33(счёт 02, шапка без «Выводимые данные», колонка «Показа-тели», на каждую строку — «БУ» и «НУ», название объединено на две строки, «Итого» БУ и НУ) - THEN строки «БУ» —
data_type = BU, строки «НУ» —data_type = NU,row_value = "НУ",group_type = OTHER, родитель — строка «БУ» над ней;parent_sum_violations = [],total_row_matches_root = true, две строки TOTAL (BU и NU), предупреждения «ниже таблицы» и «вид данных … приняты как БУ» нет, статусdone
Scenario: Количество, валюта и разницы в колонке «Показатели»
- WHEN разбирается синтетическая ОСВ
s34(колонка «Показатели» с метками «Кол.», «Вал.», «ПР», «ВР» и одной неизвестной меткой) - THEN «Кол.» — QUANTITY, «Вал.» — CURRENCY, «ПР»/«ВР»/неизвестная —
data_type = null, все — показатели вне сверки родителя; одно предупреждениеOSV_ROWSо нераспознанной метке; статусdone
Scenario: «В валюте» под «Валюта <код>»
- WHEN разбирается синтетическая ОСВ
s35(под контрагентом пары «Валюта USD» / «В валюте», «Валюта EUR» / «В валюте») - THEN «В валюте» — CURRENCY, родитель — «Валюта …» над ней; контрагент = Σ строк «Валюта …» по рублям;
статус
done
Scenario: Строки «Вал.»/«Кол.» в колонке текста не меняются
- WHEN разбираются s04, s30 и все реальные фикстуры
../fixtures/osv/** - THEN golden-снимки совпадают с прежними (родитель показателя — строка над ним, как и было)