diff --git a/openspec/changes/osv-flat-form/.openspec.yaml b/openspec/changes/archive/2026-10-19-osv-flat-form/.openspec.yaml similarity index 100% rename from openspec/changes/osv-flat-form/.openspec.yaml rename to openspec/changes/archive/2026-10-19-osv-flat-form/.openspec.yaml diff --git a/openspec/changes/osv-flat-form/design.md b/openspec/changes/archive/2026-10-19-osv-flat-form/design.md similarity index 100% rename from openspec/changes/osv-flat-form/design.md rename to openspec/changes/archive/2026-10-19-osv-flat-form/design.md diff --git a/openspec/changes/osv-flat-form/proposal.md b/openspec/changes/archive/2026-10-19-osv-flat-form/proposal.md similarity index 100% rename from openspec/changes/osv-flat-form/proposal.md rename to openspec/changes/archive/2026-10-19-osv-flat-form/proposal.md diff --git a/openspec/changes/osv-flat-form/specs/osv-parsing/spec.md b/openspec/changes/archive/2026-10-19-osv-flat-form/specs/osv-parsing/spec.md similarity index 100% rename from openspec/changes/osv-flat-form/specs/osv-parsing/spec.md rename to openspec/changes/archive/2026-10-19-osv-flat-form/specs/osv-parsing/spec.md diff --git a/openspec/changes/osv-flat-form/tasks.md b/openspec/changes/archive/2026-10-19-osv-flat-form/tasks.md similarity index 100% rename from openspec/changes/osv-flat-form/tasks.md rename to openspec/changes/archive/2026-10-19-osv-flat-form/tasks.md diff --git a/openspec/specs/osv-parsing/spec.md b/openspec/specs/osv-parsing/spec.md index 623edc3..2102f69 100644 --- a/openspec/specs/osv-parsing/spec.md +++ b/openspec/specs/osv-parsing/spec.md @@ -60,12 +60,12 @@ SHALL пропускаться с предупреждением файла `SHE - **THEN** у строк контрагентов `extra["КПП"]` содержит значение, суммы не сдвинуты ### Requirement: Шапка документа и схема группировок -Парсер SHALL извлечь из области выше заголовка таблицы: `organization_name` (первая непустая текстовая строка, trim), -`account_code_raw` (после «по счету»/«по счёту», как в файле: «60», «58.03»), `period_text` (после «за» до «г.»/конца), +Парсер 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`. Элемент вида `<группировка>.ИНН` (суффикс после последней точки — по словарю `service_columns.inn`, +`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`, а элемент служит **подсказкой** для строк ИНН (требование «Строки ИНН рядом с контрагентом»): стоит после владельца — ИНН под @@ -98,9 +98,25 @@ SHALL пропускаться с предупреждением файла `SHE - **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[]` документа листа предупреждением @@ -125,6 +141,11 @@ SHALL пропускаться с предупреждением файла `SHE - **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` (регистр @@ -133,8 +154,8 @@ SHALL пропускаться с предупреждением файла `SHE `^\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; строка ИНН без пары — OTHER; -последняя строка уровня 0 с текстом из словаря «Итого» — TOTAL. +(число не-счётных предков после последнего 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` — первым найденным из: @@ -187,6 +208,13 @@ SHALL пропускаться с предупреждением файла `SHE - **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`; «-», «—», «–», пустая ячейка → @@ -203,9 +231,7 @@ turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по `data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто, а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка -«Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT -(первая строка уровня 0); итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если корня нет (пустая ОСВ, -R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в +«Показатели»») в суммирование не входят и сами родителями не проверяются; строка 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, применённые к листу); любое @@ -215,6 +241,12 @@ balance_identity_violations, parent_sum_violations}`. Результат про `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). Строка относится к такому счёту, если @@ -276,6 +308,32 @@ balance_identity_violations, parent_sum_violations}`. Результат про - **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` + ### Requirement: Форматы, лимиты, время Парсер SHALL читать XLSX (openpyxl, полное чтение), XLS (xlrd, `formatting_info`), XLSB (pyxlsb; outline level недоступен — каскад начинается с отступа). Несоответствие `file_type` сигнатуре файла → попытка по сигнатуре и предупреждение файла `FILE_TYPE_MISMATCH` (`severity = info`) в `metrics.warnings`.