From 1d2a46f3e7cbb1c01d47a16804519f713e765cbd Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 12:52:03 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D1=87=D1=91=D1=82=2090=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B5=20=D1=81=D1=87=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B1=D0=B5=D0=B7=20=D1=81=D0=B0=D0=BB=D1=8C=D0=B4?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B5=20(OQ-132,=20O-1,=20#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 4 ++-- README.md | 8 ++++---- api/rules/README.md | 2 +- api/rules/osv_checks.yaml | 6 ++++-- api/src/parser/checks.py | 2 +- api/tests/unit/parser/test_checks.py | 2 +- api/tests/unit/parser/test_rules.py | 12 ++++++++---- .../osv-indicator-column/specs/osv-parsing/spec.md | 4 ++-- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4aed44e..6759afb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,9 +85,9 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote по колонкам либо по нетто `дебет − кредит`, если у родителя сальдо свёрнуто (1С показывает счёт нетто, а субсчета и контрагентов — развёрнуто). Это решение зафиксировано в design №31; не ослаблять и не расширять без обсуждения. -**Счета без сальдо по аналитике (OQ-132, #11 п. 2):** список счетов (91, 99) — данные `api/rules/osv_checks.yaml`, не код; +**Счета без сальдо по аналитике (OQ-132, #11 п. 2):** список счетов (90, 91, 99) — данные `api/rules/osv_checks.yaml`, не код; тождество строки на них не проверяется, «сумма детей = родитель» (обороты всегда, сальдо — только корень → субсчета) и -«Итого» сохраняются. Счёт 90 в список не входит. Не расширять список «чтобы файл прошёл» — только по решению владельца. +«Итого» сохраняются. Счёт 90 добавлен решением владельца 19.10.2026 (O-1 из #58). Не расширять список «чтобы файл прошёл» — только по решению владельца. **Настройки:** `PARSE_MAX_BYTES` (по умолчанию 50 МиБ; больше — `UNSUPPORTED_FORMAT`) и `PARSE_TIMEOUT_SECONDS` (120 с; кооперативный, → `TIMEOUT`) — `core/config.py`. Фазу чтения таймаут не прерывает: до библиотеки чтения действуют diff --git a/README.md b/README.md index d530c1e..25c550f 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо | `inn_rows.py` | строки ИНН рядом с контрагентом (восемь раскладок, контрольная сумма без подсказки шапки, противоречие подсказке, OQ-163); выполняется над деревом до классификации | | `indicators.py` | строки-показатели (§1.3): метка вида суммы из колонки «Показатели» или текста строки (`read_label`), привязка показателя к строке данных над ним (`levels_with_indicators`) | | `classify.py` | сбор строк таблицы (`collect_body_rows`: «Итого», свёрнутое сальдо) и тип группировки, счёт, контрагент, `data_type`, `extra` (`classify_rows`) | -| `checks.py` | контрольные суммы §1.4: тождество сальдо строки, «сумма детей = родитель», «Итого» = корень; счета без сальдо по аналитике (91, 99) — вне тождества строки (`osv_checks.yaml`) | +| `checks.py` | контрольные суммы §1.4: тождество сальдо строки, «сумма детей = родитель», «Итого» = корень; счета без сальдо по аналитике (90, 91, 99) — вне тождества строки (`osv_checks.yaml`) | | `osv.py` | оркестратор `OsvParser`: лимит размера → чтение (с лимитами чтения) → лист за листом → статус и метрики | | `deadline.py` | кооперативный таймаут: шаги сами вызывают `Deadline.check` (поток прервать нельзя) | | `errors.py` | ожидаемые сбои с кодом контракта (`UnreadableError`, `UnsupportedFormatError`, `InvalidFormatError`, `InvalidDocumentError`, `ParseTimeoutError`) | @@ -206,10 +206,10 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо нарастающий, ровно квартал — квартальный, остальное (скользящие 12 месяцев) — `period_year` = год конца, `period_quarter` и `is_cumulative` = `null` и предупреждение «не с начала года» (OQ-131: ядро такой документ к версии не привязывает). -**Счета без сальдо по аналитике (OQ-132).** У 91 и 99 строки аналитики несут только обороты, сальдо закрывается на +**Счета без сальдо по аналитике (OQ-132).** У 90, 91 и 99 строки аналитики несут только обороты, сальдо закрывается на субсчёт закрытия (91.09), поэтому тождество строки на них не проверяется; иначе каждая ОСВ 91 получала бы `needs_check`. -Список счетов — данные: `accounts_without_analytic_balance` в `api/rules/osv_checks.yaml` (начальный состав `91`, `99`; -90 не входит — внутри года у его субсчетов сальдо есть). Строка относится к счёту списка по коду ближайшего счёта её ветки: +Список счетов — данные: `accounts_without_analytic_balance` в `api/rules/osv_checks.yaml` (состав `90`, `91`, `99`; +счёт 90 добавлен решением владельца 19.10.2026, O-1 из #58). Строка относится к счёту списка по коду ближайшего счёта её ветки: код совпадает с записью или является её субсчётом (`91`, `91.01`, но не `910`). Сверка «сумма детей = родитель» остаётся: обороты — всегда, сальдо — только у родителя с дочерними субсчетами (корень → 91.01/91.02/91.09); у родителя с одной аналитикой сальдо не сверяется. «Итого» сверяется как обычно. diff --git a/api/rules/README.md b/api/rules/README.md index bcfd9f8..536b5ed 100644 --- a/api/rules/README.md +++ b/api/rules/README.md @@ -12,7 +12,7 @@ AST-линтер `tests/unit/parser/test_no_coordinates.py`. |---|---|---| | `osv_headers.yaml` | `sheet_markers`, `groups` (`opening`/`turnover`/`closing`), `debit`, `credit`, `service_columns` (только `inn`), `total_row`, `total_row_secondary` («Итого развернутое»), `column_captions` («Код»/«Наименование» под названием группировки), `counterparty_inn_suffix` / `inn_only_row` / `inn_kpp_cell` (ИНН в тексте названия, строкой выше, «ИНН/КПП» в ячейке; обязательные именованные группы `tail`, `inn`), `header_markers` (`account`, `period`, `data_type`, `currency`, `org_params`, `subkonto`) | признак листа ОСВ; группы сумм (начало / обороты / конец); «Дебет»/«Кредит»; колонка ИНН (прочие колонки таблицы попадают в `rows[].extra` под своим заголовком и в словаре не нужны); строка «Итого»; метки шапки | | `osv_groups.yaml` | `mapping`, `default` | название группировки из шапки -> `ACCOUNT` / `DEPARTMENT` / `COUNTERPARTY` / `CONTRACT` / `OTHER` (`default` — для всего прочего) | -| `osv_checks.yaml` | `accounts_without_analytic_balance` | коды счетов верхнего уровня без сальдо по аналитике (начальный состав `91`, `99`; OQ-132): тождество строки «начало + обороты = конец» на них не проверяется (субсчета включены, `910` — нет); сверки «сумма детей = родитель» по оборотам и «Итого» сохраняются. Не `Pattern`: записи — коды счетов, а не регулярные выражения | +| `osv_checks.yaml` | `accounts_without_analytic_balance` | коды счетов верхнего уровня без сальдо по аналитике (состав `90`, `91`, `99`; OQ-132, 90 добавлен 19.10.2026): тождество строки «начало + обороты = конец» на них не проверяется (субсчета включены, `910` — нет); сверки «сумма детей = родитель» по оборотам и «Итого» сохраняются. Не `Pattern`: записи — коды счетов, а не регулярные выражения | | `periods.yaml` | `rules` (`pattern`, `quarter`, `is_cumulative`), `year`, `end`, `months` (названия месяцев, группы `m01…m12`), `not_single_month` (тексты, где один месяц не задаёт границы периода: дата на число, квартал, «N месяцев»), `range` (диапазон месяцев с годами, группы `m1`, `y1`, `m2`, `y2`), `standalone` (период отдельной ячейкой без метки) | текст периода -> `period_quarter` и `is_cumulative` (§1.5, `ASSUMPTION(OQ-02)`); `year` — паттерн года, `end` — конец текста периода в строке шапки («г.», «год») | | `data_types.yaml` | `header`, `standalone_header` (тип данных отдельной ячейкой без метки), `row`, `currency_codes`, `currency_aliases` | «Выводимые данные» -> `BU` / `NU` / `SUM`; строки «Вал.» / «Кол.» -> `CURRENCY` / `QUANTITY`; коды валют ISO 4217 (строка, равная коду, — валютная); написания валюты в шапке (`руб.` -> `RUB`; ключ алиаса обязан быть в `currency_codes`) | diff --git a/api/rules/osv_checks.yaml b/api/rules/osv_checks.yaml index 61d634c..58056db 100644 --- a/api/rules/osv_checks.yaml +++ b/api/rules/osv_checks.yaml @@ -6,8 +6,10 @@ # - сверка «сумма детей = родитель» сохраняется: обороты сверяются всегда, сальдо — только у родителя, у которого # есть субсчета (корень со своими субсчетами); у родителя с одной лишь аналитикой сальдо не сверяется; # - «Итого» сверяется как обычно. -# Запись — код счёта верхнего уровня («91»); к нему относятся и его субсчета (91.01, 91.09), но не «910». Счёт 90 не -# входит: внутри года у его субсчетов сальдо обычно есть. +# Запись — код счёта верхнего уровня («91»); к нему относятся и его субсчета (91.01, 91.09), но не «910». +# Решение владельца 19.10.2026: счёт 90 добавлен в список (O-1 из #58): у его аналитики сальдо нет, при этом у родителей +# 90.xx.y сальдо на конец есть. accounts_without_analytic_balance: + - '90' - '91' - '99' diff --git a/api/src/parser/checks.py b/api/src/parser/checks.py index ff9566e..05b2501 100644 --- a/api/src/parser/checks.py +++ b/api/src/parser/checks.py @@ -16,7 +16,7 @@ (пустая ОСВ, R-13) — «Итого» должна быть пустой или нулевой; «Итого» не найдена (R-20) — сверка невозможна, результат `false`. Строки-показатели (валюта, количество, НУ) в сумму уровня 0 не входят: это другие единицы. -Счета без сальдо по аналитике (91, 99; список — `rules/osv_checks.yaml`, OQ-132): у них строки аналитики несут только +Счета без сальдо по аналитике (90, 91, 99; список — `rules/osv_checks.yaml`, OQ-132): у них строки аналитики несут только обороты, сальдо закрыто на субсчёт закрытия (91.09). Для строк такого счёта тождество п. 1 не проверяется; в п. 2 обороты сверяются как обычно, а сальдо — только у родителя, у которого есть дочерние субсчета (у родителя с одной лишь аналитикой сальдо детей пусто по определению); п. 3 («Итого») не меняется. diff --git a/api/tests/unit/parser/test_checks.py b/api/tests/unit/parser/test_checks.py index 6b7650c..f90f4bd 100644 --- a/api/tests/unit/parser/test_checks.py +++ b/api/tests/unit/parser/test_checks.py @@ -396,7 +396,7 @@ def test_run_checks_warning_lists_are_capped() -> None: assert "100," not in identity -# --- Счета без сальдо по аналитике (OQ-132): 91, 99 --- +# --- Счета без сальдо по аналитике (OQ-132): 90, 91, 99 --- NO_SALDO = OsvCheckRules(accounts_without_analytic_balance=["91", "99"]) diff --git a/api/tests/unit/parser/test_rules.py b/api/tests/unit/parser/test_rules.py index b096238..e8f0f0f 100644 --- a/api/tests/unit/parser/test_rules.py +++ b/api/tests/unit/parser/test_rules.py @@ -622,8 +622,8 @@ def test_currency_code_unknown(rules: Rules, text: str) -> None: # --- Счета без сальдо по аналитике (osv_checks.yaml, OQ-132) --- -def test_accounts_without_analytic_balance_are_91_and_99(rules: Rules) -> None: - assert rules.checks.accounts_without_analytic_balance == ["91", "99"] +def test_accounts_without_analytic_balance_are_90_91_and_99(rules: Rules) -> None: + assert rules.checks.accounts_without_analytic_balance == ["90", "91", "99"] @pytest.mark.parametrize( @@ -634,8 +634,12 @@ def test_accounts_without_analytic_balance_are_91_and_99(rules: Rules) -> None: (" 91.09 ", True), ("99", True), ("99.01.1", True), - ("90", False), - ("90.01", False), + ("90", True), + ("90.01", True), + ("90.01.1", True), + (" 90.09 ", True), + ("900", False), + ("901", False), ("910", False), ("9", False), ("", False), diff --git a/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md b/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md index 042624e..529813d 100644 --- a/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md +++ b/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md @@ -130,9 +130,9 @@ balance_identity_violations, parent_sum_violations}`. Результат про `OSV_XLSB_NO_OUTLINE`); строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Парсер MUST NOT подгонять структуру под сходимость. -Исключение — **счета без сальдо по аналитике** (решение владельца 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 проверяться; сверка «сумма детей = родитель» сохраняется: обороты — всегда по колонкам, сальдо — только у родителя с дочерними субсчетами (корень → субсчета), а у