diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 9bfb98b..48b1309 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -7,7 +7,8 @@ ## §1. Парсинг ОСВ (tabular-parser + маппинг в ядре) -Источник: «Парсинг ОСВ» (DOC-260010), FNR01 (DOC-037740), реальные файлы `fixtures/osv/`. +Источник: «Парсинг ОСВ» (DOC-260010), FNR01 (DOC-037740), реальные файлы `fixtures/osv/`. Реализация — `tabular-parser`, +Срез 2a (change `slice-2a-osv-parser`); что именно сделано и как выглядит результат — §1.6. ### 1.0 Главный принцип: структура определяется динамически, не по координатам @@ -20,7 +21,8 @@ Листы без признаков ОСВ/ББ/ОФР — пропускать с warning, не падать. 2. **Найти заголовок таблицы** по содержимому: строка(и), где встречаются одновременно «Сальдо на начало…», «Оборот(ы) за период», «Сальдо на конец…» (с вариантами: «Нач. сальдо», «Обороты», «Кон. сальдо», «Дебет/Кредит», - «Дт/Кт», «Д-т/К-т»). Заголовок может занимать 1–4 строки с merged-ячейками; группировки могут быть как под «Счет» + «Дт/Кт», «Д-т/К-т»). **Обязательны группы «обороты» и «сальдо на конец»; отсутствовать может только «сальдо на + начало»** (иначе заголовок не найден → `INVALID_FORMAT`, §1.6). Заголовок может занимать 1–4 строки с merged-ячейками; группировки могут быть как под «Счет» в первой колонке, так и строкой «Субконто», так и перечислением через запятую (вариант 2). 3. **Определить колонки сумм по тексту**, а не по позиции: для каждой из трёх групп («начало», «оборот», «конец») найти подзаголовки «Дебет»/«Кредит» в следующей строке под ней (с учётом merged-диапазонов). Допустимы ОСВ без @@ -30,13 +32,15 @@ заголовком `ИНН` **или** колонка, где ≥ 80 % непустых значений — 10/12 цифр. 5. **Определить иерархию строк** по outline level Excel; если уровней нет (файл пересохранён, csv, другая система) — по отступу (ведущие пробелы / `alignment.indent`), по жирности счёта, по шаблону кода счёта и, в крайнем случае, по - факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings`. + факту «сумма детей = родитель». Стратегии применяются каскадом, выбранная стратегия пишется в `metrics.warnings` как + `sheet=<имя листа> hierarchy=`. Строка «Итого» в каскаде не участвует + (всегда уровень 0); при меньше чем двух строках данных (пустая ОСВ) — `trivial`. 6. **Шапку документа** (организация, счёт, период, «выводимые данные») искать регулярками по всем ячейкам выше заголовка таблицы; отсутствие любого поля — не ошибка, а `null` + warning (ядро попросит аналитика уточнить при проверке). 7. **Самопроверка результата** (контрольные суммы §1.4) — единственный способ понять, что структура угадана верно. Если тождества не сходятся — не «подгонять», а вернуть `needs_check` с диагностикой, какой шаг не уверен. 8. Правила парсера — **данные, а не код**: словари синонимов заголовков, группировок, периодов лежат в - `tabular-parser/rules/*.yaml` и расширяются без релиза. Новый формат ОСВ = новая фикстура + при необходимости новый + `tabular-parser/api/rules/*.yaml` (`osv_headers`, `osv_groups`, `periods`, `data_types`) и расширяются без релиза. Новый формат ОСВ = новая фикстура + при необходимости новый синоним, а не новая ветка `if`. Тесты парсера строятся по принципу «свойств»: для любого входного файла результат удовлетворяет инвариантам §1.4, @@ -84,24 +88,38 @@ r11 Договоры ← ### 1.2 Колонки сумм Заголовки сумм объединены (merged) и позиции колонок плавают между файлами даже одной организации. Определять по тексту -(§1.0 п. 3). Всё правее последней распознанной колонки сумм — не часть таблицы (в фикстурах там встречаются посторонние -числа) — складывать в `extra`, в суммы не брать. Числа могут быть numeric **или** текстом («1 234 567,89», «(1 234)», +(§1.0 п. 3). Всё правее последней колонки «Кредит» — не часть таблицы (в фикстурах там встречаются посторонние +числа): игнорируется, в суммы и в `extra` не попадает. В `rows[].extra` (контракт v1.1) идут только колонки **внутри** +таблицы, не сопоставленные с шестью суммами и ИНН (ключ — заголовок колонки: «КПП», «Количество»…). Числа могут быть numeric **или** текстом («1 234 567,89», «(1 234)», «-», «—», неразрывные пробелы) — нормализовать в Decimal; пустая ячейка/прочерк = NULL (не 0). Отрицательные возможны. ### 1.3 Строки -Для каждой строки табличной части: `row_number` (исходный номер), `level` (= outline level Excel; в xls — `rowinfo_map`), -`row_value` (текст первой колонки), шесть сумм, ИНН (если колонка есть; оставить только цифры, иначе NULL), +Для каждой строки табличной части: `row_number` (исходный номер), `level` (outline level Excel, в xls — `rowinfo_map`; без группировок — каскад §1.0 п. 5), +`row_value` (текст первой колонки), шесть сумм, ИНН (**только из колонки ИНН**; в ячейке оставляются цифры, результат +только при 10 или 12 цифрах — «ИНН 352819549855» → `352819549855`, иначе NULL; ИНН внутри текста строки контрагента +(«Полиграф, 3528315710») парсер не извлекает: `counterparty_name` = весь текст строки, **[открыто]** OQ-100), `data_type` (строки «USD», «Вал.», «Кол.» под контрагентом → CURRENCY/QUANTITY; по умолчанию — из шапки). +Строки «Вал.», «Кол.» и трёхбуквенный код валюты («USD») — не уровень группировки, а вид сумм родителя: +`group_type = OTHER`, `data_type = CURRENCY | QUANTITY`, позицию в `group_schema` не занимают, **не наследуют** +`counterparty_name`/`counterparty_inn` (контрагента потребитель находит по родителю), `account_code_raw` наследуют от +ветки; их суммы остаются в шести полях, но в проверки родителя не входят (§1.4). + Классификация строки (п. 6–10 «Парсинг ОСВ»): -1. Строка — счёт, если текст матчится `^\d{2,3}(\.\d{1,2}){0,2}(,\s.*)?$` (с опциональным названием после запятой: - «60.01, Расчеты с поставщиками») → ACCOUNT, `account_code` (нормализованный), все дочерние строки наследуют счёт. +1. Строка — счёт, если текст матчится `^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` (регистр учитывается; + сегмент субсчёта — 1–2 цифры **или 1–3 заглавные буквы**: буквенные субсчета 1С «76.АВ», «76.ВА», «76.ОТ» встречаются в + 8 реальных файлах; после запятой — опциональное название, пробел необязателен: «60.01, Расчеты с поставщиками») + → ACCOUNT, `account_code_raw` — код как в файле без названия, `account_code` (нормализованный) строит ядро, все + дочерние строки наследуют счёт. Остаточный риск: имя контрагента вида «12.ООО» будет принято за счёт. Счета с буквой в + **первом** сегменте (забалансовые «МЦ.04», «ГТД», «НЕ.01.1») не распознаются как ACCOUNT — **[открыто]** OQ-103. 2. Не-счёт: позиция в ветке = количество не-счётных предков + 1 → соответствующая группировка из `group_schema` (после последнего ACCOUNT). Увеличение уровня — ребёнок, равный — сосед, уменьшение — конец группы. 3. После COUNTERPARTY дочерние продолжают классифицироваться по схеме (CONTRACT, затем OTHER). 4. Строка «Итого» (level 0, последняя) — контрольная сумма всего документа; не сохранять как `osv_entry` - **[открыто №2]**, но использовать для сверки. + **[открыто №2]**, но использовать для сверки. Парсер отдаёт её в `rows` как `group_type = TOTAL` (level 0, без + родителя; `ASSUMPTION(OQ-01)`: хранить ли «Итого», решает ядро — при маппинге строку TOTAL отбросить); в + `metrics.rows` она не считается. 5. Все остальные строки сохраняются (п. 11 «Парсинг ОСВ») с `parent_entry_id` **[открыто №2]**. Нормализация кода счёта: `60.1 → 60.01`, `60.01.1 → 60.01.1`, `76.7.1 → 76.07.1`; `account_root` = первые две-три цифры @@ -109,11 +127,23 @@ r11 Договоры ← ### 1.4 Контрольные суммы парсера +Допуск ±0.01, пустое значение = 0. - По каждой строке: `opening_debit − opening_credit + turnover_debit − turnover_credit == closing_debit − closing_credit` (±0.01). -- По каждому родителю: суммы родителя = Σ детей того же `data_type` (по каждой из шести колонок). Строки CURRENCY/QUANTITY - в суммирование не входят. -- Строка счёта верхнего уровня == «Итого». -Нарушение → `recognition_job.status = needs_check`, `document.status = NEEDS_CHECK`, причина — в `metrics.warnings`. +- По каждому родителю: суммы родителя = Σ детей того же `data_type`. Строки CURRENCY/QUANTITY в суммирование не входят + и сами родителями не проверяются. **Обороты** сверяются по колонкам (дебет с дебетом, кредит с кредитом). **Сальдо** + (начало, конец) — по колонкам либо, **если у родителя сальдо свёрнуто** (не больше одной ненулевой стороны), по нетто + `дебет − кредит` (R-21): 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета и контрагентов — + развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт). Без этого правила 32 из 66 реальных файлов не сходились бы + по шести колонкам только из-за способа показа. Остаточный риск: перестановка сторон при совпавшем нетто родителя + этой проверкой не ловится — её ловят тождество строки и «Итого = корень». **[открыто]** OQ-105: подтвердить, что + нетто-свёртка родителя — штатное поведение 1С для всех версий выгрузки. +- Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0). Если корня нет (**пустая ОСВ**: заголовок и + сразу «Итого» — в фикстурах две, schastl 58 и 66 за 2 кв. 2025) — «Итого» должна быть пустой или нулевой; такая ОСВ + штатна: `done`, `hierarchy=trivial`, `metrics.rows = 0`, предупреждение «нет строк данных». Если строка «Итого» не + найдена вовсе — сверка невозможна: `total_row_matches_root = false` и предупреждение «строка «Итого» не найдена» + (результат `needs_check`, R-20). +Нарушение → `recognition_job.status = needs_check`, `document.status = NEEDS_CHECK`, причина — в `metrics.warnings`, +номера строк — в `checks` результата. Парсер структуру под сходимость не подгоняет. ### 1.5 Маппинг периода (FNR01) → `period_quarter`, `is_cumulative` @@ -127,12 +157,50 @@ r11 Договоры ← | 4 квартал, IV кв. | Q4 | **false** [открыто] | | год, за 2025 год, 12 месяцев, годовая | Q4 | true | +`ASSUMPTION(OQ-02)`: парсер определяет `period_quarter` и `is_cumulative` **только по тексту периода** +(`rules/periods.yaml`, правила идут от специфичных к общим): квартальная ведомость Q2–Q4 считается не нарастающей; +сверку по данным (`Q3.opening == Q1.opening`) выполняет ядро и может переопределить признак. `period_year` — четыре +цифры года 19xx/20xx из текста, не являющиеся частью длинного числа. + Практика на фикстурах: ОСВ «за 3 квартал 2024» у обеих компаний — **квартальные** (opening ≠ сальдо на 01.01), а ОСВ «за 2024 г.» — годовые. Признак можно проверить по данным: если есть ОСВ Q1 того же года и `Q3.opening == Q1.opening` → нарастающая. Сверка периода документа с периодом версии → `document.period_check` (MATCH / MISMATCH / UNKNOWN); загрузка не блокируется. +### 1.6 Результат парсера (Срез 2a): статусы, коды, лимиты, словари + +Результат — файл по контракту v1.1: по одному `osvDocument` на лист ОСВ (`locator = sheet:<имя листа>`; имя листа +подставляется в `locator` и сообщения, но поиск по нему не ведётся). Форма и пример — `contracts/examples/ +result-osv-needs-check.json`. + +| Исход | Условие | +|---|---| +| `done` | контрольные суммы §1.4 сошлись на всех листах | +| `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` / `TIMEOUT` | превышен `PARSE_TIMEOUT_SECONDS` | +| `error` / `RECOGNITION_FAILED` | непредвиденное исключение; одна строка `Тип: сообщение` | + +Формат определяется по сигнатуре байтов (zip → XLSX, либо XLSB при наличии `xl/workbook.bin`; OLE2 → XLS), а не по +`file_type` запроса; несовпадение — предупреждение. Листы форм ББ/ОФР (§12 п. 1) и листы без признаков ОСВ пропускаются с +предупреждением (`sheet=<имя> skipped: statement (slice 2b)` / `skipped: нет признаков ОСВ`). Другие предупреждения: +`sheet=<имя> hierarchy=<стратегия>` (всегда), свёрнутое сальдо, нераспознанное число (с номером строки), не найденное +поле шапки, «нет строк данных». xlsb не содержит outline level — каскад начинается с отступа (предупреждение +«xlsb: outline level недоступен»). + +Лимиты: `PARSE_MAX_BYTES` (по умолчанию 50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с; кооперативный — проверка между листами, +стратегиями и блоками строк, зависание внутри библиотеки чтения не прерывается). Любой реальный файл разбирается быстрее +5 с (максимум ≈ 0,6 с). + +Словари: `osv_headers.yaml` (признаки листов, группы сумм, «Дебет»/«Кредит», колонка ИНН, «Итого», метки шапки), +`osv_groups.yaml` (группировка → `group_type`), `periods.yaml` (период → квартал и нарастающий итог), `data_types.yaml` +(БУ/НУ/сумма, строки «Вал.»/«Кол.», коды и написания валют). Каждая запись словаря обязана срабатывать хотя бы на одной +фикстуре или синтетике (тест покрытия). + --- ## §2. Приведение периодов (DOC-259006) diff --git a/context/06-test-data.md b/context/06-test-data.md index 29dac97..04c9295 100644 --- a/context/06-test-data.md +++ b/context/06-test-data.md @@ -93,14 +93,25 @@ расхождение 0,5 млн: исправительные проводки задним числом, тоже реальный кейс). «За 2024 г.» — годовые. Проверять по данным, не только по тексту. 9. **Валюта**: `Валюта БУ, НУ: RUB` или `руб.` — равнозначно. Строк Вал./Кол. в этом наборе нет, но ТЗ их описывает - (ОСВ 60.32 в у. е.) — нужен синтетический тест. + (ОСВ 60.32 в у. е.) — закрыто синтетикой `s04` (§7). 10. **Названия субсчетов** в строках счёта могут присутствовать («60.01, Расчеты с поставщиками») или нет — в этом - наборе нет; поддержать оба. + наборе нет; поддержаны оба, синтетика `s09` (§7). 11. **Контрагенты-физлица** и ИП («Абрамова Татьяна Викторовна (Перевозчик)», «Аверина Е.И. ИП», «Ведомость в счет з/платы») — не ЮЛ; резолв даёт NULL, тип «Внешний». Суффиксы в скобках — часть имени. 12. **xls (BIFF8)** читается `xlrd` (`formatting_info=True` для outline level через `rowinfo_map`); **xlsx** — `openpyxl` - (не `read_only`, иначе нет `row_dimensions[].outline_level`). XLSB — `pyxlsb` (в наборе нет; ТЗ требует). + (не `read_only`, иначе нет `row_dimensions[].outline_level`). XLSB — `pyxlsb` (в реальном наборе нет; ТЗ требует; + есть только рукописный синтетический `s13`, см. §7). 13. Размеры: до ~1000 строк, до 330 КБ; 2280 merged-диапазонов в файле на 677 строк — не падать на merged. +14. **Две пустые ОСВ**: `schastl/2025-Q2/…по счету 58 за 2 квартал 2025 г..xlsx` и `…по счету 66 за 2 квартал 2025 г..xlsx` — + заголовок и сразу «Итого», строк данных нет. Штатный случай: `done`, `hierarchy=trivial`, `metrics.rows = 0`. +15. **Буквенные субсчета 1С** «76.АВ», «76.ВА», «76.ОТ» — в 8 файлах ОСВ 76 (АО РБ: 2024 г., 3 кв. 2024, 1–3 кв. 2025; + Счастливочная: 1–3 кв. 2024). Это счета (ACCOUNT), а не контрагенты; регулярка кода счёта допускает 1–3 заглавные буквы + в сегменте субсчёта (§1.3 п. 1 правил). +16. **Сальдо родителя нетто, детей развёрнуто**: 1С показывает счёт свёрнутым сальдо (62 = 16,1 млн Дт), а субсчета — + развёрнутым (62.01 = 40,2 млн Дт, 62.02 = 24,1 млн Кт); без учёта этого 32 из 66 файлов не сходились бы по + колонкам (§1.4 правил, R-21). +17. **ИНН внутри текста строки**: у Счастливочной в ОСВ 76 за 2024 г. встречаются контрагенты вида «Полиграф, 3528315710» + (20 строк) без колонки ИНН; парсер ИНН из текста не извлекает (OQ-100). ## 4. ББ / ОФР (PDF) и ответы ИС Сканирование @@ -140,7 +151,7 @@ fixtures/ osv/ rb/ 2024-Q1/ 2024-Q2/ 2024-Q3/ 2024-FY/ 2025-Q1/ 2025-Q2/ 2025-Q3/ # файлы с исходными именами schastl/ ... - synthetic/ header-variant-2.xlsx, currency-rows.xlsx, xlsb-sample.xlsb, broken-header.xlsx + synthetic/ README.md + s01…s16 (генератор в tabular-parser, §7) statements/ rb/ schastl/ gir-bo/ # pdf statements-xlsx/ # ББ/ОФР в Excel (§12): лист «Отчетность» из Анализ*.xlsx, @@ -154,7 +165,7 @@ fixtures/ # буквально) golden/ Консолидация_Группа РБ.xlsx # эталон КФО по датам - expected_osv_parse/*.json # ожидаемый результат парсера (зафиксировать после ревью) + expected_osv_parse/<зеркало osv/ без расширения>.json # golden-снимки парсера ОСВ: 82 файла (66 реальных + 16 синтетических) directories/ ecp-export-russkiy-biskvit.xlsx # срез выгрузки ЕЦП CRM по ГК «Русский Бисквит» (Срез 1) ``` @@ -186,5 +197,49 @@ fixtures/ примеры РФ). Загрузчик — `python -m scripts.seed [--dev]` из `arm-core/api/` (идемпотентен, upsert по натуральному ключу; в compose его выполняет одноразовый сервис `arm-core-migrate`). Здесь остаются только тестовые данные группы. Полный план счетов 1С из бандла макета не передан — seed строится на Плане счетов 94н и типовых субсчетах 1С (OQ-47). -Первый прогон парсера на `osv/**` сохранить как `expected_osv_parse/*.json` после ручной сверки итогов со строкой «Итого» -— дальше это regression-набор. +Первый прогон парсера на `osv/**` сохранён как `expected_osv_parse/*.json` после ручной сверки итогов со строкой «Итого» +(§7) — дальше это regression-набор. + +## 7. Синтетика, golden-снимки и xlsb парсера ОСВ (Срез 2a) + +Реальные файлы покрывают один вариант шапки и один формат файла (xls/xlsx из 1С 8.3). Остальное — синтетика +`fixtures/osv/synthetic/` (вымышленные организации и ИНН; суммы согласованы арифметически, поэтому все файлы, кроме +`s10`, дают `done`). Файлы собирает детерминированный генератор `tabular-parser/api/tests/synthetic/generate.py` +(повторный запуск даёт те же байты), ожидания по каждому файлу — в `tests/synthetic/test_synthetic_files.py`, описание — +в `fixtures/osv/synthetic/README.md`. + +| Файл | Назначение (что из §1 проверяет) | +|---|---| +| `s01_variant2_header` | шапка варианта 2 («Период:», «Детализация по субсчетам, субконто: …», заголовок «Субконто», данные «Сумма») | +| `s02_no_outline_indent` | иерархия без outline level — по отступам (`hierarchy=indent`); `Валюта БУ, НУ: руб.` | +| `s03_no_outline_no_indent` | ни outline, ни отступов — по кодам счетов и жирности (`hierarchy=account_pattern`); римский квартал | +| `s04_currency_quantity_rows` | строки «USD», «Вал.», «Кол.» под контрагентом (`CURRENCY`/`QUANTITY`) | +| `s05_text_numbers` | суммы текстом: «1 234 567,89», «(1 234,00)», «—», неразрывные и узкие пробелы | +| `s06_two_osv_and_other_sheet` | два листа ОСВ и посторонний лист «Справка» | +| `s07_no_opening_collapsed_closing` | нет группы «Сальдо на начало»; свёрнутое сальдо на конец одной колонкой (в т. ч. R-21) | +| `s08_extra_kpp_column` | колонка «КПП» между «ИНН» и суммами → `extra` | +| `s09_subaccount_names` | названия субсчетов в строке счёта, буквенный субсчёт | +| `s10_broken_sum` | испорченная сумма → `needs_check` с номерами строк | +| `s11_header_repeat` | заголовок таблицы повторяется внутри данных (печать по страницам) | +| `s12_statement_and_osv` | лист ББ и лист ОСВ в одной книге: форма пропускается с предупреждением | +| `s13_no_outline_indent` (`.xlsb`) | та же ведомость, что `s02`, в xlsb | +| `s14_statement_forms` | три формы ББ/ОФР, каждая опознаётся своим признаком | +| `s15_group_title_synonyms` | названия групп сумм «Входящее/Начальное/Конечное/Исходящее сальдо» | +| `s16_header_synonyms` | шапка без организации, период «2024», группировки `OTHER`, «Детализация: …» | + +`s14`–`s16` добавлены тестом покрытия словарей: каждая запись `rules/*.yaml` обязана сработать на реальных или +синтетических файлах. + +**xlsb рукописный.** `s13` собран **не Excel и не LibreOffice** (их нет на машине разработки), а минимальным писателем +BIFF12 `tabular-parser/api/tests/synthetic/xlsb_writer.py`: он пишет только то, что читает `pyxlsb`. Файл читается +парсером, но совместимость с xlsb, сохранённым Excel (стили, другая раскладка записей), **не проверена**; нужен реальный +файл из Excel (OQ-102). Инструкция замены — в README `fixtures/osv/synthetic/`. + +**Golden-снимки.** Для каждого файла `fixtures/osv/**` (включая синтетику) в `fixtures/golden/expected_osv_parse/` лежит +результат разбора (82 файла = 66 реальных + 16 синтетических): файл результата контракта v1.1 без `job_id`, `file_id`, +`service.version`, `metrics.duration_ms`. Снимок — **регрессия, не оракул**: он фиксирует, что парсер выдаёт сейчас. +Обновление — только `make golden-update` в `tabular-parser` с просмотром diff и отдельным коммитом в `fixtures`; +`make golden-check` (код возврата 1 при расхождении) — без записи. Оракулы корректности — property-тест +(`done`, контрольные суммы, корень = счёт шапки, период распознан, разбор < 5 с) и ручная сверка «Итого» с ячейками +файла, прочитанными независимо от парсера (АО РБ 60 за 1 кв. 2025, Счастливочная 62 за 3 кв. 2024 xls, ОСВ по субсчёту +58.03). diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 6f09e20..dfabec1 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -6,8 +6,8 @@ | ID | Вопрос | Откуда | Влияет на | Статус / допущение | |---|---|---|---|---| -| OQ-01 | Хранить ли все строки ОСВ или только контрагентские; нужен ли `parent_entry_id` (замечание 2 ревью v5.13) | DBML, «Парсинг ОСВ» п. 11 | срез 2 (парсер, `osv_entry`) | допущение: хранить все строки + `level` + `parent_entry_id`; строку «Итого» не хранить, использовать для сверки | -| OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | допущение: по тексту — квартальная; при наличии Q1 того же года сверять `opening` | +| OQ-01 | Хранить ли все строки ОСВ или только контрагентские; нужен ли `parent_entry_id` (замечание 2 ревью v5.13) | DBML, «Парсинг ОСВ» п. 11 | срез 2 (парсер, `osv_entry`) | допущение: хранить все строки + `level` + `parent_entry_id`; строку «Итого» не хранить, использовать для сверки; **допущение парсера (Срез 2a):** строка «Итого» отдаётся в `rows` как `group_type = TOTAL` (level 0, без родителя), в `metrics.rows` не считается; хранить ли её — решает ядро в 2c (`ASSUMPTION(OQ-01)` в `classify.py`) | +| OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | допущение: по тексту — квартальная; при наличии Q1 того же года сверять `opening`; **допущение парсера (Срез 2a):** `period_quarter` и `is_cumulative` вычисляются только по тексту периода (`rules/periods.yaml`; Q2–Q4 «N квартал» → `false`), сверка по данным — в ядре, подсрез 2c (`ASSUMPTION(OQ-02)`) | | OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | допущение: ключ (счёт, контрагент); пропуск квартала → блокирующая ошибка чек-листа | | OQ-04 | ОСВ по субсчёту (58.03 вместо 58) закрывает ли обязательный документ «ОСВ 58» | фикстуры | чек-лист | допущение: нет; показывать как «частично» | | OQ-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | допущение: `RECOGNIZED` ставится, но подтверждение всё равно обязательно | @@ -50,7 +50,7 @@ | OQ-42 | «Проверка распознавания» для ОСВ (xlsx) в макете отсутствует (строки `doc-row--readonly`, `39`). Нужен ли отдельный экран проверки ОСВ (дерево строк, подсветка контрагентов) или проверка ОСВ только через сводную ОСВ (`30`)? | макет [39] | срез 2 (UI) | допущение: минимальный экран таблицы ОСВ по §1 правил без левой панели исходника | | OQ-43 | Кнопка «К отчетности» неактивна у консолидаций в статусах «Инициация» и «Верификация РФ», активна у «Консолидация РФ» и «Верифицирован» (`34`). Каково правило доступности отчёта по статусу / роли? | макет [34] | срез 4, 7 (policy) | допущение: отчёт доступен с `CONSOLIDATION_RF` и далее для всех ролей с `CONSOLIDATION_VIEW`; «Инициация» — только карточка | | OQ-44 | «Добавить версию» создаёт версию сразу без модалки (`43-01`), копируя периметр; нужно ли подтверждение и выбор сценария/подхода новой версии? | макет [43-01] | срез 7 | допущение: без модалки, периметр и сценарий копируются из предыдущей версии | -| OQ-45 | Контракт `recognition-result.schema.json` допускает `data_type = NU` (строки налогового учёта в ОСВ «БУ, НУ»), а enum DBML `osv_data_type` — только BU, CURRENCY, QUANTITY, SUM. Добавлять NU в DBML (v5.14) или не хранить строки НУ? | контракт v1.0 vs DBML | срез 2 (маппинг `osv_entry`) | допущение принято и применено в Срезе 0: контракт v1.0 оставляет `NU` (в копиях схем трёх сервисов `DataType` содержит `NU`), ORM `OsvDataType` в arm-core 1:1 с DBML v5.13 (BU, CURRENCY, QUANTITY, SUM), маппинг результата в `osv_entry` — `NoopMapper`, поэтому расхождение Срез 0 не затрагивает; решение (кандидат DBML v5.14: добавить `NU` в `osv_data_type`, либо не хранить строки НУ) — до начала Среза 2; источник: arm-core, предложения, раздел C | +| OQ-45 | Контракт `recognition-result.schema.json` допускает `data_type = NU` (строки налогового учёта в ОСВ «БУ, НУ»), а enum DBML `osv_data_type` — только BU, CURRENCY, QUANTITY, SUM. Добавлять NU в DBML (v5.14) или не хранить строки НУ? | контракт v1.0 vs DBML | срез 2 (маппинг `osv_entry`) | допущение принято и применено в Срезе 0: контракт v1.0 оставляет `NU` (в копиях схем трёх сервисов `DataType` содержит `NU`), ORM `OsvDataType` в arm-core 1:1 с DBML v5.13 (BU, CURRENCY, QUANTITY, SUM), маппинг результата в `osv_entry` — `NoopMapper`, поэтому расхождение Срез 0 не затрагивает; решение (кандидат DBML v5.14: добавить `NU` в `osv_data_type`, либо не хранить строки НУ) — до начала Среза 2; источник: arm-core, предложения, раздел C; **допущение парсера (Срез 2a):** `data_type = NU` выдаётся как есть, без сверки с БУ (`ASSUMPTION(OQ-45)`), решение — в ядре, подсрез 2c | | OQ-46 | Обязательны ли все ОСВ расширенного подхода (02, 03, 08, 44, 26, 91, 51, 52, 59, 80, 83, 86, 97, 001, 008, 009, 011) при `approach = EXTENDED`, или часть — «при наличии»? | ТЗ 5.1; `required_document_rule.is_mandatory` | срез 0 (seed), срез 3 (чек-лист) | допущение принято и применено в Срезе 0: все 27 строк EXTENDED в seed `arm-core/api/seed/core/required_document_rules.yaml` (маркер `ASSUMPTION(OQ-46)`) — `is_mandatory = true`; BASIC — 10 строк, все обязательные; смена правила — правка одного yaml; вопрос к методологам остаётся открытым до Среза 3 (чек-лист); источник: arm-core, предложения, раздел A | | OQ-47 | Источник полного плана счетов 1С для seed `chart_of_account`: в фикстурах файла нет, «план счетов из бандла макета» не передан | `06-test-data.md` §6, `fixtures/README.md` | срез 0 (seed) | допущение принято и применено в Срезе 0: seed `arm-core/api/seed/core/chart_of_accounts.csv` — 122 строки (62 синтетических счёта Плана счетов 94н, 11 забалансовых, 49 субсчетов и подсубсчетов 1С БП 3.0, упомянутых в §4/§6); названия нестандартных субсчетов (66/67 `.06/.22/.24/.26`, 76.07.x, 76.08/.28/.38) выведены по их роли в §4 правил, а не взяты из справочника 1С; названия-заглушки заменить по справочнику 1С клиента, остальное добавляет администратор (ARCH-004); источник: arm-core, предложения, раздел A | | OQ-48 | Правило элиминации № 1 (§4), сторона Б «76.07.1 − 76.07.5 − 76.07.9»: 76.07.5 и 76.07.9 не потомки 76.07.1, поэтому «вычитание» чисто информативно. Так и задумано? | `05-domain-rules.md` §4 (правило 1); seed `elimination_rules_v1.yaml`; arm-core: предл. 1 (T14.1) | срез 5 (пары ВГО) | допущение принято: исключения 76.07.5/76.07.9 на стороне Б хранятся как `is_excluded` и не влияют на сумму (76.07.1 их не содержит); алгоритм на них не опирается; подтвердить с методологами | @@ -86,7 +86,7 @@ | OQ-78 | Шлюз читает ровно `//.json` (спека); старый код читал все файлы каталога `Output//`. Какие файлы и когда реальная ИС кладёт в `Output`, в каком порядке (момент готовности)? | scan-gateway: п. 5 таблицы предложений (T8.5) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** ИС кладёт **все распознанные документы одновременно** в каталог ``, по одному json на документ (пример: `aep_3.json` + `aep_3_ofr.json`); шлюз читает **все** json каталога и склеивает в один ответ (готовность — по стабилизации набора файлов, `SCAN_SETTLE_SECONDS`); ошибка одного документа не отменяет остальные (`metrics.warnings`). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | OQ-79 | Временный файл `.<имя>.part` лежит в `Input` (атомарная запись `.part` + rename): реальная ИС без маски может забрать недописанный файл. Уточнить у владельцев ИС маску и поведение или перенести временный файл в служебный подкаталог рядом с `Input`? | scan-gateway: п. 6 таблицы предложений (R-5, T7/T8) | срез 3 (стенд ИС) | **закрыт человеком 04.10.2026:** временный файл записи уходит из `Input` в служебный каталог рядом (`SCAN_TMP_DIR`, по умолчанию `Tmp`, на той же шаре — `rename` остаётся атомарным). Реализуется в Срезе 0 (доработка PR scan-gateway #3) | | OQ-80 | `make up` расходится с эталоном (scan-gateway и `dev/Makefile.common`): в `tabular-parser` нет `minio-init` — на свежем томе бакета `arm-ka` нет, `test_s3_integration` даёт `NoSuchBucket`, хотя README сервиса обещает бакет; в `arm-core` `up -d` без `--wait` и без `--project-name` (имя в compose фиксировано, безвредно). Также `check-full` в arm-core и tabular-parser — через пререквизиты (порядок при `make -j` не гарантирован), в scan-gateway — через `$(MAKE)`; `lint` arm-core включает `mypy scripts`. Подключить `include ../docs/dev/Makefile.common` в сервисах? | tabular-parser: `Makefile` (цель `up`); scan-gateway: п. 18 таблицы предложений (I-1); docs: O-1 и O-2 (задача 1 фазы 2), O-4 и O-5 (задача 2 фазы 2), `dev/checks.md`, `dev/Makefile.common` | срез 0 (процесс), все сервисы | допущение принято: эталон — scan-gateway, расхождения описаны в `dev/checks.md`, Makefile сервисов не правились (design #22, #24); выровнять — правкой Makefile сервисов (follow-up) | -| OQ-81 | Переменные `ARM_KA_DOCS` / `ARM_KA_FIXTURES` поддержаны неравномерно: `arm-core` — обе; `tabular-parser` — только `ARM_KA_DOCS`; `scan-gateway` — ни одной (жёстко `../docs`, `../../fixtures/ocr`). Текст причины skip («fixtures недоступен: положите репозиторий…») не содержит строки «ARM_KA_FIXTURES недоступен» из сценария «Нет фикстур» spec `quality-checks`; для docs-теста текст тоже без `ARM_KA_DOCS`. Нарушение spec — править сервисы? | scan-gateway и tabular-parser (тесты, Makefile); docs: O-3 (задача 1 фазы 2), `dev/checks.md`; spec `quality-checks` | срез 0 (процесс), все сервисы | допущение принято: расхождение описано в `dev/checks.md`; правка тестов и Makefile в scan-gateway и tabular-parser — follow-up человеку (сервисы не правились) | +| OQ-81 | Переменные `ARM_KA_DOCS` / `ARM_KA_FIXTURES` поддержаны неравномерно: `arm-core` — обе; `tabular-parser` — только `ARM_KA_DOCS`; `scan-gateway` — ни одной (жёстко `../docs`, `../../fixtures/ocr`). Текст причины skip («fixtures недоступен: положите репозиторий…») не содержит строки «ARM_KA_FIXTURES недоступен» из сценария «Нет фикстур» spec `quality-checks`; для docs-теста текст тоже без `ARM_KA_DOCS`. Нарушение spec — править сервисы? | scan-gateway и tabular-parser (тесты, Makefile); docs: O-3 (задача 1 фазы 2), `dev/checks.md`; spec `quality-checks` | срез 0 (процесс), все сервисы | допущение принято: расхождение описано в `dev/checks.md`; правка тестов и Makefile в scan-gateway и tabular-parser — follow-up человеку (сервисы не правились); **частично закрыт (Срез 2a, tabular-parser):** хелпер `tests/fixtures_helpers.py` поддерживает `ARM_KA_FIXTURES` (текст skip называет переменную и путь), `ARM_KA_DOCS` поддержан в тесте идентичности контракта; `scan-gateway` — по-прежнему ни одной переменной, остаётся follow-up человеку | | OQ-82 | `.dockerignore` в `arm-core` не исключает `api/.env.test`: файл попадает в образ (у `tabular-parser` и `scan-gateway` исключён). Исправлять? | arm-core `.dockerignore`; docs: T3.2 (задача 3 фазы 2) | срез 0 (образы), прод-сборка | допущение принято: безвредно (в файле стандартные dev-учётные данные compose, не секреты), в Срезе 0 не правилось; добавить исключение в arm-core — follow-up | | OQ-83 | Контейнеры профиля `services` подписаны на те же subject и queue group общего NATS, что и экземпляры в интеграционных тестах сервисов (`recognition.result` / `arm-core`, `*.request`): NATS может отдать сообщение теста контейнеру и наоборот; интеграционные тесты `scan-gateway` и `arm-core` одновременно тоже делят NATS. Сделать queue group и subject-префикс настраиваемыми для тестов (например `NATS_TEST_PREFIX`) или поднимать отдельный NATS на тесты? | scan-gateway: п. 11 таблицы предложений (T14.1, T11.8); docs: O-11 (задача 3 фазы 2; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно | | OQ-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнут `scan-gateway` (`OPENBAO__DATABASE__*`) и `tabular-parser` (`OPENBAO__DATABASE__*`, `OPENBAO__SMB__*` — `extra_forbidden`), в нём нет `OPENBAO__MINIO__MINIO_SECURE=false` (по умолчанию `true` — https к http-MinIO). Прогнать сценарий, зафиксировать набор переменных и портов (`SERVER__PORT`, `SCAN_HOTFOLDER_PATH` для `scan-gateway`), решить, нужен ли отдельный `.env.example` на сервис, и снять пометку «черновик» с раздела `dev/README.md`? | spec `dev-environment`, сценарий «Запуск из venv против compose без Nexus»; docs: design #43, `dev/README.md` («Запуск сервисов из venv без Nexus»); финальное ревью фазы 2: I-1 | срез 0 (процесс, окружение разработки); кандидат в follow-up issue | вопрос: допущение Среза 0 — раздел README остаётся черновиком, сценарий не подтверждён (проверка требовала `.venv` внутри сервисных репозиториев, в фазе 2 доступных только для чтения) | @@ -103,6 +103,14 @@ | OQ-95 | Код `LE_IN_OTHER_GROUP` в отчёте импорта называет ЮЛ (ИНН, имя) из ГК, которую филиальный пользователь с `DIRECTORY_ADMIN` видеть не должен. Допустимо ли раскрытие в отчёте импорта (право по seed только у ADMIN ГО)? | §13.3; §10.1; arm-core T10.9 | срез 1 | допущение: отчёт импорта вне policy видимости, право даётся только ГО | | OQ-96 | Граница «`effective_date` не раньше сегодня» (OQ-21) считается по UTC: для МСК правило сдвигается на 3 часа после полуночи. Какой часовой пояс — «банковский» МСК или UTC? | §13.4; arm-core R-9 | срез 1 | допущение: UTC | | OQ-97 | Право `DIRECTORY_DELETE` по матрице §10 только у ADMIN: удалять алиасы и пустые ГСК/ГСЗ могут только администраторы, а не КА. Расширить право на KA_RF/KA_GO/CURATOR_GO? | §10; arm-core O-6 | срез 1 (seed) | допущение: матрица не меняется | +| OQ-98 | Единый список кодов валют для строк `data_type = CURRENCY` («USD», «EUR» под контрагентом): достаточно ли ISO 4217-списка из `rules/data_types.yaml` (`currency_codes`: RUB, USD, EUR, CNY, KZT, BYN, GBP, CHF, JPY, TRY, AED)? Нужны ли нестандартные написания и коды, которые используют конфигурации 1С | tabular-parser: design D-4, R-6, R-22 | срез 2c (маппинг CURRENCY-строк), словарь `data_types.yaml` | допущение принято: ISO-перечень, дополняется строкой в YAML вместе с синтетикой; коды не входят в тест покрытия словарей (R-22) | +| OQ-99 | Несколько ОСВ **одного счёта** в одной книге (разные листы: два периода или две организации): парсер выдаёт по одному `document` на лист (`locator = sheet:<имя>`) и ничего не дедуплицирует. Как ядро выбирает/сопоставляет такие документы с периодом и организацией? | tabular-parser: spec `osv-parsing` («Листы ОСВ находятся по содержимому») | срез 2c (маппинг, `document.period_check`) | допущение принято: каждый лист — отдельный документ, выбор — за ядром и аналитиком при проверке | +| OQ-100 | ИНН внутри **текста** строки контрагента («Полиграф, 3528315710», 20 строк в ОСВ 76 Счастливочной) и склеенная ячейка «ИНН/КПП»: извлекать ли ИНН в парсере или в ядре при резолве контрагента? Сейчас `counterparty_inn` берётся только из колонки ИНН (§1.3), `counterparty_name` = весь текст строки | tabular-parser: R-15, R-17 (design №25, №27) | срез 2c (резолв `counterparty_id` по ИНН/имени) | допущение принято: парсер не извлекает; ядро разбирает имя; при отрицательном решении — правило в парсере одной функцией | +| OQ-101 | Числа с ведущим «+», «.5», «5.» парсер отвергает как нечисловой текст (`null` + предупреждение с номером строки). Встречаются ли такие записи в выгрузках других систем; принимать ли их | tabular-parser: T7.3 | срез 2a+, словарь/правила `numbers.py` | допущение принято: отвергаются; в реальных файлах не встречаются | +| 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 реальных файлов иначе не сходились | Служебные идентификаторы в 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 36683b7..0ec604c 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -74,21 +74,62 @@ UI справочника по макету) разделён: всё, кром ## Срез 2 — Загрузка файлов + табличный парсер (ОСВ, ББ, ОФР из Excel) — самый рискованный, делать рано +Решение владельца 05.10.2026: срез делится на четыре подсреза со своими change'ами и PR; сначала **только парсер ОСВ**. +Порядок: 2a → 2b → 2c → 2d. DoD среза в целом (без правил под конкретный файл, без координат в коде парсера) — в конце раздела. + +### Срез 2a — Парсер ОСВ (`tabular-parser`) — реализован, ожидает merge + +Change'и: `tabular-parser/openspec/changes/slice-2a-osv-parser` (issue arm-tabular-parser#5, PR #6), сквозной +`docs/openspec/changes/slice-2a-osv-parser` (arm-ka-docs#9), фикстуры arm-ka-fixtures#4. Копия схемы v1.1 в `arm-core` — +отдельный bounded-PR без логики после merge docs и tabular-parser. + +**Сделано (по факту):** + +- **Парсер ОСВ** `tabular-parser/api/src/parser/`: динамическое обнаружение структуры по содержимому (§1.0): листы ОСВ, + заголовок таблицы и колонки сумм по тексту (обязательны «обороты» и «сальдо на конец»), шапка и `group_schema` + (варианты 1 и 2), иерархия каскадом `outline → indent → account_pattern → sums` (+ `trivial` для пустой ОСВ), + классификация строк, `Decimal`, контрольные суммы §1.4, статусы и коды (`05-domain-rules.md` §1.6), лимиты + `PARSE_MAX_BYTES` (50 МиБ) и `PARSE_TIMEOUT_SECONDS` (120 с). Чтение XLSX/XLS/XLSB по сигнатуре. +- **Словари** `api/rules/{osv_headers,osv_groups,periods,data_types}.yaml` (данные, не код) с проверкой при старте и + тестом «каждая запись срабатывает хотя бы раз». +- **Контракт v1.1**: `rows[].extra` (нераспознанные колонки внутри таблицы); примеры `result-osv-minimal.json`, + `result-osv-needs-check.json`. +- **Результат на реальных данных:** все 66 файлов `fixtures/osv/**` — `done`, контрольные суммы сходятся, максимум 0,6 с + на файл; 16 синтетических файлов (`fixtures/osv/synthetic/`); 82 golden-снимка (`fixtures/golden/expected_osv_parse/`); + тест-линтер «нет координат в `src/parser/`»; CLI `python -m src.parser.cli <файл>`. +- **Нетривиальное решение (R-21):** сверка «сумма детей = родитель» по сальдо идёт по колонкам либо по нетто, если у + родителя сальдо свёрнуто (1С показывает счёт нетто, субсчета — развёрнуто); без него 32 из 66 файлов не сходились. +- **Допущения парсера, решаются в 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`. +- **Не сделано / риски:** xlsb проверен только рукописным синтетическим файлом (OQ-102); ИНН из текста строки не + извлекается (OQ-100); счета с буквой в первом сегменте не распознаются (OQ-103). + +### Срез 2b — ББ/ОФР из Excel (`tabular-parser`) + +- `tabular-parser`: ББ/ОФР из Excel по §12 в тот же `statementDocument`; поиск листов с формами по содержимому + (в 2a такие листы пропускаются с предупреждением `skipped: statement (slice 2b)`); словари синонимов `rules/*.yaml`; + контрольные формулы §7 для форм. +- Тесты: property-тесты инвариантов на **всех** файлах `fixtures/statements-xlsx/**` (формулы §7) + синтетика (ББ+ОФР на + одном листе, csv-подобный лист); снимки результата — рядом с golden ОСВ. + +### Срез 2c — Загрузка файлов и маппинг результата в ядре (`arm-core`) + - `arm-core`: `stored_file` + `s3_object` + `recognition_job` (outbox) + дедуп по `content_hash` + замена файла (`SUPERSEDED`). -- `tabular-parser`: **динамическое** обнаружение структуры (§1.0): поиск листов с ОСВ/ББ/ОФР по содержимому, заголовка - таблицы и колонок сумм по тексту, иерархии строк каскадом стратегий (outline level → отступ → шаблон кода → суммы), - шапки регулярками; классификация по `group_schema`; контрольные суммы; ББ/ОФР из Excel по §12 в тот же - `statementDocument`; словари синонимов `rules/*.yaml`; результат по контракту; NATS request/result; S3 `results/`. - Маппинг результата в ядре: `document` (OSV) + `osv_entry` (все строки, OQ-01) + резолв `account_id`, `account_root`, - `counterparty_id` по ИНН/имени/алиасу; `document.period_check / counterparty_check`; статусы. -- Тесты: property-тесты инвариантов на **всех** файлах `fixtures/osv/**` и `fixtures/statements-xlsx/**` («Итого» = - корень; тождество сальдо/оборотов; Σ детей = родитель; формулы §7 для форм) + синтетика для форматов, которых нет в - фикстурах (шапка вариант 2, без outline level, csv-подобный лист, Вал./Кол., xlsb, ББ+ОФР на одном листе, текстовые - числа). Снимок результатов → `fixtures/golden/expected_osv_parse/`. + `counterparty_id` по ИНН/имени/алиасу; `document.period_check / counterparty_check`; статусы. Решения, отложенные + парсером: OQ-01 (хранение «Итого»), OQ-02 (нарастающие по данным), OQ-45 (`NU`), OQ-67, OQ-69, OQ-70 (маппер и + `recognition.result`), OQ-100 (ИНН из текста имени контрагента). - Маппинг ББ/ОФР в ядре из `statementDocument` — общий для Excel и OCR (Срез 3 его переиспользует). -- UI: панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов. -- DoD: все реальные файлы и вся синтетика проходят **без единого правила под конкретный файл или координату**; в коде - парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером). +- Копия схемы контракта v1.1 в `arm-core` (`OsvRow.extra`) — если не сделана bounded-PR раньше. + +### Срез 2d — UI «Первичные документы» + +- Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов. + +**DoD среза 2 целиком:** все реальные файлы и вся синтетика проходят **без единого правила под конкретный файл или +координату**; в коде парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером). ## Срез 3 — ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение документа diff --git a/contracts/examples/result-osv-needs-check.json b/contracts/examples/result-osv-needs-check.json index 0ac352f..1d029c7 100644 --- a/contracts/examples/result-osv-needs-check.json +++ b/contracts/examples/result-osv-needs-check.json @@ -22,15 +22,17 @@ "header_variant": 1 }, "group_schema": [ + { "type": "ACCOUNT", "name": "Счет" }, { "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true } ], "columns": { - "opening_debit": "C", - "opening_credit": "D", - "turnover_debit": "E", - "turnover_credit": "F", - "closing_debit": "G", - "closing_credit": "H" + "opening_debit": "D", + "opening_credit": "E", + "turnover_debit": "F", + "turnover_credit": "G", + "closing_debit": "H", + "closing_credit": "I", + "inn": "B" }, "rows": [ { @@ -39,7 +41,7 @@ "parent_row_number": null, "row_value": "60", "group_type": "ACCOUNT", - "group_name": null, + "group_name": "Счет", "account_code_raw": "60", "counterparty_name": null, "counterparty_inn": null, @@ -55,7 +57,7 @@ "row_number": 2, "level": 1, "parent_row_number": 1, - "row_value": "ООО Лютик, ИНН 7711111111", + "row_value": "ООО Лютик", "group_type": "COUNTERPARTY", "group_name": "Контрагенты", "account_code_raw": "60", @@ -67,7 +69,8 @@ "turnover_debit": "500.00", "turnover_credit": "200.50", "closing_debit": "0.00", - "closing_credit": "700.50" + "closing_credit": "700.50", + "extra": { "КПП": "352801001" } }, { "row_number": 3, @@ -77,16 +80,33 @@ "group_type": "OTHER", "group_name": null, "account_code_raw": "60", - "counterparty_name": "ООО Лютик", - "counterparty_inn": "7711111111", + "counterparty_name": null, + "counterparty_inn": null, "data_type": "CURRENCY", "opening_debit": "0.00", "opening_credit": "400.00", "turnover_debit": "100.00", "turnover_credit": "50.00", "closing_debit": "0.00", - "closing_credit": "400.00", - "extra": { "КПП": "352801001" } + "closing_credit": "400.00" + }, + { + "row_number": 4, + "level": 0, + "parent_row_number": null, + "row_value": "Итого", + "group_type": "TOTAL", + "group_name": null, + "account_code_raw": null, + "counterparty_name": null, + "counterparty_inn": null, + "data_type": "BU", + "opening_debit": "0.00", + "opening_credit": "1000.00", + "turnover_debit": "500.00", + "turnover_credit": "200.50", + "closing_debit": "0.00", + "closing_credit": "700.50" } ], "checks": { diff --git a/openspec/changes/slice-2a-osv-parser/tasks.md b/openspec/changes/slice-2a-osv-parser/tasks.md index dfa44a2..32f4c91 100644 --- a/openspec/changes/slice-2a-osv-parser/tasks.md +++ b/openspec/changes/slice-2a-osv-parser/tasks.md @@ -3,9 +3,9 @@ - [x] 1.1 Контракт v1.1: `contracts/recognition-result.schema.json` (`rows[].extra`, title/description 1.1), примеры `result-osv-minimal.json` (+`extra`, `schema_version 1.1`) и новый `result-osv-needs-check.json`; `openspec validate --strict`; проверка схемой через `jsonschema` (скрипт в `dev/checks.md`, если есть, иначе вручную в отчёте). -- [ ] 1.2 `09-delivery-plan.md`: Срез 2 → подсрезы 2a–2d с составом каждого; `07-open-questions.md`: новые OQ (коды валют - CURRENCY-строк; несколько ОСВ одного счёта в книге), OQ-81 закрыт, пометки «допущение парсера» у OQ-01/02/45. -- [ ] 2.1 После реализации парсера (дочерний change, задачи 3.1–4.3): §1 `05-domain-rules.md` — статусы/коды, `hierarchy=<имя>` +- [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. +- [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 (таблица в отчёте). - [ ] 2.2 После merge дочернего change: `openspec archive slice-2a-osv-parser`; напоминание про bounded-PR копии схемы в arm-core.