From 831180847289ff76fb76c72389dc5ceb7c674829 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 13:30:34 +0400 Subject: [PATCH] =?UTF-8?q?fix(osv):=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D1=81=20=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=20A=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20rebas?= =?UTF-8?q?e=20=E2=80=94=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0,=20golden-=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA,=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=20=D1=81=D1=82=D1=8B=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D1=80=D0=BE=D0=B9=20=C2=AB=D0=98=D1=82=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=C2=BB=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 +- api/tests/synthetic/test_generate.py | 2 +- api/tests/synthetic/test_synthetic_files.py | 4 +- api/tests/unit/parser/test_osv_flat_form.py | 48 +++++++++++++++++++ openspec/changes/osv-flat-form/design.md | 3 +- openspec/changes/osv-flat-form/proposal.md | 4 +- .../osv-flat-form/specs/osv-parsing/spec.md | 27 +++++++---- 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d7b81b8..d9a2abc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,7 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote обращения к ячейке константами. Значения enum контракта вычисляются из `src.contract` и под линтер не попадают. - Каждая запись словаря должна срабатывать хотя бы на одном реальном или синтетическом файле (`tests/fixtures/test_rules_coverage.py`); мёртвый синоним удаляется, пробел в фикстурах закрывается синтетикой. -- Golden-снимки (`../fixtures/golden/expected_osv_parse/`, 131 файл: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 36 синтетических) — регрессия, а не +- Golden-снимки (`../fixtures/golden/expected_osv_parse/`, 136 файлов: 72 реальных + 23 пакета ДКБ (`osv-dkb/**` до 1 МиБ, снимки в `osv-dkb/`) + 41 синтетический) — регрессия, а не оракул; правятся только `make golden-update` с просмотром diff и отдельным коммитом в `fixtures`. Корректность проверяют property-тест и ручная сверка «Итого». - Деньги — `Decimal`, `float` в домене нет; `needs_check` — только по контрольным суммам, структуру под сходимость не diff --git a/api/tests/synthetic/test_generate.py b/api/tests/synthetic/test_generate.py index eddc7d5..6c78d51 100644 --- a/api/tests/synthetic/test_generate.py +++ b/api/tests/synthetic/test_generate.py @@ -26,7 +26,7 @@ def test_generation_is_reproducible_byte_for_byte(tmp_path: Path) -> None: assert left.read_bytes() == right.read_bytes(), left.name -def test_generated_set_is_s01_to_s32_and_s40_to_s44_with_one_xlsb(tmp_path: Path) -> None: +def test_generated_set_is_s01_to_s36_and_s40_to_s44_with_one_xlsb(tmp_path: Path) -> None: names = [path.name for path in generate_all(tmp_path)] assert names == list(FILE_NAMES) diff --git a/api/tests/synthetic/test_synthetic_files.py b/api/tests/synthetic/test_synthetic_files.py index e375732..0ede05d 100644 --- a/api/tests/synthetic/test_synthetic_files.py +++ b/api/tests/synthetic/test_synthetic_files.py @@ -162,7 +162,7 @@ EXPECTED: dict[str, Expect] = { "s34_indicator_column_qty_val_diff.xlsx": Expect("outline", 2025, "Q3", False), "s35_currency_slices.xlsx": Expect("outline", 2025, "Q3", False), "s36_no_data_type_header.xlsx": Expect("outline", 2025, "Q3", False, data_type=None), - # плоская форма 1С (#59): в шапке нет признака данных (БУ/НУ), `data_type = None` (умолчание — трек A, AURORA-1613) + # плоская форма 1С (#59): в шапке нет признака данных (БУ/НУ), `header.data_type = None`; строки — BU (#58) # счёт в шапке «по счету: 76.Н.1», данные без оформления; период «за 2025 г.» "s40_flat_header_account.xlsx": Expect( "header_account", 2025, "Q4", True, data_type=None, organization='ООО "Товарное хозяйство"' @@ -260,7 +260,7 @@ def tree(doc: OsvDocument) -> list[tuple[str, int, int | None, tuple[Decimal | N def test_synthetic_set_is_complete(fixtures_dir: Path) -> None: - """В `fixtures/osv/synthetic/` лежат файлы набора s01…s36, s40…s44 (s13 — xlsb; допустима деградация: нет только его).""" + """В `fixtures/osv/synthetic/` лежат файлы набора s01…s36, s40…s44 (s13 — xlsb; без него допустимо).""" present = sorted(path.name for path in (fixtures_dir / SYNTHETIC_DIR).glob("s*") if path.is_file()) assert present == [name for name in EXPECTED_NAMES if name in present] missing = [name for name in EXPECTED_NAMES if name not in present] diff --git a/api/tests/unit/parser/test_osv_flat_form.py b/api/tests/unit/parser/test_osv_flat_form.py index ed45612..47c4710 100644 --- a/api/tests/unit/parser/test_osv_flat_form.py +++ b/api/tests/unit/parser/test_osv_flat_form.py @@ -13,6 +13,7 @@ from src.parser.rules import Rules, load_rules TIMEOUT_SECONDS = 60 HEADER_ACCOUNT_CODE = "OSV_HIERARCHY_BY_HEADER_ACCOUNT" +NU_ROWS = 3 # две строки НУ под контрагентами и итоговая НУ после «Итого» @pytest.fixture(scope="module") @@ -180,3 +181,50 @@ def test_flat_list_with_broken_total_needs_check(rules: Rules) -> None: messages = [note.message for note in document.warnings] assert any("≠ сумме 2 строк уровня 0" in message for message in messages), messages assert not any("корневого счёта нет" in message for message in messages) + + +def test_flat_list_with_indicator_column_and_second_total(rules: Rules) -> None: + """Стык треков A и B (#58, #59): колонка «Показатели», «Итого развернутое» перед «Итого» и строка НУ после неё. + + Показатели не становятся контрагентами (R-6, R-18), не попадают в тождество строки (R-19) и не сбивают сверку + «Итого» (она идёт по основной «Итого» с меткой БУ). + """ + rows: list[list[object]] = [ + [], + ["Оборотно-сальдовая ведомость по счету: 76"], + ["Контрагенты"], + ["за 2025 г."], + ['ООО "Тест"'], + [], + [ + "Субконто", + "Показатели", + M("Сальдо на начало периода", 2), + M("Обороты за период", 2), + M("Сальдо на конец периода", 2), + ], + [None, None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"], + ["Август - Агро ООО", "БУ", 1000, None, 500, 300, 1200, None], + [None, "НУ", 900, None, 400, 300, 1000, None], + ["АВИС ООО", "БУ", None, 200, 100, 400, None, 500], + [None, "НУ", None, 150, 100, 350, None, 400], + ["Итого развернутое", "БУ", 1000, 200, 600, 700, 1200, 500], + ["Итого", "БУ", 1000, 200, 600, 700, 1200, 500], + [None, "НУ", 900, 150, 500, 650, 1000, 400], + ] + + document, codes = _parse(rules, rows) + + assert document.status == "done", [note.message for note in document.warnings] + assert document.checks is not None + assert document.checks.total_row_matches_root is True + indicators = [item for item in document.rows if item.data_type == "NU"] + assert len(indicators) == NU_ROWS + assert {item.counterparty_name for item in indicators} == {None} + assert {item.group_type for item in indicators if item.group_type != "TOTAL"} == {"OTHER"} + counterparties = [item.row_value for item in document.rows if item.group_type == "COUNTERPARTY"] + assert counterparties == ["Август - Агро ООО", "АВИС ООО"] + assert [item.group_type for item in document.rows if item.group_type == "TOTAL" and item.data_type == "BU"] == [ + "TOTAL", + "TOTAL", + ] diff --git a/openspec/changes/osv-flat-form/design.md b/openspec/changes/osv-flat-form/design.md index 4125b34..6b027dc 100644 --- a/openspec/changes/osv-flat-form/design.md +++ b/openspec/changes/osv-flat-form/design.md @@ -105,7 +105,8 @@ r11 Август - Агро ООО | … суммы … ← с - [OQ-158] Контрагенты по плану счетов на 76 и прочих расчётных счетах могут включать физлиц: «Осталось N» в сопоставлении вырастет. Смягчение: предупреждение `OSV_ROWS` на документе; список уже сужен (без 71/73, R-9). -- [Трек A] До слияния трека A строки без «Выводимые данные» — `data_type = null`; ядро их в свод не берёт. +- [Трек A] Слит в main (#58): строки без «Выводимые данные» — `data_type = BU` с предупреждением, `header.data_type` + остаётся `null`; golden s40–s44 обновлены при rebase (O-3 ledger). - [Golden] Правило расчётных счетов срабатывает только при схеме из одной ACCOUNT без названия вида; у реальных файлов и пакета ДКБ вид назван в шапке, поэтому снимки `osv/**` и `osv-dkb/**` не изменились (OTHER → COUNTERPARTY не случилось). Эффект проверен на файлах контура. diff --git a/openspec/changes/osv-flat-form/proposal.md b/openspec/changes/osv-flat-form/proposal.md index 274b563..eaf1872 100644 --- a/openspec/changes/osv-flat-form/proposal.md +++ b/openspec/changes/osv-flat-form/proposal.md @@ -47,8 +47,8 @@ arm-ka-docs (`05-domain-rules.md` §1.1, §1.3, §1.4, `07-open-questions.md`), ## Non-goals - Валюта («Валюта <код>», «В валюте»), колонка «Показатели», `data_type` строки при молчащей шапке — трек A (AURORA-1613 - + валютная часть 1661). До его слияния строки плоских файлов без «Выводимые данные» приходят с `data_type = null`, и - ядро не берёт их в свод (`SUMMARY_DATA_TYPES = (BU, SUM)`): файлы распознаются, но в сводную ОСВ не попадают. + + валютная часть 1661), слит в main (#58). После его слияния строки плоских файлов без «Выводимые данные» получают + `data_type = BU` с предупреждением `OSV_HEADER`, `header.data_type` остаётся `null`; в свод они попадают. - «Учредители» на 80 (именованная группировка `OTHER`, O-29) — не меняется (R-8). - Вид субконто «Организации» (множественное число) — синоним COUNTERPARTY в `osv_groups.yaml` (R-10, R-17), единственное «Организация» — собственная организация учёта, не вид субконто; прочие неизвестные виды в схему не diff --git a/openspec/changes/osv-flat-form/specs/osv-parsing/spec.md b/openspec/changes/osv-flat-form/specs/osv-parsing/spec.md index a94cf4e..7afcd4b 100644 --- a/openspec/changes/osv-flat-form/specs/osv-parsing/spec.md +++ b/openspec/changes/osv-flat-form/specs/osv-parsing/spec.md @@ -106,10 +106,12 @@ 2. строки ИНН, склеенной с этой строкой (требование «Строки ИНН рядом с контрагентом»); 3. конца текста названия: 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН» (`counterparty_inn_suffix`; «Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)»). -Контрольная сумма ИНН из колонки и из названия MUST NOT проверяться. Строки «Вал.», «Кол.», трёхбуквенного кода валюты -SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST NOT занимать позицию в `group_schema` и MUST NOT -наследовать `counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки, R-19); их суммы остаются в -шести полях. Остальные строки получают `data_type` шапки. Все строки, включая TOTAL, SHALL присутствовать в `rows` +Контрольная сумма ИНН из колонки и из названия MUST NOT проверяться. Строки-показатели (требование «Строки-показатели +и колонка «Показатели»») SHALL получать `group_type = OTHER`, `data_type` по метке, MUST NOT занимать позицию в +`group_schema` и MUST NOT наследовать `counterparty_name`/`counterparty_inn` (`account_code_raw` наследуется от ветки +родителя, R-19); их суммы остаются в шести полях. Строка с меткой «БУ» в колонке «Показатели» получает `data_type = BU`. +Остальные строки получают `data_type` шапки, а если шапка вид данных не называет — `BU` и одно предупреждение `OSV_HEADER` +на лист «вид данных в шапке не указан — строки без метки приняты как БУ» (`header.data_type` остаётся `null`). Все строки, включая TOTAL, SHALL присутствовать в `rows` (`ASSUMPTION(OQ-01)`: хранить ли «Итого», решает ядро); склеенные строки ИНН в `rows` MUST NOT попадать. #### Scenario: Подразделение, контрагент с ИНН, физлицо @@ -138,6 +140,15 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST - **THEN** `group_type = OTHER`, `data_type = CURRENCY` и `QUANTITY`, `counterparty_name`/`counterparty_inn` равны `null`, `account_code_raw` совпадает с родителем, строки исключены из проверки «сумма детей = родитель», статус `done` +#### Scenario: Шапка без вида данных +- **WHEN** разбирается синтетическая ОСВ `s36` (шапка без «Выводимые данные», колонки «Показатели» нет) +- **THEN** `header.data_type = null`, у всех строк данных и «Итого» `data_type = BU`, в `warnings` одно `OSV_HEADER` «вид + данных в шапке не указан — строки без метки приняты как БУ», статус `done` + +#### Scenario: «Валюта <код>» — обычная строка +- **WHEN** в синтетической ОСВ `s35` под контрагентом строка «Валюта USD» с рублёвыми суммами +- **THEN** `group_type = OTHER`, `data_type` шапки, строка участвует в сверке «сумма детей = родитель» + #### Scenario: Контрагенты расчётного счёта без названия вида - **WHEN** разбирается синтетическая ОСВ `s43` (лист как ОСВОКТ-24 «60 сч.»: колонка «Субконто», строка «60.01», под ней контрагенты с отступом) либо `s41` @@ -150,8 +161,8 @@ SHALL получать `group_type = OTHER`, `data_type` CURRENCY/QUANTITY, MUST turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по каждому родителю суммы = Σ детей с тем же `data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто, -а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки CURRENCY/QUANTITY в суммирование не входят и сами -родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0) либо сумма строк уровня 0; основная «Итого» — первая строка TOTAL, текст которой не «Итого развернутое» (`total_row_secondary`), в каком бы порядке они ни стояли. Если корневой ACCOUNT нет, а строки уровня 0 есть (плоский список), «Итого» SHALL сверяться с их суммой (те же допуски R-21); если строк данных нет (пустая ОСВ, R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в +а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка +«Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0) либо сумма строк уровня 0; основная «Итого» — первая строка TOTAL без признака показателя, текст которой не «Итого развернутое» (`total_row_secondary`), в каком бы порядке они ни стояли; итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если корневой ACCOUNT нет, а строки уровня 0 есть (плоский список), «Итого» SHALL сверяться с их суммой (те же допуски R-21); если строк данных нет (пустая ОСВ, R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в `warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root, balance_identity_violations, parent_sum_violations}`. Результат проверки SHALL записываться в документ листа (контракт v2.0): `status` листа — `done`, когда контрольные суммы §1.4 сошлись (и строки ИНН без нарушений), иначе `needs_check` (прежние условия §1.6, применённые к листу); любое @@ -167,9 +178,9 @@ balance_identity_violations, parent_sum_violations}`. Результат про оборотами) и единственная итоговая строка листа проверяются тождеством как обычные; основная «Итого» проверяется как прежде. -Исключение — **счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 91 и 99 строки аналитики несут +Исключение — **счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 90, 91 и 99 строки аналитики несут только обороты, сальдо закрыто на субсчёт закрытия (91.09). Список счетов — данные `rules/osv_checks.yaml` -(`accounts_without_analytic_balance`, начальный состав `91`, `99`; 90 не входит). Строка относится к такому счёту, если +(`accounts_without_analytic_balance`, состав `90`, `91`, `99`; счёт 90 добавлен решением владельца 19.10.2026). Строка относится к такому счёту, если код ближайшего счёта её ветки (`account_code_raw`) совпадает с записью списка либо является её субсчётом (`91`, `91.01`, но не `910`). Для таких строк тождество строки SHALL NOT проверяться; сверка «сумма детей = родитель» сохраняется: обороты — всегда по колонкам, сальдо — только у родителя с дочерними субсчетами (корень → субсчета), а у