From f5bb178899685e8e01daff2265321476732f6844 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 20:41:09 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=A1=D0=92:=20=D0=BD=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=B9,=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D1=85=20=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC,=20=C2=AB=D0=92=D0=A1=D0=95=D0=93=D0=9E=C2=BB=20(#?= =?UTF-8?q?81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AURORA-1717 (названия), AURORA-1719 (остатки, «по дебету», «Сальдо вх/исх», «ДТВВ»), AURORA-1732 («ВСЕГО»). Co-Authored-By: Claude Opus 5.5 --- api/rules/osv_headers.yaml | 30 ++++++++++++++++++--- api/tests/synthetic/test_synthetic_files.py | 8 +----- api/tests/unit/parser/test_detect.py | 19 +++++++++++++ api/tests/unit/parser/test_rules.py | 8 +++++- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/api/rules/osv_headers.yaml b/api/rules/osv_headers.yaml index 17c89c4..01a158f 100644 --- a/api/rules/osv_headers.yaml +++ b/api/rules/osv_headers.yaml @@ -7,29 +7,51 @@ # ведомость», «д т», `\w` вместо диапазона; # - новый синоним = новая строка в нужном списке, без правки кода. -# Лист содержит ОСВ: любая ячейка (первые строки листа) матчит один из паттернов +# Лист содержит ОСВ: любая ячейка (первые строки листа) матчит один из паттернов. Названия других учётных систем и +# отчётов 1С (AURORA-1717) — только с начала ячейки: «… см. оборотную ведомость» в примечании формы листом ОСВ не делает sheet_markers: - 'оборотно\s*сальдовая\s+ведомость' + - '^оборотн\w*\s+ведомост' # «Оборотная ведомость по счетам …» (выгрузки КТЗ) + - '^осв\b' # «ОСВ по сч. 41 за 3 кв.» (КТЗ) + - '^оборотн\w*\s+баланс' # «Оборотный баланс» (КТЗ) + - '^фин\.?\s*обороты' # «Фин. Обороты Листок» (Navision, Айсберри) + - '^отчет\s+по\s+главной\s+книге' # SAP, ООО «Виола» # Три группы сумм в заголовке таблицы. «Оборотно сальдовая» (название ведомости) не должно матчить turnover: -# отсюда границы слова `\bоборот(ы)?\b`. +# отсюда границы слова `\bоборот(ы)?\b`. Сторона может стоять в ячейке группы после названия — в том числе вплотную +# («НачалоДт», «ОборотыДтВВ»): паттерн группы тогда заканчивается перед стороной (опережающая проверка), остаток ячейки +# разбирают `debit`/`credit` (AURORA-1719) groups: opening: - 'сальдо\s+на\s+начало' - '\bнач(альн\w*)?\.?\s*сальдо' - 'входящ\w*\s+сальдо' + - 'остат\w*\s+на\s+начало' # «Остаток на начало (периода)» (КТЗ) + - 'остат\w*(?=\s+по\s+(дебету|кредиту)\s+на\s+начало)' # «Остаток по дебету на начало» (КТЗ, ЧАЗ) + - 'сальдо\s+начальное' # «Сальдо НачальноеДТВВ» (SAP, НМЖК) + - 'сальдо\s+вх\b' # «Сальдо вх Дт» (SAP, Виола) turnover: - '\bоборот(ы)?\b' + - '^оборот(ы)?(?=\s*(дт|кт))' # «ОборотДт» (Navision), «ОборотыДтВВ» (SAP, НМЖК) closing: - 'сальдо\s+на\s+конец' - '\bкон(ечн\w*)?\.?\s*сальдо' - 'исходящ\w*\s+сальдо' + - 'остат\w*\s+на\s+конец' # «Остаток на конец (периода)» (КТЗ) + - 'остат\w*(?=\s+по\s+(дебету|кредиту)\s+на\s+конец)' # «Остаток по кредиту на конец» (КТЗ, ЧАЗ) + - 'сальдо\s+конечное' # «Сальдо КонечноеКТВВ» (SAP, НМЖК) + - 'сальдо\s+исх\b' # «Сальдо исх Кт» (SAP, Виола) -# Подзаголовки сумм под группой («Дебет»/«Кредит», «Дт»/«Кт», «Д-т»/«К-т»): ячейка начинается с синонима +# Подзаголовки сумм под группой («Дебет»/«Кредит», «Дт»/«Кт», «Д-т»/«К-т») или сторона в остатке ячейки группы: +# ячейка (остаток) начинается с синонима debit: - '^(дебет|дт|д т)\b' + - '^по\s+дебету\b' # «Оборот по дебету», «Остаток по дебету на начало» (КТЗ) + - '^дт\s*вв\b' # «Сальдо НачальноеДТВВ» (SAP, НМЖК) credit: - '^(кредит|кт|к т)\b' + - '^по\s+кредиту\b' # «Оборот по кредиту» (КТЗ) + - '^кт\s*вв\b' # «Сальдо НачальноеКТВВ» (SAP, НМЖК) # Служебные колонки таблицы по тексту заголовка. Код знает колонки ИНН и «Показатели»; прочие колонки таблицы (КПП, код, # количество, валюта и т. п.) попадают в `rows[].extra` под текстом своего заголовка и словаря не требуют. @@ -52,6 +74,8 @@ total_row_exact: - '^итого$' # выгрузка 1С «по контрагентам»: итог листа — «ИТОГ» (без «О»); «ИТОГ по 60.111» — итог субсчёта (subaccount_sums) - '^итог$' + # «ВСЕГО» — итог листа ведомостей ГК КТЗ по счёту 009 (AURORA-1732) + - '^всего$' # Итог субсчёта в выгрузке 1С «по контрагентам»: «ИТОГ по 60.111» — суммы строки-счёта с тем же кодом (группа account); # отдельной строкой в результат не попадает (05-domain-rules.md §1.3, AURORA-1714) diff --git a/api/tests/synthetic/test_synthetic_files.py b/api/tests/synthetic/test_synthetic_files.py index b6b3d02..f1641f2 100644 --- a/api/tests/synthetic/test_synthetic_files.py +++ b/api/tests/synthetic/test_synthetic_files.py @@ -68,10 +68,8 @@ S55 = "s55_not_osv_titles.xlsx" S56 = "s56_almost_osv_tables.xlsx" S57 = "s57_form_mentions_osv.xlsx" OTHER_SYSTEMS_FILES = (S48, S49, S50, S51, S52, S53, S54, S55, S56, S57) -PENDING_DICTIONARY = "tp#81 Task 2: словари названий, групп и сторон" PENDING_FALLBACK = "tp#81 Task 3: запасной признак листа по заголовку таблицы" -PENDING_81 = {S48: PENDING_DICTIONARY, S49: PENDING_DICTIONARY, S50: PENDING_DICTIONARY, S53: PENDING_DICTIONARY} -PENDING_81 |= {S51: PENDING_FALLBACK, S52: PENDING_FALLBACK, S54: PENDING_FALLBACK} +PENDING_81 = {S51: PENDING_FALLBACK, S52: PENDING_FALLBACK, S54: PENDING_FALLBACK} S48_SHEETS = ("ОВ", "ОСВ сокр", "Оборотный баланс", "Навижн", "Главная книга", "Верхний регистр", "Вторая колонка") TYPO_ROW = 13 # строка «7701234567» с неверной контрольной суммой в s28 EXPECTED_NAMES = [ @@ -865,7 +863,6 @@ def assert_done_with_sums(doc: OsvDocument, text: str, expected: tuple[Decimal | assert doc.rows[-1].group_type == "TOTAL" -@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY) def test_s48_other_system_titles_are_osv(fixtures_dir: Path) -> None: """Каждое название ведомости — свой лист ОСВ; «ВСЕГО» и «Всего » — итоговая строка (AURORA-1717, 1732).""" outcome = parse_named(fixtures_dir, S48) @@ -881,7 +878,6 @@ def test_s48_other_system_titles_are_osv(fixtures_dir: Path) -> None: assert not [text for text in warnings_of(outcome) if TOTAL_NOT_FOUND in text] -@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY) def test_s49_balance_and_turnover_by_side(fixtures_dir: Path) -> None: """«Остаток на начало/конец», «Оборот по дебету/кредиту», «Остаток по дебету на начало»; преамбула «Остаток на начало: 0» над таблицей строкой групп не считается.""" @@ -893,7 +889,6 @@ def test_s49_balance_and_turnover_by_side(fixtures_dir: Path) -> None: assert_done_with_sums(second, "ООО Покупатель-1", BUYER_SUMS) -@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY) def test_s50_sap_in_out_and_glued_side(fixtures_dir: Path) -> None: """SAP «Сальдо вх/исх Дт/Кт» и сторона, приклеенная к группе («ДТВВ»); «ООО Дтвв-Сервис» — контрагент.""" outcome = parse_named(fixtures_dir, S50) @@ -930,7 +925,6 @@ def test_s52_form_stays_form_navision_is_osv_reference_is_other(fixtures_dir: Pa assert_done_with_sums(one_document(outcome), ALFA, ALFA_SUMS) -@pytest.mark.xfail(strict=True, reason=PENDING_DICTIONARY) def test_s53_xls_balance_by_side(fixtures_dir: Path) -> None: """s49 в xls (без объединений): те же колонки и суммы.""" outcome = parse_named(fixtures_dir, S53) diff --git a/api/tests/unit/parser/test_detect.py b/api/tests/unit/parser/test_detect.py index 87ec004..a62706c 100644 --- a/api/tests/unit/parser/test_detect.py +++ b/api/tests/unit/parser/test_detect.py @@ -282,6 +282,25 @@ def test_one_row_header_with_sides_in_group_cells(rules: Rules) -> None: assert [first_data_row(sheet, header)] == [3] +def test_side_without_word_boundary_only_in_group_cell(rules: Rules) -> None: + """#81, SAP НМЖК: сторона приклеена к названию группы («Сальдо НачальноеДТВВ», «ОборотыДтВВ»); «Дтвв» в тексте + контрагента ниже — не сторона и колонок не сдвигает.""" + sheet = sheet_from_rows( + [ + [TITLE], + ["Счет", "Сальдо НачальноеДТВВ", "Сальдо НачальноеКТВВ", "ОборотыДтВВ", "ОборотыКтВВ"] + + ["Сальдо КонечноеДТВВ", "Сальдо КонечноеКТВВ"], + [L(0), *at({1: "60", 2: 10, 7: 10})], + [L(1), *at({1: "ООО Дтвв-Сервис", 2: 10, 7: 10})], + ] + ) + + header = find_table_header(sheet, rules) + + assert header.columns == ColumnMap(2, 3, 4, 5, 6, 7, frozenset()) + assert (header.first_row, header.last_row) == (2, 2) + + def test_unmerged_group_titles_take_subheaders_up_to_the_next_group(rules: Rules) -> None: sheet = sheet_from_rows( [ diff --git a/api/tests/unit/parser/test_rules.py b/api/tests/unit/parser/test_rules.py index c85fc3a..81cba57 100644 --- a/api/tests/unit/parser/test_rules.py +++ b/api/tests/unit/parser/test_rules.py @@ -330,10 +330,16 @@ def test_sheet_title_is_not_a_turnover_header(rules: Rules) -> None: def test_initial_balance_synonym_is_not_in_the_default_dictionary(rules: Rules) -> None: # «Нач. остаток» добавляет сценарий «новый синоним без релиза кода» (R-8): в штатном словаре его быть не должно - for text in ("Нач. остаток", "Начальный остаток", "Остаток на начало периода"): + for text in ("Нач. остаток", "Начальный остаток"): assert not any(p.search(text) for p in rules.headers.groups["opening"]), text +@pytest.mark.parametrize("text", ["остаток на начало периода", "остаток по дебету на начало", "сальдо вх дт"]) +def test_other_systems_opening_synonyms(rules: Rules, text: str) -> None: + """#81, AURORA-1719: написания группы «начало» выгрузок КТЗ и SAP (текст — нормализованный).""" + assert any(p.search(text) for p in rules.headers.groups["opening"]), text + + @pytest.mark.parametrize("text", ["Дебет", "Дт", "Д-т", "дебет, руб."]) def test_debit_synonyms(rules: Rules, text: str) -> None: assert any(p.search(text) for p in rules.headers.debit)