34 KiB
Raw Blame History

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