diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 48b1309..ceabe58 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -16,8 +16,8 @@ бухгалтером. **Ни одна буква колонки, ни один номер строки, ни одно имя листа не являются контрактом.** Фикстуры в `fixtures/osv/` — это примеры двух организаций, а не спецификация формата. Парсер обязан: -1. **Найти лист(ы) с ОСВ** по содержимому: любая ячейка, матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра, - «ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`. +1. **Найти лист(ы) с ОСВ** по содержимому: ячейка в первых 30 строках листа (`SHEET_MARKER_ROWS`: шапка ведомости всегда + над таблицей), матчащая `оборотно[- ]сальдовая ведомость` (без учёта регистра, «ё/е», переносов строк). Один файл может содержать несколько листов с разными ОСВ → несколько `document`. Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать. 2. **Найти заголовок таблицы** по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…», «Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит», @@ -29,7 +29,8 @@ «начала» (только обороты и конец), с одной свёрнутой колонкой сальдо (знак = сторона), с дополнительными колонками («Количество», «Валюта», «ИНН», «КПП», «Код»). Неизвестные колонки — сохранять в `extra` строки, не терять. 4. **Определить колонку текста строки** — первая непустая слева от сумм (обычно A, но не обязательно); ИНН — колонка с - заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — 10/12 цифр. + заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — ровно 10/12 цифр (число из 9/11 цифр при этом выборе + нулём не дополняется, иначе числовой КПП сошёл бы за ИНН — R-24). 5. **Определить иерархию строк** по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) — по отступу (ведущие пробелы / `alignment.indent`), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings` как @@ -97,7 +98,8 @@ r11 Договоры ← Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5), `row_value` (текст первой колонки), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат -только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; ИНН внутри текста строки контрагента +только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; число из 9/11 цифр — ИНН региона 01–09 +с потерянным ведущим нулём — при извлечении из выбранной колонки дополняется нулём слева, R-24; ИНН внутри текста строки контрагента («Полиграф, 3528315710») парсер не извлекает: `counterparty_name` = весь текст строки, **[открыто]** OQ-100), `data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки). @@ -107,7 +109,8 @@ r11 Договоры ← ветки; их суммы остаются в шести полях, но в проверки родителя не входят (§1.4). Классификация строки (п. 6–10 «Парсинг ОСВ»): -1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (регистр учитывается; +1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (флаги `re.ASCII | re.DOTALL`: + цифры только 0–9, название после запятой может быть многострочным; регистр учитывается; сегмент субсчёта — 1–2 цифры **или 1–3 заглавные буквы**: буквенные субсчета 1С «76.АВ», «76.ВА», «76.ОТ» встречаются в 8 реальных файлах; после запятой — опциональное название, пробел необязателен: «60.01, Расчеты с поставщиками») → ACCOUNT, `account_code_raw` — код как в файле без названия, `account_code` (нормализованный) строит ядро, все @@ -134,9 +137,20 @@ r11 Договоры ← (начало, конец) — по колонкам либо, **если у родителя сальдо свёрнуто** (не больше одной ненулевой стороны), по нетто `дебет − кредит` (R-21): 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета и контрагентов — развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт). Без этого правила 32 из 66 реальных файлов не сходились бы - по шести колонкам только из-за способа показа. Остаточный риск: перестановка сторон при совпавшем нетто родителя - этой проверкой не ловится — её ловят тождество строки и «Итого = корень». **[открыто]** OQ-105: подтвердить, что - нетто-свёртка родителя — штатное поведение 1С для всех версий выгрузки. + по шести колонкам только из-за способа показа. **[открыто]** OQ-105: подтвердить, что нетто-свёртка родителя — + штатное поведение 1С для всех версий выгрузки. Остаточные риски (проверками §1.4 не ловятся): + - RR-1 — у свёрнутого родителя сверяется только нетто: распределение сальдо детей между Дт и Кт (перестановка сторон + при совпавшем нетто) не проверяется; + - RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя с потерянной в файле стороной + неотличимо от нетто-показа 1С и проходит при совпавшем нетто; + - RR-3 — нетто допускается на любом уровне дерева (не только «счёт → субсчета»): ошибка распределения сальдо между + договорами под свёрнутым контрагентом не ловится; + - RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто + строки, ловится лишь сверкой с развёрнутым родителем по колонкам; + - слепая зона: обе стороны сальдо развёрнутого ребёнка увеличены на одно X, а все предки до корня свёрнуты — не + ловится ничем; + - родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом; + - вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16). - Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (**пустая ОСВ**: заголовок и сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не @@ -180,10 +194,10 @@ result-osv-needs-check.json`. | `needs_check` | нарушена любая проверка §1.4 хотя бы на одном листе (или «Итого» не найдена); причины — `metrics.warnings`, номера строк — `checks` | | `error` / `INVALID_DOCUMENT` | в книге нет ни одного листа ОСВ; в сообщении — число просмотренных листов | | `error` / `INVALID_FORMAT` | лист ОСВ есть, но нет заголовка таблицы, обязательной группы сумм (§1.0 п. 2) или иерархию определить не удалось; в сообщении — лист и шаг | -| `error` / `UNREADABLE` | книга не открывается | -| `error` / `UNSUPPORTED_FORMAT` | сигнатура файла не XLS/XLSX/XLSB либо размер больше `PARSE_MAX_BYTES` | +| `error` / `UNREADABLE` | книга не открывается (лог WARNING) | +| `error` / `UNSUPPORTED_FORMAT` | сигнатура файла не XLS/XLSX/XLSB, размер больше `PARSE_MAX_BYTES` либо превышен лимит чтения (распакованный zip, площадь объединённых ячеек) | | `error` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` | -| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение` | +| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение`, стек — в лог ERROR | Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по `file_type` запроса; несовпадение — предупреждение. Листы форм ББ/ОФР (§12 п. 1) и листы без признаков ОСВ пропускаются с @@ -193,8 +207,10 @@ result-osv-needs-check.json`. «xlsb: outline level недоступен»). Лимиты: `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с; кооперативный — проверка между листами, -стратегиями и блоками строк, зависание внутри библиотеки чтения не прерывается). Любой реальный файл разбирается быстрее -5 с (максимум ≈ 0,6 с). +в циклах по листу и между стратегиями; чтение книги библиотекой он не прерывает). Поэтому до библиотеки чтения действуют +лимиты чтения (R-23) → `UNSUPPORTED_FORMAT`: распакованный zip (xlsx/xlsb) ≤ 10 × `PARSE_MAX_BYTES`, суммарная площадь +объединённых ячеек книги ≤ 200 000 (реальный максимум 10 090). Защиты от XML-бомб (`defusedxml`) нет — **[открыто]** +OQ-108. Любой реальный файл разбирается быстрее 5 с (максимум ≈ 0,6 с). Словари: `osv_headers.yaml` (признаки листов, группы сумм, «Дебет»/«Кредит», колонка ИНН, «Итого», метки шапки), `osv_groups.yaml` (группировка → `group_type`), `periods.yaml` (период → квартал и нарастающий итог), `data_types.yaml` diff --git a/context/06-test-data.md b/context/06-test-data.md index 04c9295..1e75c8c 100644 --- a/context/06-test-data.md +++ b/context/06-test-data.md @@ -110,7 +110,7 @@ 16. **Сальдо родителя нетто, детей развёрнуто**: 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета — развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт); без учёта этого 32 из 66 файлов не сходились бы по колонкам (§1.4 правил, R-21). -17. **ИНН внутри текста строки**: у Счастливочной в ОСВ 76 за 2024 г. встречаются контрагенты вида «Полиграф, 3528315710» +17. **ИНН внутри текста строки**: у Счастливочной в ОСВ 76 за 1–3 кв. 2024 встречаются контрагенты вида «Полиграф, 3528315710» (20 строк) без колонки ИНН; парсер ИНН из текста не извлекает (OQ-100). ## 4. ББ / ОФР (PDF) и ответы ИС Сканирование diff --git a/context/07-open-questions.md b/context/07-open-questions.md index dfabec1..41b0927 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -110,7 +110,11 @@ | OQ-102 | Нужен **реальный xlsb из Excel**: единственный `fixtures/osv/synthetic/s13_no_outline_indent.xlsb` собран тестовым писателем BIFF12 и не доказывает чтение файлов, сохранённых Excel (стили, другая раскладка записей). Получить xlsb у аналитиков или пересохранить `s02` в Excel и заменить `s13` по инструкции в README fixtures | tabular-parser: R-9, O-7 | срез 2a (достоверность xlsb) | допущение принято: читаем `pyxlsb` без outline level и форматирования; проверка — на первом настоящем xlsb | | OQ-103 | Счета с буквой в **первом** сегменте кода (забалансовые «МЦ.04», «ГТД», «НЕ.01.1») не распознаются как ACCOUNT: регулярка требует цифр в первом сегменте, строка станет контрагентом/группировкой по позиции. Нужны ли такие ОСВ, как их кодировать в `chart_of_account` | tabular-parser: R-16 (design №26), `classify.py` | срез 2c (план счетов), парсер | допущение принято: не распознаются; в 66 реальных файлах не встречаются | | OQ-104 | Группировка «Валюта» в строке детализации (шапка варианта 2 «Детализация по субсчетам: …, Валюта») совпадает с меткой валюты шапки и порождает ложное предупреждение «валюта «» не распознана» (`currency = null` в обоих случаях). Править парсер (различать область шапки и строку детализации)? | tabular-parser: O-8, T12.1 (синтетика s16) | срез 2a+ (`header.py`) | допущение принято: дефект малозначим, предупреждение зафиксировано в golden-снимке s16 | -| OQ-105 | Сверка «сумма детей = родитель» по нетто (R-21): у родителя сальдо свёрнуто (счёт — одна сторона), у детей — развёрнуто. Подтвердить аналитиками, что нетто-свёртка родителя — штатное поведение 1С для **всех** версий выгрузки, а не артефакт двух организаций; остаточный риск — перестановка сторон при совпавшем нетто не ловится этой проверкой (ловят тождество строки и «Итого = корень») | tabular-parser: R-21 (design №31), `05-domain-rules.md` §1.4 | срез 2a (контрольные суммы), срез 2c | допущение принято: нетто допустимо, если у родителя не более одной ненулевой стороны сальдо; 32 из 66 реальных файлов иначе не сходились | +| OQ-105 | Сверка «сумма детей = родитель» по нетто (R-21): у родителя сальдо свёрнуто (счёт — одна сторона), у детей — развёрнуто. Подтвердить аналитиками, что нетто-свёртка родителя — штатное поведение 1С для **всех** версий выгрузки, а не артефакт двух организаций; остаточные риски RR-1…RR-4 и слепые зоны (перестановка сторон и одинаковое увеличение обеих сторон сальдо ребёнка под свёрнутыми предками не ловятся ничем) перечислены в §1.4 | tabular-parser: R-21 (design №31), `05-domain-rules.md` §1.4 | срез 2a (контрольные суммы), срез 2c | допущение принято: нетто допустимо, если у родителя не более одной ненулевой стороны сальдо; 32 из 66 реальных файлов иначе не сходились | +| OQ-106 | «Итого = корень» сверяется с **первой** корневой строкой ACCOUNT (уровень 0). Лист с несколькими корнями уровня 0 (несколько счетов в одной ОСВ; ОСВ по субконто без строк-счетов) получает `needs_check`, хотя «Итого» может быть равна Σ корней. Сверять «Итого» с суммой корней? | tabular-parser: финальное ревью Среза 2a, M-4 | срез 2a+ (контрольные суммы `checks.py`), чек-лист документов | допущение принято: сверка с первым корнем; многокорневой лист → `needs_check` (аналитик проверяет вручную); в фикстурах таких листов нет | +| OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) | +| OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23 | tabular-parser (readers), платформа (зависимости образа) | допущение принято: действуют лимиты R-23 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000; XML-бомбы не отсекаются (README «Известные ограничения») | +| OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | открыт: правка воркера — follow-up; лимит размера сейчас срабатывает после скачивания (память под файл выделяется) | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 0ec604c..4209962 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -102,7 +102,7 @@ Change'и: `tabular-parser/openspec/changes/slice-2a-osv-parser` (issue arm-tabu - **Допущения парсера, решаются в 2c:** `is_cumulative` только по тексту периода (OQ-02), `data_type = NU` как есть (OQ-45), «Итого» выдаётся строкой `TOTAL` (OQ-01). - **Документы:** `05-domain-rules.md` §1 приведён к факту (§1.6 — результат парсера), `06-test-data.md` §7 (синтетика, - golden, рукописный xlsb), OQ-98…OQ-105 открыты, OQ-81 закрыт для `tabular-parser`. + golden, рукописный xlsb), OQ-98…OQ-105 открыты (финальное ревью добавило OQ-106…OQ-109), OQ-81 частично закрыт (в `tabular-parser` `ARM_KA_FIXTURES` поддержан; `scan-gateway` — follow-up). - **Не сделано / риски:** xlsb проверен только рукописным синтетическим файлом (OQ-102); ИНН из текста строки не извлекается (OQ-100); счета с буквой в первом сегменте не распознаются (OQ-103). diff --git a/contracts/examples/result-osv-needs-check.json b/contracts/examples/result-osv-needs-check.json index 1d029c7..81d64a8 100644 --- a/contracts/examples/result-osv-needs-check.json +++ b/contracts/examples/result-osv-needs-check.json @@ -2,16 +2,22 @@ "schema_version": "1.1", "job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01", "file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02", - "service": { "name": "tabular-parser", "version": "0.1.0" }, + "service": { "name": "tabular-parser", "version": "1.0.0" }, "status": "needs_check", - "metrics": { "rows": 3, "warnings": ["hierarchy=outline", "balance identity violated at rows [3]"] }, + "metrics": { + "rows": 3, + "warnings": [ + "sheet=Лист_1 hierarchy=outline", + "§1.4: сальдо на начало + обороты ≠ сальдо на конец (±0.01): строки 3" + ] + }, "documents": [ { "document_type": "OSV", "locator": "sheet:Лист_1", "header": { "organization_name": "ООО Ромашка", - "organization_inn": "7700000000", + "organization_inn": null, "account_code_raw": "60", "period_text": "1 квартал 2025 г.", "period_year": 2025, diff --git a/openspec/changes/slice-2a-osv-parser/proposal.md b/openspec/changes/slice-2a-osv-parser/proposal.md index 3f1470e..b5bd58f 100644 --- a/openspec/changes/slice-2a-osv-parser/proposal.md +++ b/openspec/changes/slice-2a-osv-parser/proposal.md @@ -25,7 +25,7 @@ - **`09-delivery-plan.md`**: Срез 2 разбит на 2a (парсер ОСВ), 2b (ББ/ОФР из Excel), 2c (загрузка и маппинг в ядре), 2d (UI «Первичные документы»); `08-source-docs-index.md`/README без изменений. - **`07-open-questions.md`**: новые OQ (коды валют для строк CURRENCY; поведение при нескольких ОСВ одного счёта в одной - книге), статус OQ-81 (закрыт хелпером), OQ-45/OQ-01/OQ-02 — пометка «допущение парсера, решение в 2c». + книге), статус OQ-81 (частично закрыт: в tabular-parser `ARM_KA_FIXTURES` поддержан хелпером, scan-gateway — follow-up), OQ-45/OQ-01/OQ-02 — пометка «допущение парсера, решение в 2c». ## Capabilities diff --git a/openspec/changes/slice-2a-osv-parser/tasks.md b/openspec/changes/slice-2a-osv-parser/tasks.md index 32f4c91..0173564 100644 --- a/openspec/changes/slice-2a-osv-parser/tasks.md +++ b/openspec/changes/slice-2a-osv-parser/tasks.md @@ -4,7 +4,7 @@ `result-osv-minimal.json` (+`extra`, `schema_version 1.1`) и новый `result-osv-needs-check.json`; `openspec validate --strict`; проверка схемой через `jsonschema` (скрипт в `dev/checks.md`, если есть, иначе вручную в отчёте). - [x] 1.2 `09-delivery-plan.md`: Срез 2 → подсрезы 2a–2d с составом каждого; `07-open-questions.md`: новые OQ (коды валют - CURRENCY-строк; несколько ОСВ одного счёта в книге — OQ-98…OQ-105), OQ-81 закрыт для tabular-parser, пометки «допущение парсера» у OQ-01/02/45. + CURRENCY-строк; несколько ОСВ одного счёта в книге — OQ-98…OQ-105), OQ-81 частично закрыт (в tabular-parser `ARM_KA_FIXTURES` поддержан; scan-gateway — follow-up), пометки «допущение парсера» у OQ-01/02/45. - [x] 2.1 После реализации парсера (дочерний change, задачи 3.1–4.3): §1 `05-domain-rules.md` — статусы/коды, `hierarchy=<имя>` в warnings, TOTAL в rows, лимиты, имена словарей; §1.5 — `ASSUMPTION(OQ-02)`; `06-test-data.md` — синтетика, golden, xlsb; сверка: сценарии спеки `osv-parsing` ↔ пункты §1 (таблица в отчёте).