diff --git a/README.md b/README.md index d0250f8..d530c1e 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо ### Строки-показатели и колонка «Показатели» (§1.3, AURORA-1613, AURORA-1661) Колонка «Показатели» (`service_columns.indicator`; заголовок часто «Показа-тели») несёт вид суммы строки: «БУ» — обычная -строка данных (`data_type = BU`), «НУ», «Вал.», «Кол.» — показатели (`CURRENCY`/`QUANTITY` по словарю -`data_types.yaml`: `indicator_column`), «ПР», «ВР» (`indicator_column_untyped`) и неизвестная метка — показатель с +строка данных (`data_type = BU`), «НУ», «Вал.», «Кол.» — показатели (`NU`/`CURRENCY`/`QUANTITY` по словарю +`data_types.yaml`: `indicator_column`), «ПР», «ВР» (`indicator_column_untyped`, `data_type = null`) и неизвестная метка — показатель с `data_type = null` (неизвестная — ещё и предупреждение `OSV_ROWS`). Показатель — строка с признаком `indicator`: он не входит в суммирование «сумма детей = родитель» и не проверяется как родитель, его родитель в дереве — предыдущая строка данных (`indicators.py`, вне каскада иерархии); `row_value` строки с меткой колонки — сама метка, колонка остаётся в @@ -170,13 +170,13 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо ### Контрольные суммы (§1.4) -Допуск ±0.01, пустое значение = 0. Строки «Вал.», «Кол.» и код валюты (`data_type = CURRENCY | QUANTITY`) в суммирование -не входят. Нетривиальное правило — сверка «сумма детей = родитель»: **обороты** сверяются по колонкам, **сальдо** — по +Допуск ±0.01, пустое значение = 0. Строки-показатели (признак строки: НУ/ПР/ВР/Кол./Вал./«В валюте»/код валюты, метка колонки «Показатели» или +текст) в суммирование не входят. Нетривиальное правило — сверка «сумма детей = родитель»: **обороты** сверяются по колонкам, **сальдо** — по колонкам либо, если у родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто `дебет − кредит`: 1С показывает счёт нетто, а субсчета и контрагентов — развёрнуто (решение R-21, design №31). Остаточный риск: перестановка сторон при совпавшем нетто родителя этой проверкой не ловится — её ловят тождество строки и «Итого = корень». -**«Итого».** Основная итоговая строка — первая TOTAL листа; она сверяется с первым корнем ACCOUNT либо, если корней +**«Итого».** Основная итоговая строка — первая TOTAL, не являющаяся показателем; она сверяется с первым корнем ACCOUNT либо, если корней уровня 0 несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03), с суммой строк уровня 0 (OQ-106, OQ-133; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя выше). Вторая итоговая строка сразу за «Итого» («Итого развернутое», `total_row_secondary`) — тоже `TOTAL`, в сверку не входит. Строка сумм diff --git a/api/src/parser/indicators.py b/api/src/parser/indicators.py index 2141dd9..13fc7ac 100644 --- a/api/src/parser/indicators.py +++ b/api/src/parser/indicators.py @@ -46,7 +46,9 @@ def read_label(sheet: Sheet, row: int, text: str, header: TableHeader, rules: Ru return RowLabel(indicator=False, data_type="BU") if data_type is not None or any(p.search(label) for p in rules.data_types.indicator_column_untyped): return RowLabel(indicator=True, data_type=data_type, text=label) - warning = f"строка {row}: метка «{label}» в колонке «Показатели» не распознана — строка вне проверок" + warning = ( + f"строка {row}: метка «{label}» в колонке «Показатели» не распознана — строка не участвует в проверках" + ) return RowLabel(indicator=True, data_type=None, text=label, warning=warning) data_type = row_data_type(text, rules, frozenset(rules.data_types.currency_codes)) return RowLabel(indicator=data_type is not None, data_type=data_type) diff --git a/api/tests/unit/parser/test_checks.py b/api/tests/unit/parser/test_checks.py index f86a32c..6b7650c 100644 --- a/api/tests/unit/parser/test_checks.py +++ b/api/tests/unit/parser/test_checks.py @@ -513,6 +513,23 @@ def test_document_of_nu_rows_without_indicator_is_checked_as_usual() -> None: assert parent_sums(rows) == [7] +def test_indicator_with_same_data_type_as_parent_is_excluded_by_flag() -> None: + """Признак — флаг, а не `data_type`: показатель НУ под родителем НУ не суммируется, не родитель и не «Итого».""" + rows = [ + row(7, ("10", None, None, None, "10", None), group_type="ACCOUNT", data_type="NU"), + row(8, ("10", None, None, None, "10", None), level=1, parent=7, data_type="NU"), + row( + 9, ("5", None, None, None, "5", None), level=1, parent=7, group_type="OTHER", data_type="NU", indicator=True + ), + row(10, ("3", None, None, None, "3", None), level=2, parent=9, data_type="NU"), + row(11, ("15", None, None, None, "15", None), group_type="TOTAL", data_type="NU", indicator=True), + row(12, ("10", None, None, None, "10", None), group_type="TOTAL", data_type="NU"), + ] + + assert parent_sums(rows) == [] + assert total_matches_root(rows) is True + + def test_total_skips_leading_indicator_total() -> None: """Первая итоговая строка без признака — основная «Итого», даже если перед ней стоит итог-показатель.""" rows = [ diff --git a/api/tests/unit/parser/test_indicators.py b/api/tests/unit/parser/test_indicators.py index f11e505..e8b1412 100644 --- a/api/tests/unit/parser/test_indicators.py +++ b/api/tests/unit/parser/test_indicators.py @@ -59,6 +59,7 @@ def test_unknown_label_warns_with_row_number(rules: Rules) -> None: assert result.warning is not None assert f"строка {row}" in result.warning assert "Ед." in result.warning + assert "не участвует в проверках" in result.warning def test_text_currency_label_without_column(rules: Rules) -> None: diff --git a/openspec/changes/osv-indicator-column/design.md b/openspec/changes/osv-indicator-column/design.md index c097322..5bb9643 100644 --- a/openspec/changes/osv-indicator-column/design.md +++ b/openspec/changes/osv-indicator-column/design.md @@ -57,8 +57,8 @@ r31 (продолжение) | НУ | … ### Иерархия: показатели вне каскада, родитель — предыдущая строка данных Новый модуль `src/parser/indicators.py` (чтобы не пересекаться с треком B в `hierarchy.py`): -`attach_indicators(rows, levels, total_row) -> Levels`. В `OsvParser.parse_sheet` каскад `resolve_levels` получает -строки без показателей; затем каждому показателю (до «Итого») назначается родитель — ближайшая строка выше, которая не +`levels_with_indicators(rows, total_row, deadline) -> Levels`: функция сама запускает каскад `resolve_levels` на +строках без показателей (в `OsvParser.parse_sheet` вызывается она одна); затем каждому показателю (до «Итого») назначается родитель — ближайшая строка выше, которая не показатель, — и уровень `level(родителя) + 1`; показатель в начале таблицы без строки данных выше — уровень 0 без родителя. Итоговые строки-показатели — уровень 0 без родителя (как «Итого»). Стратегия иерархии (`Levels.strategy`) — стратегия каскада. @@ -71,8 +71,9 @@ r31 (продолжение) | НУ | … `_trailing_totals` продолжает серию итоговых строк и через строки с меткой показателя в колонке «Показатели» (r31 после r30): они остаются в таблице, классифицируются TOTAL с `data_type` метки и `indicator = True`; -`checks._total` берёт первую итоговую строку **не показатель**. Трек B меняет `_trailing_totals`/`_total` под -«Итого развернутое» перед «Итого» — при слиянии второй трек добавляет своё условие к версии первого. +`checks._total` берёт первую итоговую строку **не показатель**. Трек B добавляет `_leading_totals` и меняет +`_total` под «Итого развернутое» перед «Итого» (`_trailing_totals` не трогает) — при слиянии второй трек добавляет +своё условие к версии первого. ### Вид данных по умолчанию @@ -101,6 +102,12 @@ r31 (продолжение) | НУ | … | 19.10.2026 | Умолчание BU — только строкам без собственного вида; `header.data_type` остаётся `null` | Шапка действительно молчит; ядру нужен вид строки, не шапки | Предупреждение на листах без «Выводимые данные» (были без него) | | 19.10.2026 | Синтетика s33–s36 (R-1: s40–s44 заняты треком B #59, s37–s39 резерв) | Рулинг владельца R-1: параллельные треки не конфликтуют по именам файлов набора | Пропуск номеров в наборе | | 19.10.2026 | Новый модуль `indicators.py` вместо правки `hierarchy.resolve_levels` | Трек B меняет каскад иерархии параллельно | Ещё один модуль конвейера | +| 19.10.2026 | T1.1: в колонке «Показатели» метка «Вал» без точки = CURRENCY (`^вал\.?$`), хотя в тексте строки (`row`) «Вал» без точки — не метка (материал, AURORA-1663) | В колонке меток закрытый набор | Ложный показатель для метки «Вал» вместо БУ | +| 19.10.2026 | T4.1: любая метка колонки «Показатели», кроме «БУ», у строки сразу после «Итого» делает её итоговой строкой-показателем (включая неизвестную метку) | Показатели не должны превращаться в «хвост ниже таблицы»; предупреждение о неизвестной метке выдаёт `read_label` | Неизвестная метка у реальной строки ниже таблицы будет втянута в таблицу | +| 19.10.2026 | R-2: трейлер коммитов «Claude Sonnet 5.5» вместо «Claude Opus 5.5» из плана | Атрибуция сессии | Правка трейлеров | +| 19.10.2026 | R-3: Task 3 плана (haiku) исполнен sonnet | Проза с рефакторингом | Стоимость | +| 19.10.2026 | R-4: мелкие находки T6.1/T6.3 исправлены до записи golden, а не отложены | Golden пишется один раз на исправленном коде | Лишний коммит | +| 19.10.2026 | R-5: golden s34 обновлён владельцем вручную после отказа классификатора в `make golden-update` | Снимок s34 — собственный новый снимок ветки; изменилась одна строка (текст предупреждения о неизвестной метке по I-2) | Ручное обновление без агентского ревью diff; diff проверен контроллером: одна строка | Решения владельца 19.10.2026 (чат): п. 1 колонка «Показатели» как метки §1.3, НУ без своих проверок; п. 2 «В валюте» → CURRENCY, «Валюта <код>» — строка OTHER в рублях, код валюты не хранится; п. 3 BU по умолчанию с предупреждением — по 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 118af58..042624e 100644 --- a/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md +++ b/openspec/changes/osv-indicator-column/specs/osv-parsing/spec.md @@ -26,20 +26,20 @@ таблицы есть числа»; сверка «Итого = корень» берёт первую итоговую строку, которая не показатель. #### Scenario: Колонка «Показатели» БУ/НУ -- **WHEN** разбирается синтетическая ОСВ `s40` (счёт 02, шапка без «Выводимые данные», колонка «Показа-тели», на каждую +- **WHEN** разбирается синтетическая ОСВ `s33` (счёт 02, шапка без «Выводимые данные», колонка «Показа-тели», на каждую строку — «БУ» и «НУ», название объединено на две строки, «Итого» БУ и НУ) - **THEN** строки «БУ» — `data_type = BU`, строки «НУ» — `data_type = NU`, `row_value = "НУ"`, `group_type = OTHER`, родитель — строка «БУ» над ней; `parent_sum_violations = []`, `total_row_matches_root = true`, две строки TOTAL (BU и NU), предупреждения «ниже таблицы» и «вид данных … приняты как БУ» нет, статус `done` #### Scenario: Количество, валюта и разницы в колонке «Показатели» -- **WHEN** разбирается синтетическая ОСВ `s41` (колонка «Показатели» с метками «Кол.», «Вал.», «ПР», «ВР» и одной +- **WHEN** разбирается синтетическая ОСВ `s34` (колонка «Показатели» с метками «Кол.», «Вал.», «ПР», «ВР» и одной неизвестной меткой) - **THEN** «Кол.» — QUANTITY, «Вал.» — CURRENCY, «ПР»/«ВР»/неизвестная — `data_type = null`, все — показатели вне сверки родителя; одно предупреждение `OSV_ROWS` о нераспознанной метке; статус `done` #### Scenario: «В валюте» под «Валюта <код>» -- **WHEN** разбирается синтетическая ОСВ `s42` (под контрагентом пары «Валюта USD» / «В валюте», «Валюта EUR» / +- **WHEN** разбирается синтетическая ОСВ `s35` (под контрагентом пары «Валюта USD» / «В валюте», «Валюта EUR» / «В валюте») - **THEN** «В валюте» — CURRENCY, родитель — «Валюта …» над ней; контрагент = Σ строк «Валюта …» по рублям; статус `done` @@ -104,12 +104,12 @@ `account_code_raw` совпадает с родителем, строки исключены из проверки «сумма детей = родитель», статус `done` #### Scenario: Шапка без вида данных -- **WHEN** разбирается синтетическая ОСВ `s43` (шапка без «Выводимые данные», колонки «Показатели» нет) +- **WHEN** разбирается синтетическая ОСВ `s36` (шапка без «Выводимые данные», колонки «Показатели» нет) - **THEN** `header.data_type = null`, у всех строк данных и «Итого» `data_type = BU`, в `warnings` одно `OSV_HEADER` «вид данных в шапке не указан — строки без метки приняты как БУ», статус `done` #### Scenario: «Валюта <код>» — обычная строка -- **WHEN** в синтетической ОСВ `s42` под контрагентом строка «Валюта USD» с рублёвыми суммами +- **WHEN** в синтетической ОСВ `s35` под контрагентом строка «Валюта USD» с рублёвыми суммами - **THEN** `group_type = OTHER`, `data_type` шапки, строка участвует в сверке «сумма детей = родитель» ### Requirement: Контрольные суммы определяют статус