Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
16 KiB
MODIFIED Requirements
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С показывает счёт нетто,
а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки CURRENCY/QUANTITY в суммирование не входят и сами
родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (пустая ОСВ,
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 подгонять
структуру под сходимость.
Исключение — счета без сальдо по аналитике (решение владельца 07.10.2026, OQ-132): у 91 и 99 строки аналитики несут
только обороты, сальдо закрыто на субсчёт закрытия (91.09). Список счетов — данные rules/osv_checks.yaml
(accounts_without_analytic_balance, начальный состав 91, 99; 90 не входит). Строка относится к такому счёту, если
код ближайшего счёта её ветки (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=…нет
Requirement: Форматы, лимиты, время
Парсер SHALL читать XLSX (openpyxl, полное чтение), XLS (xlrd, formatting_info), XLSB (pyxlsb; outline level недоступен —
каскад начинается с отступа). Несоответствие 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: Превышен лимит
- 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(«распакованный размер … больше лимита»), книга не открывается