From 69564ffcbde8eecebe540614a689c0dd14cf6d6a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 12:40:01 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20README,=20CLAUDE.md=20=D0=B8=20change?= =?UTF-8?q?=20osv-flat-form=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 2 +- README.md | 8 +++++++- api/rules/osv_headers.yaml | 3 ++- api/src/parser/classify.py | 6 ++++-- openspec/changes/osv-flat-form/design.md | 16 ++++++++++++---- openspec/changes/osv-flat-form/tasks.md | 12 ++++++------ 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6759afb..d7b81b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote - `grid.py` + `readers/` — модель листа и чтение xlsx/xls/xlsb; единственное место, где допустимы индексы и буквы колонок (`column_letter` — только диагностика). `read_book(data, file_type) -> (листы, предупреждения)`, формат — по сигнатуре. - `rules.py` — загрузчик словарей `api/rules/*.yaml`; `detect.py` — вид листа и заголовок таблицы; `header.py` — шапка - и `group_schema`; `hierarchy.py` — каскад `outline → indent → account_pattern → sums` (+ `trivial`); `inn_rows.py` — + и `group_schema`; `hierarchy.py` — каскад `outline → indent → account_pattern → header_account → sums` (+ `trivial`); `inn_rows.py` — строки ИНН рядом с контрагентом (OQ-163); `indicators.py` — строки-показатели (колонка «Показатели», «В валюте»); `numbers.py` — `Decimal` и ИНН (контрольная сумма); `classify.py` — строки и группировки; `checks.py` — контрольные суммы §1.4; `osv.py` — оркестратор `OsvParser`; `deadline.py` — кооперативный таймаут; `errors.py` — коды ошибок; `cli.py` — разбор файла. - `get_parser(settings)` создаёт `OsvParser` один раз при старте (кэша нет); `NullParser` — только для тестов воркера. diff --git a/README.md b/README.md index 25c550f..d08787c 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,12 @@ openspec/ требования и дельта текущего ср ParseOutcome`. Структура ОСВ ищется **по содержимому**: ни букв колонок, ни номеров строк, ни имён листов в коде нет (это проверяет AST-линтер, см. ниже), синонимы лежат в `api/rules/*.yaml`. +**Плоская форма 1С по одному счёту (#59, AURORA-1661):** «по счету: X» в шапке задаёт счёт всем строкам, строка вида +субконто между названием и периодом не считается организацией, «Итого развернутое» — итоговая строка в любом порядке +относительно «Итого», плоский список без строк-счетов разбирается стратегией `header_account`, а аналитика расчётных +счетов (`counterparty_accounts` в `osv_groups.yaml`) без названия вида получает `COUNTERPARTY` с предупреждением. +Правила — `../docs/context/05-domain-rules.md` §1.1, §1.3, §1.4. + ### Модули (`api/src/parser/`) Конвейер идёт сверху вниз; все модули, кроме `grid.py` и `readers/`, оперируют индексами из содержимого и не знают @@ -117,7 +123,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо | `rules.py` | загрузчик словарей `rules/*.yaml` в frozen-модели (pydantic), `normalize_text`, `Pattern`; ошибка словаря — `RuntimeError` с файлом и ключом при старте | | `detect.py` | вид листа (`classify_sheet`: ОСВ / форма ББ-ОФР / прочее), блок заголовка таблицы (`find_table_header`: 1–4 строки, шесть колонок сумм, колонка текста, ИНН, дополнительные колонки, правая граница, повтор заголовка внутри данных) | | `header.py` | шапка документа (организация, счёт, период, тип данных, валюта) и схема группировок `group_schema` (варианты шапки 1 и 2) | -| `hierarchy.py` | каскад стратегий уровней строк: `outline` → `indent` → `account_pattern` → `sums`; `trivial` — меньше двух строк данных; выбранная стратегия пишется в `warnings[]` документа листа как `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`info`) | +| `hierarchy.py` | каскад стратегий уровней строк: `outline` → `indent` → `account_pattern` → `header_account` → `sums`; `trivial` — меньше двух строк данных; `header_account` — плоский список под счётом шапки (код счёта в шапке, строк-счетов в теле нет; плоская форма 1С, #59); выбранная стратегия пишется в `warnings[]` документа листа как `OSV_HIERARCHY_BY_<СТРАТЕГИЯ>` (`info`) | | `numbers.py` | суммы в `Decimal` с двумя знаками (`to_money`), ИНН из текста ячейки (`inn_digits`); `float` в домене нет | | `inn_rows.py` | строки ИНН рядом с контрагентом (восемь раскладок, контрольная сумма без подсказки шапки, противоречие подсказке, OQ-163); выполняется над деревом до классификации | | `indicators.py` | строки-показатели (§1.3): метка вида суммы из колонки «Показатели» или текста строки (`read_label`), привязка показателя к строке данных над ним (`levels_with_indicators`) | diff --git a/api/rules/osv_headers.yaml b/api/rules/osv_headers.yaml index b4956f3..bafa8ac 100644 --- a/api/rules/osv_headers.yaml +++ b/api/rules/osv_headers.yaml @@ -51,7 +51,8 @@ total_row: total_row_exact: - '^итого$' -# Вторая итоговая строка после «Итого» («Итого развернутое» в 1С): тоже TOTAL, но «Итого» листа — основная, первая из них +# Вторая итоговая строка («Итого развернутое» в 1С): тоже TOTAL, но «Итого» листа — основная. Стоит сразу после основной +# «Итого» или ПЕРЕД ней (плоская форма 1С, #59); сверяется основная total_row_secondary: - '^итого\s+развернут' diff --git a/api/src/parser/classify.py b/api/src/parser/classify.py index 9ef2c94..789c897 100644 --- a/api/src/parser/classify.py +++ b/api/src/parser/classify.py @@ -310,7 +310,8 @@ def collect_body_rows( """Строки данных листа (включая «Итого»), номер строки «Итого» (`None` — не найдена) и предупреждения. Строки после «Итого» — подвал (подписи, числа правее таблицы): ни в данные, ни в `extra` они не попадают (T4.5); - исключение — вторые итоговые строки («Итого развернутое») сразу за «Итого»: они остаются в таблице как TOTAL. + исключение — вторые итоговые строки («Итого развернутое»): они остаются в таблице как TOTAL и стоят как сразу + за «Итого», так и перед ней (плоская форма 1С); возвращаемый номер — первая строка итогового блока. """ warnings: list[str] = [] if header.columns.collapsed: @@ -471,7 +472,8 @@ def classify_rows( # noqa: PLR0913, PLR0917 - интерфейс плана (Ta ) -> list[ClassifiedRow]: """Классификация строк по дереву `levels` и схеме группировок. - - «Итого» (`total_row`) и вторые итоговые строки после неё («Итого развернутое») — TOTAL, уровень 0 без родителя; + - «Итого» (`total_row`) и вторые итоговые строки («Итого развернутое»; стоят после неё или перед ней) — TOTAL, + уровень 0 без родителя; в `rows` остаются (# ASSUMPTION(OQ-01): хранить ли «Итого», решает ядро); - счёт (`ACCOUNT_RE`) — ACCOUNT, `account_code_raw` — код как в файле; дети наследуют код ветки; - строка-показатель (`BodyRow.indicator`: метка колонки «Показатели» не «БУ», «Вал.», «Кол.», код валюты, §1.3) — diff --git a/openspec/changes/osv-flat-form/design.md b/openspec/changes/osv-flat-form/design.md index 5ec4727..b4970bf 100644 --- a/openspec/changes/osv-flat-form/design.md +++ b/openspec/changes/osv-flat-form/design.md @@ -89,6 +89,11 @@ r11 Август - Агро ООО | … суммы … ← с | 8 | R-8 | 19.10.2026 | «Учредители» на счёте 80 не трогаем: именованная группировка остаётся OTHER (R-6 сохраняется) | Принято по рекомендации сессии при одобрении запуска владельцем; именованные группировки правило расчётных счетов не переопределяет | Учредители не станут COUNTERPARTY автоматически — гудвилл (§4 п. 2) потребует ручного сопоставления | | 9 | R-9 | 19.10.2026 | Счета 71 и 73 исключены из `counterparty_accounts`; итог: 58, 60, 62, 66, 67, 75, 76, 80 | Подотчётные и персонал — физлица, а не контрагенты (OQ-158); принято по рекомендации сессии | Контрагенты на 71/73 без названия вида останутся OTHER и не попадут в сопоставление | | 10 | R-10 | 19.10.2026 | «Организации» — синоним COUNTERPARTY в `osv_groups.yaml` (`^организаци`) | Вид субконто письма (сч. 60) — организации-контрагенты; принято по рекомендации сессии | Другой смысл вида «Организации» на иных счетах даст COUNTERPARTY; видно в golden/предупреждениях | +| 11 | R-11 | 19.10.2026 | Данные синтетики s40, s41, s42 исправлены: у «Итого развернутое» добавлены обороты, равные суммам строк (s40: 650/750, s41: 5000/4991, s42: 5700/6600) | В плане у этой строки обороты пусты, поэтому тождество строки (начало + обороты = конец) не сходилось, и файлы не могли стать `done` независимо от кода; бриф плана ошибочен | s40–s42 расходятся с исходным текстом плана. **Приёмка Task 6 показала:** в реальных файлах контура у «Итого развернутое» обороты тоже пусты (`45e06f7d`, `638ecb3d`, `7e49f8b5`, `f2e0e04e`, `270b41da`, `738bfcf5`, письмо), тождество на этой строке остаётся нарушенным, и файл уходит в `needs_check`: правка данных синтетики скрыла дефект кода, а не данных; решается отдельно (см. отчёт task-6) | +| 12 | R-12 | 19.10.2026 | Строка вида субконто не даёт группировку из ячеек-меток шапки («Субконто», «Счет»), меток валюты («Валюта БУ, НУ») и ячеек, на которые срабатывает паттерн `currency` | Ячейка между названием и периодом может быть меткой шапки, а не видом субконто; иначе в схему попали бы ложные группировки (T2.2, ревью задачи 2) | Метка, совпавшая с настоящим видом субконто, не станет группировкой; вид «Организации» уже учтён (R-10) | +| 13 | R-13 | 19.10.2026 | Предупреждение `OSV_ROWS` «вид субконто в шапке не назван…» — одно на каждый различный `account_code_raw` строк, принятых по плану счетов | План требовал «один раз на документ»; у листа с несколькими субсчетами (62.01, 62.02) заметка с точным кодом полезнее одной общей | Больше заметок на листах с несколькими субсчетами (лист `67 сч.` — четыре); на статус не влияют (`info`) | +| 14 | R-14 | 19.10.2026 | `Expect.data_type` в `test_synthetic_files.py` стал `str \| None`: у s40–s44 ожидается `None` | В шапке нет БУ/НУ; умолчание BU — зона трека A (`data_type`) | При слиянии трека A ожидания s40–s44 придётся поправить на `BU` | +| 15 | R-15 | 19.10.2026 | Предупреждение `OSV_TOTAL` при несходимости «Итого» без корня ACCOUNT, но со строками уровня 0, говорит «≠ сумме N строк уровня 0» (T4.1) | Прежний текст «корневого счёта нет» вводил в заблуждение для плоского списка | Текст в `metrics.warnings` изменился; снимков с прежним текстом нет | ## Risks / Trade-offs @@ -100,8 +105,11 @@ r11 Август - Агро ООО | … суммы … ← с ## Open Questions -Закрыты по рекомендации сессии при одобрении запуска 19.10.2026 (R-8…R-10); владелец может пересмотреть: +Решения R-8…R-10 приняты по рекомендации сессии при одобрении запуска 19.10.2026; вопросы **открыты** (docs: `07-open-questions.md`, +OQ-169…OQ-172), в коде действует допущение: -- «Учредители» на 80 (O-29, `02c5df51`) — остаётся OTHER; гудвилл §4 п. 2 — вопрос владельцу. -- Счета 71 и 73 (подотчётные, персонал) — исключены из списка расчётных. -- «Организации» — синоним COUNTERPARTY. +- OQ-169 — «Учредители» на 80 (O-29, `02c5df51`) остаются OTHER (R-8); гудвилл §4 п. 2 — вопрос владельцу. +- OQ-170 — счета 71 и 73 (подотчётные, персонал) исключены из списка расчётных (R-9). +- OQ-171 — «Организации» — синоним COUNTERPARTY (R-10). +- OQ-172 — безымянная аналитика первого уровня на счёте 63 (51 строка в golden) не входит в список расчётных счетов: + нужен ли 63. diff --git a/openspec/changes/osv-flat-form/tasks.md b/openspec/changes/osv-flat-form/tasks.md index 56fc4b0..e63f592 100644 --- a/openspec/changes/osv-flat-form/tasks.md +++ b/openspec/changes/osv-flat-form/tasks.md @@ -2,24 +2,24 @@ ## 1. Синтетика и снимки (tabular-parser, fixtures) -- [ ] 1.1 Синтетика `s40`–`s44` в `api/tests/synthetic/generate.py`, файлы в `fixtures/osv/synthetic/`, снимки текущего поведения +- [x] 1.1 Синтетика `s40`–`s44` в `api/tests/synthetic/generate.py`, файлы в `fixtures/osv/synthetic/`, снимки текущего поведения ## 2. Шапка (tabular-parser) -- [ ] 2.1 Код счёта «по счету: X», строка вида субконто, «План счетов <организация>» (`header.py`, `rules.py`, `osv_headers.yaml`) +- [x] 2.1 Код счёта «по счету: X», строка вида субконто, «План счетов <организация>» (`header.py`, `rules.py`, `osv_headers.yaml`) ## 3. Итоговый блок (tabular-parser) -- [ ] 3.1 «Итого развернутое» перед «Итого» — TOTAL; сверка по основной «Итого» (`classify.py`, `checks.py`, `osv.py`) +- [x] 3.1 «Итого развернутое» перед «Итого» — TOTAL; сверка по основной «Итого» (`classify.py`, `checks.py`, `osv.py`) ## 4. Плоский список (tabular-parser) -- [ ] 4.1 Стратегия `header_account`, счёт шапки у корня ветки, «Итого» без корня ACCOUNT (`hierarchy.py`, `classify.py`, `checks.py`, `osv.py`, `osv_warnings.py`) +- [x] 4.1 Стратегия `header_account`, счёт шапки у корня ветки, «Итого» без корня ACCOUNT (`hierarchy.py`, `classify.py`, `checks.py`, `osv.py`, `osv_warnings.py`) ## 5. Контрагенты по плану счетов (tabular-parser) -- [ ] 5.1 `counterparty_accounts` в `osv_groups.yaml`, COUNTERPARTY на позиции 1 за схемой, предупреждение `OSV_ROWS` +- [x] 5.1 `counterparty_accounts` в `osv_groups.yaml`, COUNTERPARTY на позиции 1 за схемой, предупреждение `OSV_ROWS` ## 6. Документация и приёмка (docs, tabular-parser) -- [ ] 6.1 `05-domain-rules.md` §1.1, §1.3, §1.4; `07-open-questions.md`; README парсера; прогон реальных файлов контура; `make check-full` +- [x] 6.1 `05-domain-rules.md` §1.1, §1.3, §1.4; `07-open-questions.md` (OQ-169…OQ-172); README и CLAUDE.md парсера; прогон реальных файлов контура (отчёт task-6; отклонения — «Итого развернутое» без оборотов); `make check`, `make check-full`