From 57c22547beb4363bd1ab477d08afd161982d470f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 21:23:27 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=A1=D0=92:=20=D0=B7=D0=B0=D0=BF=D0=B0?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B9=20=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82?= =?UTF-8?q?=20=D1=82=D1=83=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D1=83=20=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=BF=D0=BF,=20=D1=87=D1=82=D0=BE=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D1=8C=D0=BC=D1=91=D1=82=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=20(#81,=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20I-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Преамбула «Обороты: …» над шапкой давала ОСВ, разбор листа падал и ронял книгу. Негатив s56 «Преамбула». Co-Authored-By: Claude Opus 5.5 --- api/src/parser/detect.py | 16 ++++++++++------ api/tests/synthetic/generate.py | 10 ++++++++++ api/tests/synthetic/test_synthetic_files.py | 8 +++++++- api/tests/unit/parser/test_detect.py | 17 +++++++++++++++++ openspec/changes/osv-other-systems/design.md | 2 +- 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/api/src/parser/detect.py b/api/src/parser/detect.py index 1570cff..23c6a09 100644 --- a/api/src/parser/detect.py +++ b/api/src/parser/detect.py @@ -184,17 +184,21 @@ def has_osv_table(sheet: Sheet, rules: Rules) -> bool: """Запасной признак листа ОСВ без названия ведомости (#81, design D1): в первых `SHEET_MARKER_ROWS` строках есть строка со всеми тремя группами сумм, и у каждой группы — колонки «Дебет» и «Кредит» (свёрнутое сальдо не годится, R-3), между которыми в строках заголовка нет других подписей (R-11: у отчёта 1С «Обороты счета» между «Оборот Дт» и - «Оборот Кт» — колонки корр. счетов, это не ОСВ). Колонки ищутся тем же разбором, что у обычного заголовка (R-6).""" + «Оборот Кт» — колонки корр. счетов, это не ОСВ). Проверяется та строка групп, которую возьмёт `find_table_header` + (первая с «оборотами» и «концом»), — иначе преамбула «Обороты: …» над шапкой уронила бы разбор листа; колонки + ищутся тем же разбором, что у обычного заголовка (R-6).""" for row in range(1, min(sheet.n_rows, SHEET_MARKER_ROWS) + 1): cells = _group_cells(sheet, row, rules) - if {cell.group for cell in cells} != set(GROUP_ORDER): + groups = {cell.group for cell in cells} + if not REQUIRED_GROUPS <= groups: continue + if groups != set(GROUP_ORDER): + return False try: - groups = [_columns_of_group(sheet, row, cells, group, rules) for group in GROUP_ORDER] + columns = [_columns_of_group(sheet, row, cells, group, rules) for group in GROUP_ORDER] except InvalidFormatError: - continue - if all(_sides_side_by_side(sheet, row, columns) for columns in groups): - return True + return False + return all(_sides_side_by_side(sheet, row, group_columns) for group_columns in columns) return False diff --git a/api/tests/synthetic/generate.py b/api/tests/synthetic/generate.py index 327c886..4fb19c9 100644 --- a/api/tests/synthetic/generate.py +++ b/api/tests/synthetic/generate.py @@ -1359,9 +1359,19 @@ def _s56() -> list[SheetSpec]: ], ), _navision_sheet("Ниже 30", top_padding=30), + _preamble_navision_sheet(), ] +def _preamble_navision_sheet() -> SheetSpec: + """Полная шапка Navision, над ней преамбула «Обороты за период: … | Остаток на конец: …»: разбор берёт строку + преамбулы, значит, и запасной признак смотрит её — лист прочий, книга не падает (ревью #81, Important-1).""" + sheet = _navision_sheet("Преамбула", top_padding=1) + sheet.put(1, 1, "Обороты за период: 50") + sheet.put(1, 2, "Остаток на конец: 150") + return sheet + + def _s57() -> list[SheetSpec]: """Негатив: форма ББ с примечанием «см. оборотную ведомость …» и текстом «Остаток на начало периода …», за ней лист слабого продолжения (одна строка с кодом) — форма и продолжение остаются формой.""" diff --git a/api/tests/synthetic/test_synthetic_files.py b/api/tests/synthetic/test_synthetic_files.py index 2dd4ed3..2660977 100644 --- a/api/tests/synthetic/test_synthetic_files.py +++ b/api/tests/synthetic/test_synthetic_files.py @@ -955,7 +955,13 @@ def test_neg_s56_almost_osv_tables_are_other(fixtures_dir: Path) -> None: outcome = parse_named(fixtures_dir, S56) assert [doc.locator for doc in documents(outcome)] == ["sheet:ОСВ 60"] - assert sheet_other(outcome) == ["sheet:Расшифровка", "sheet:Без начала", "sheet:Свёрнутое начало", "sheet:Ниже 30"] + assert sheet_other(outcome) == [ + "sheet:Расшифровка", + "sheet:Без начала", + "sheet:Свёрнутое начало", + "sheet:Ниже 30", + "sheet:Преамбула", + ] def test_neg_s57_form_with_osv_words_stays_form(fixtures_dir: Path) -> None: diff --git a/api/tests/unit/parser/test_detect.py b/api/tests/unit/parser/test_detect.py index 89f547c..f44ed2d 100644 --- a/api/tests/unit/parser/test_detect.py +++ b/api/tests/unit/parser/test_detect.py @@ -170,6 +170,23 @@ def test_has_osv_table_only_first_rows(rules: Rules, blank_rows: int, expected: assert has_osv_table(sheet, rules) is expected +@pytest.mark.parametrize( + "preamble", + [ + ["Обороты за период: 50", "Остаток на конец: 150"], + ["Остаток на начало: 100", "Обороты: 50", "Остаток на конец: 150"], + ], +) +def test_has_osv_table_checks_the_row_find_table_header_takes(rules: Rules, preamble: list[object]) -> None: + """Финальное ревью, Important-1: преамбула с «оборотами» над полной шапкой — строка, которую возьмёт разбор + (`find_table_header`); она не годится, значит, лист не ОСВ (иначе разбор листа упадёт и уронит книгу).""" + sheet = sheet_from_rows([preamble, NAVISION_HEADER, NAVISION_ROW]) + + with pytest.raises(InvalidFormatError): + find_table_header(sheet, rules) + assert has_osv_table(sheet, rules) is False + + def test_classify_sheet_falls_back_to_table(rules: Rules) -> None: assert classify_sheet(sheet_from_rows([NAVISION_HEADER, NAVISION_ROW]), rules) is SheetKind.OSV diff --git a/openspec/changes/osv-other-systems/design.md b/openspec/changes/osv-other-systems/design.md index 5129fd9..731dcee 100644 --- a/openspec/changes/osv-other-systems/design.md +++ b/openspec/changes/osv-other-systems/design.md @@ -135,7 +135,7 @@ xlsb) на main `a8c9605` и на ветке. Сравнение по каждо | 2026-10-28 | R-2. Запасной признак ищет строку групп только в первых `SHEET_MARKER_ROWS` (30) строках | у безымянных листов таблица начинается сразу; ограничение снижает время и ложные срабатывания на длинных листах «прочее» | безымянный лист с преамбулой длиннее 30 строк не распознаётся; поднимется регрессией D3 | | 2026-10-28 | R-3. Запасной признак не принимает свёрнутое сальдо (одна колонка в группе) | строгость против ложных ОСВ (D1) | ОСВ без названия со свёрнутым сальдо не распознаётся | | 2026-10-28 | R-5. Новые `sheet_markers` якорятся к началу ячейки (`^…`); якорь снимается для конкретного названия, только если по спискам 1717 файлы с ним иначе не находятся | «Оборотная ведомость», «Оборотный баланс» могут встретиться в примечаниях к ББ; признак по названию проверяется раньше форм | часть ведомостей с текстом перед названием не распознаётся; видно в регрессии D3 | -| 2026-10-28 | R-6. Запасной признак переиспользует поиск строки групп и колонок обычного заголовка (`_find_group_row`, `_columns_of_group`) в строгом режиме, а не отдельную функцию | запасной признак и конвейер не должны расходиться в том, что считать заголовком | — | +| 2026-10-28 | R-6. Запасной признак проверяет ту же строку групп, что возьмёт `find_table_header` (первую с «оборотами» и «концом» в первых 30 строках), и колонки ищет тем же `_columns_of_group`; строгие условия — только к этой строке (финальное ревью, I-1: иначе преамбула «Обороты: …» над шапкой давала ОСВ, разбор падал и ронял книгу; негатив s56 «Преамбула») | запасной признак и конвейер не должны расходиться в том, что считать заголовком | безымянная ОСВ с преамбулой «Обороты: …» над шапкой не распознаётся | | 2026-10-28 | R-7. Порядок PR: `fixtures` (golden синтетики, отдельный коммит `make golden-update`) → tabular-parser → docs | golden-тесты tabular-parser читают `../fixtures` | — | | 2026-10-28 | R-8. Приёмка — синтетика + `make check-full` + нет ухудшений D3; число исправленных файлов банка — отчёт, не порог | эксперимент автора задач — ориентир, а не обязательство; остаток держат 1720–1723 | — | | 2026-10-28 | R-9. Написание «Остаток по дебету/кредиту на начало/конец» (6 файлов КТЗ/ЧАЗ) включено в словарь: группа «остаток» с опережающей проверкой, сторона — из остатка ячейки | та же семья «Остаток на начало/конец» (1719), решается записью словаря | лишняя запись, удаляется |