217 lines
34 KiB
Markdown
217 lines
34 KiB
Markdown
# ОСВ 1С в плоской форме (AURORA-1661, структура) Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** ОСВ 1С по одному счёту в плоской форме («по счету: X», колонка «Субконто», без строки счёта, «Итого развернутое» перед «Итого») разбирается без `INVALID_FORMAT`, со счётом и организацией из шапки, а контрагенты расчётных счетов без названия вида получают `COUNTERPARTY`.
|
||
|
||
**Architecture:** Изменения в конвейере листа ОСВ tabular-parser: шапка (`header.py`) — код счёта с двоеточием, строка вида субконто, «План счетов»; итоговый блок (`classify.collect_body_rows`, `checks._total`); новая стратегия каскада `header_account` (`hierarchy.py`) и счёт шапки у корня ветки (`classify_rows`); правило расчётных счетов — данные `osv_groups.yaml`. Контракт результата не меняется.
|
||
|
||
**Tech Stack:** Python 3.11, pydantic v2, openpyxl, pytest; OpenSpec 1.13.
|
||
|
||
**Spec:** `openspec/changes/osv-flat-form/` (proposal.md, design.md, specs/osv-parsing/spec.md)
|
||
|
||
**Ветка:** `feat/59-osv-flat-form` во всех трёх worktree: `/Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/{tabular-parser,fixtures,docs}`. Issue — RSHB/arm-tabular-parser#59. Jira — AURORA-1661 (структурная часть).
|
||
|
||
## Global Constraints
|
||
|
||
- Каждая shell-команда начинается с `cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/<repo> &&`; git — только `git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/<repo>`; в основные checkout'ы (`/Users/andrey.epifancev/projects/awp-ca/tabular-parser` и т. п., ветка main) не заходить и не писать. Хук отказал — стоп, вернуться к контроллеру с текстом отказа.
|
||
- Код Python правится только Edit/Write, не `sed -i`/heredoc через Bash.
|
||
- Нет координат в `src/parser/**` (AST-линтер `tests/unit/parser/test_no_coordinates.py`): ни букв колонок, ни номеров строк, ни имён листов. Новые синонимы и списки — данные `api/rules/*.yaml`, не `if` в коде.
|
||
- Деньги — `Decimal`, `float` нет. `needs_check` — только по контрольным суммам; структуру под сходимость не подгонять.
|
||
- Контракт (`api/contracts/`, `src/contract/`) не меняется; `CONTRACT_VERSION` прежний.
|
||
- Синтетика этого плана — `s40`–`s44` (номера `s33`–`s39` — у параллельного трека A, не занимать).
|
||
- Golden (`../fixtures/golden/expected_osv_parse/`) правится только `make golden-update` с просмотром diff, коммит — в репозиторий fixtures отдельно от кода, сообщение с `(tabular-parser#59)`. Изменение снимка реального файла, не объяснённое задачей, — стоп и вопрос контроллеру.
|
||
- Реальные файлы контура (`/Users/andrey.epifancev/projects/awp-ca/result_dir.zip`, `/Users/andrey.epifancev/projects/awp-ca/ОСВ сч. 60 за 2 кв. 2026.xlsx`) в git не кладутся: только распаковка во временный каталог вне репозиториев для сверки.
|
||
- Трек A (`.wt/osv-indicators/`, AURORA-1613 + валюта 1661) владеет путём `data_type` (`rules/data_types.yaml`, `rules.RowDataType`, `classify.row_data_type`, `checks._INDICATOR_DATA_TYPES`, колонка «Показатели», умолчание BU) — эти места не трогать.
|
||
- Коммиты — по-русски, с `(#59)` в теме и строкой `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||
- Проверка задачи: `cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser && make check` зелёный (≈95 с; включает fixtures-тесты).
|
||
|
||
## Review Focus
|
||
|
||
1. «по счету:» без кода после двоеточия (код в соседней ячейке или нет вовсе) — `account_code_raw` пуст, работает фолбэк OQ-134 из строки таблицы с прежним предупреждением; тест в задаче 2.
|
||
2. Название и период в одной строке («…по счету 60 за 1 квартал 2025 г.») — строки вида субконто нет, организация по прежнему правилу (первая не-метка); тест в задаче 2.
|
||
3. На листе только «Итого развернутое», без обычной «Итого» — она единственная итоговая и сверяется как «Итого»; тест в задаче 3.
|
||
4. Счёт в шапке, строк-счетов нет, но есть outline (контрагент → договоры) — побеждает `outline`, `header_account` не используется, строки всё равно наследуют счёт шапки; тест в задаче 4.
|
||
5. Счёт ветки «760» или «600» при записи `76`/`60` в `counterparty_accounts` — не расчётный, строки OTHER; тест в задаче 5.
|
||
|
||
---
|
||
|
||
### Task 1: Синтетика s40–s44 и снимки текущего поведения
|
||
|
||
**Files:**
|
||
- Modify: `api/tests/synthetic/generate.py` (функции `_s40`…`_s44`, записи реестра `FILES`, docstring `main` — «s01…s44»)
|
||
- Create: `../fixtures/osv/synthetic/s40_flat_header_account.xlsx`, `s41_flat_chart_of_accounts.xlsx`, `s42_subaccounts_totals_reversed.xlsx`, `s43_subkonto_unnamed_60.xlsx`, `s44_subkonto_unnamed_10.xlsx`
|
||
- Create: снимки `../fixtures/golden/expected_osv_parse/synthetic/s40…s44.json` (`make golden-update`)
|
||
|
||
**Interfaces:**
|
||
- Produces: пять файлов синтетики; ожидания по ним задачи 2–5 проверяют разбором через `OsvParser`/`get_parser()`.
|
||
|
||
Содержимое листов (все — один лист, колонка A — текст, B–G — суммы «Сальдо на начало / Обороты за период / Сальдо на конец периода» × «Дебет | Кредит»; заголовок групп — объединения B:C, D:E, F:G; числа — `Decimal`; пусто — нет ячейки):
|
||
|
||
- `s40`: r2 «Оборотно-сальдовая ведомость по счету: 76.Н.1», r3 «Контрагенты», r4 «за 2025 г.», r5 «ООО "Товарное хозяйство"», r7 «Единица измерения: рубль (код по ОКЕИ 383)», r9 «Субконто» + группы, r10 подзаголовки; данные без outline и отступов: «Август - Агро ООО» 1000.00 | — | 500.00 | 300.00 | 1200.00 | —; «АВИС ООО» — | 200.00 | 100.00 | 400.00 | — | 500.00; «Агроаспект ООО» — | — | 50.00 | 50.00 | — | —; «Итого развернутое» 1000.00 | 200.00 | — | — | 1200.00 | 500.00; «Итого» 800.00 | — | 650.00 | 750.00 | 700.00 | —.
|
||
- `s41`: r2 «Оборотно-сальдовая ведомость по счету: 60», r3 «Организации», r4 «за 2 Квартал 2026 г. План счетов ООО Хреновская Нива», r5 «Субконто» + группы, r6 подзаголовки; «Автодор-Платные Дороги ООО» 820.21 | — | 5000.00 | 4991.00 | 829.21 | —; «АГРОПРОФИ ООО» — | 2763.79 | — | — | — | 2763.79; «Итого развернутое» 820.21 | 2763.79 | — | — | 829.21 | 2763.79; «Итого» — | 1943.58 | 5000.00 | 4991.00 | — | 1934.58.
|
||
- `s42`: r2 «Оборотно-сальдовая ведомость по счету: 76», r3 «за 2025 г.», r4 «ООО "Товарное хозяйство"», r6 «Единица измерения: рубль (код по ОКЕИ 383)», r8 «Субконто» + группы, r9 подзаголовки; «76.2» 1000.00 | — | — | 1000.00 | — | —; «76.5» — | 300.00 | 700.00 | 600.00 | — | 200.00; «76.Н» — | — | 5000.00 | 5000.00 | — | —; «76.Н.1» — | — | 5000.00 | 5000.00 | — | —; «Итого развернутое» 1000.00 | 300.00 | — | — | — | 200.00; «Итого» 700.00 | — | 5700.00 | 6600.00 | — | 200.00.
|
||
- `s43`: r1 «ООО "Октябрьское"», r2 «Оборотно-сальдовая ведомость по счету 60», r3 «Период: 9 месяцев 2024 г.», r5 «Субконто» + группы, r6 подзаголовки; «60.01» 34618000.00 | — | 4236500.00 | 38854500.00 | — | —; отступ 1: «ИП Знаменских А.И.» 34618000.00 | — | — | 34618000.00 | — | —; отступ 1: «ООО "ЭСТЕТ ОМСК"» — | — | 4236500.00 | 4236500.00 | — | —; «Итого» и затем «Итого развернутое» — обе 34618000.00 | — | 4236500.00 | 38854500.00 | — | —.
|
||
- `s44`: как `s43`, но счёт 10: r2 «…по счету 10», «10.01» и строки «Мука пшеничная», «Сахар-песок» с теми же суммами.
|
||
|
||
- [ ] **Step 1: Добавить `_s40`…`_s44` и записи реестра**, по образцу `_s31`/`_s32` (листы через `SheetSpec`/`put`, отступ — `CellSpec(..., indent=1)`).
|
||
- [ ] **Step 2: Сгенерировать файлы**
|
||
|
||
Run: `cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser/api && .venv/bin/python -m tests.synthetic.generate ../../fixtures/osv/synthetic` (если `.venv` нет — `cd .. && make venv`).
|
||
Expected: в выводе пути `s40…s44`; `git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures status` — пять новых файлов, прежние файлы синтетики без изменений (генератор детерминирован).
|
||
|
||
- [ ] **Step 3: Снимки текущего поведения**
|
||
|
||
Run: `cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser && make golden-update`
|
||
Expected: новые снимки только `synthetic/s40…s44.json`; у `s40`, `s41` — `status: "error"`, `INVALID_FORMAT` «иерархия строк не определена»; `s42` — `needs_check`, «Итого развернутое» — OTHER; `s43`, `s44` — контрагенты/материалы OTHER. Других изменений снимков нет.
|
||
|
||
- [ ] **Step 4: `make check` зелёный**
|
||
|
||
- [ ] **Step 5: Commit (два репозитория)**
|
||
|
||
```bash
|
||
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures add osv/synthetic golden/expected_osv_parse/synthetic
|
||
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/fixtures commit -m "Синтетика s40–s44: плоская форма ОСВ 1С, снимки текущего поведения (tabular-parser#59)"
|
||
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser add api/tests/synthetic/generate.py
|
||
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser commit -m "test: синтетика s40–s44 — плоская форма ОСВ 1С (#59)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Шапка — код счёта с двоеточием, строка вида субконто, «План счетов»
|
||
|
||
**Files:**
|
||
- Modify: `api/rules/osv_headers.yaml` (`header_markers.chart_of_accounts: ['\bплан\s+счетов\b']`)
|
||
- Modify: `api/src/parser/rules.py` (`HeaderMarker` + `"chart_of_accounts"`; `OsvGroupRules.known`)
|
||
- Modify: `api/src/parser/header.py` (`_account_code`, `_organization`, новый `_kind_row_cells`, `_variant1_groups`/`build_group_levels`)
|
||
- Test: `api/tests/unit/parser/test_header.py`, `api/tests/unit/parser/test_rules.py`
|
||
|
||
**Interfaces:**
|
||
- Produces: `OsvGroupRules.known(text: str) -> GroupType | None` — тип по явной записи `mapping`, `None` для `default`; `header._kind_row_cells(cells: list[_HeadCell], rules: Rules) -> list[_HeadCell]` — ячейки строго между строкой ячейки с меткой `account` и строкой периода (`_find_period`), если первая выше; иначе `[]`.
|
||
|
||
- [ ] **Step 1: Тесты (падают)** — в `test_header.py` через `sheet_from_rows`:
|
||
- `test_account_code_after_colon`: «Оборотно-сальдовая ведомость по счету: 76.Н.1» → `account_code_raw == "76.Н.1"`, `ACCOUNT_NOT_FOUND_WARNING` нет в `warnings`.
|
||
- `test_colon_without_code_falls_back_to_table_row` (Review Focus 1): «…по счету:» без кода → `account_code_raw == ""` и `ACCOUNT_NOT_FOUND_WARNING` в `warnings` (фолбэк делает `osv._with_table_account`).
|
||
- `test_kind_row_between_title_and_period_is_not_organization`: строки «…по счету: 76», «Контрагенты», «за 2025 г.», «ООО "Товарное хозяйство"» → `organization_name == 'ООО "Товарное хозяйство"'`.
|
||
- `test_organization_from_chart_of_accounts_tail`: «…по счету: 60», «Организации», «за 2 Квартал 2026 г. План счетов ООО Хреновская Нива» → `organization_name == "ООО Хреновская Нива"`, `period_quarter == "Q2"`, `period_year == 2026`.
|
||
- `test_title_and_period_in_one_row_keep_old_organization_rule` (Review Focus 2): существующая раскладка `_variant1` (`org` выше названия с периодом) → `organization_name == 'АО "Ромашка"'`.
|
||
- `test_known_kind_row_extends_single_account_schema`: шапка как у `s40`, заголовок «Субконто» → `build_group_schema(...)[0] == [ACCOUNT("Субконто"), COUNTERPARTY("Контрагенты")]`.
|
||
- `test_unknown_kind_row_is_not_added_to_schema`: строка «Организации» → схема `[ACCOUNT("Субконто")]`.
|
||
- в `test_rules.py`: `test_known_group_is_none_for_default` — `rules.groups.known("Контрагенты") == "COUNTERPARTY"`, `known("Организации") is None`, `known("Номенклатура") == "OTHER"`.
|
||
- [ ] **Step 2: Запуск** — `cd …/tabular-parser/api && .venv/bin/pytest tests/unit/parser/test_header.py tests/unit/parser/test_rules.py -q` → новые тесты FAIL.
|
||
- [ ] **Step 3: Реализация.**
|
||
- `_account_code`: хвост `_tail(...)` очистить от ведущих `:` и пробелов перед `_ACCOUNT_CODE.match`.
|
||
- `_organization`: сначала хвост метки `chart_of_accounts` в любой ячейке шапки (текст после совпадения, `strip(" ,;:")`, непустой); затем первая не-метка, исключая ячейки `_kind_row_cells`; затем `_title_tail_organization`. Ячейка периода, содержащая «План счетов», остаётся периодом (`_cut_period` режет на «г.»).
|
||
- `_is_structural_marker`: метка `chart_of_accounts` — структурная.
|
||
- Схема: если `_variant1_groups` дал ровно одну ACCOUNT, добавить по элементу текста `_kind_row_cells` (через запятую, `strip`), для которого `rules.groups.known(item)` не `None`: `_group(known, item)`; вариант 2 не меняется.
|
||
- [ ] **Step 4: Тесты PASS**, затем `make golden-update`: ожидаемо меняются только `synthetic/s40` и `s41` (если они ещё `error` — без изменений) и `s42` (`account_code_raw` «76» вместо «76.2», без предупреждения «взят из первой строки-счёта»). Изменение реального снимка — стоп (Global Constraints).
|
||
- [ ] **Step 5: `make check` зелёный; commit** — tabular-parser: `feat(osv): код счёта «по счету: X», строка вида субконто, «План счетов» в шапке (#59)`; fixtures (если снимки изменились): `Снимки s40–s42: шапка плоской формы (tabular-parser#59)`.
|
||
|
||
---
|
||
|
||
### Task 3: «Итого развернутое» перед «Итого»
|
||
|
||
**Files:**
|
||
- Modify: `api/src/parser/classify.py` (`collect_body_rows`, новый `_leading_totals`)
|
||
- Modify: `api/src/parser/checks.py` (`_total`, `total_matches_root`, `run_checks`)
|
||
- Modify: `api/src/parser/osv.py` (передать `rules.headers.total_row_secondary` в `run_checks`)
|
||
- Test: `api/tests/unit/parser/test_classify.py`, `api/tests/unit/parser/test_checks.py`
|
||
|
||
**Interfaces:**
|
||
- Produces: `collect_body_rows(...)` — тот же кортеж `(body, total_row, warnings)`, но `total_row` — **первая** строка итогового блока (вторые итоговые строки непосредственно перед основной «Итого» входят в блок); `run_checks(rows, exempt=None, *, secondary_totals: Sequence[Pattern] = ()) -> tuple[OsvChecks, list[OsvNote]]`; `total_matches_root(rows, secondary_totals: Sequence[Pattern] = ()) -> bool`.
|
||
|
||
- [ ] **Step 1: Тесты (падают)**
|
||
- `test_secondary_total_before_main_total_is_in_total_block` (`test_classify.py`): лист как `s42` → `total_row` = номер строки «Итого развернутое»; после `classify_rows` обе строки — `TOTAL`, `level == 0`, `parent is None`.
|
||
- `test_only_secondary_total_is_the_total` (Review Focus 3): на листе только «Итого развернутое» → она `TOTAL`, `total_row` — её номер.
|
||
- `test_total_check_uses_main_total_when_secondary_comes_first` (`test_checks.py`): строки TOTAL в порядке «Итого развернутое», «Итого»; суммы «Итого» = корень, «Итого развернутое» — нет → `total_matches_root(rows, secondary) is True`; без `secondary` (прежний вызов) — `False`.
|
||
- Разбор `s42` через `get_parser()`: `status == "done"`, `checks.parent_sum_violations == []`, `checks.balance_identity_violations == []`.
|
||
- [ ] **Step 2: Запуск — FAIL.**
|
||
- [ ] **Step 3: Реализация.** `_leading_totals(sheet, header, rows, total, rules) -> int` — идти от индекса основной «Итого» назад, пока текст строки (колонка текста) матчит `total_row_secondary`; вернуть номер самой верхней (или `total`). В `collect_body_rows` вернуть его как `total_row`; хвост — по-прежнему от последней из вторых строк после основной. `checks._total(rows, secondary)` — первая TOTAL, чей `body.text` не матчит ни один паттерн `secondary`, иначе первая TOTAL.
|
||
- [ ] **Step 4: PASS; `make golden-update`** — ожидаемо `synthetic/s42.json` (обе итоговые строки TOTAL, `status: done`); реальные снимки (пакет ДКБ с «Итого развернутое» после «Итого») не меняются.
|
||
- [ ] **Step 5: `make check`; commit** — `fix(osv): «Итого развернутое» перед «Итого» — итоговая строка, сверка по основной «Итого» (#59)`; fixtures — `Снимок s42: «Итого развернутое» перед «Итого» (tabular-parser#59)`.
|
||
|
||
---
|
||
|
||
### Task 4: Плоский список под счётом шапки
|
||
|
||
**Files:**
|
||
- Modify: `api/src/parser/hierarchy.py` (`strategy_header_account`, `resolve_levels`)
|
||
- Modify: `api/src/parser/osv_warnings.py` (`HIERARCHY_TEXTS["header_account"] = "плоский список под счётом шапки (header_account)"`)
|
||
- Modify: `api/src/parser/classify.py` (`classify_rows(..., header_account: str | None = None)`)
|
||
- Modify: `api/src/parser/checks.py` (`total_matches_root` без корня ACCOUNT)
|
||
- Modify: `api/src/parser/osv.py` (передать `header_account` в `resolve_levels` и `classify_rows`)
|
||
- Test: `api/tests/unit/parser/test_hierarchy.py`, `api/tests/unit/parser/test_checks.py`, Create `api/tests/unit/parser/test_osv_flat_form.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `collect_body_rows` и `run_checks(..., secondary_totals=...)` задачи 3.
|
||
- Produces: `strategy_header_account(rows: Sequence[BodyRow], deadline: Deadline | None = None) -> dict[int, int] | None` — `{row: 0}` для всех строк, `None`, если хоть одна `is_account`; `resolve_levels(rows, total_row, deadline, *, header_account: bool = False) -> Levels` — `header_account` пробуется после `account_pattern` и перед `sums`, только при `header_account=True`; `classify_rows(..., *, inns=None, unpaired=frozenset(), header_account: str | None = None)` — корневая ветка `_Branch(header_account, 0)`.
|
||
|
||
- [ ] **Step 1: Тесты (падают)**
|
||
- `test_header_account_strategy_flat_rows` (`test_hierarchy.py`): три строки без outline/отступа/счёта, `header_account=True` → `strategy == "header_account"`, все уровни 0, родители `None`.
|
||
- `test_header_account_not_used_without_header_code`: те же строки, `header_account=False` → `InvalidFormatError` (прежнее поведение).
|
||
- `test_header_account_not_used_with_account_rows`: строки «60.01», «ООО А» без оформления, `header_account=True` → `account_pattern`.
|
||
- `test_total_without_account_root_matches_level_zero_sum` (`test_checks.py`): три корня OTHER/COUNTERPARTY без ACCOUNT, «Итого» = их сумма (сальдо «Итого» свёрнуто нетто) → `True`; одна сумма испорчена на 1 → `False`; только «Итого» с нулями → `True` (R-13 прежний).
|
||
- `test_osv_flat_form.py::test_flat_list_s40` (разбор `s40` через `get_parser()` или `OsvParser.parse_sheet` на листе из `sheet_from_rows`): `status == "done"`, предупреждение `OSV_HIERARCHY_BY_HEADER_ACCOUNT`, у каждой строки данных `level == 0`, `parent_row_number is None`, `account_code_raw == "76.Н.1"`, `organization_name == 'ООО "Товарное хозяйство"'`.
|
||
- `test_flat_list_s41`: `status == "done"`, `account_code_raw` строк `"60"`.
|
||
- `test_outline_wins_over_header_account_and_rows_inherit_header_code` (Review Focus 4): счёт в шапке «60», строк-счетов нет, outline: «ООО А» (0) → «Договор 1» (1) → `strategy == "outline"`, у обеих строк `account_code_raw == "60"`.
|
||
- [ ] **Step 2: Запуск — FAIL.**
|
||
- [ ] **Step 3: Реализация** по Interfaces; в `osv.py`: `resolve_levels(body, total_row, deadline, header_account=bool(doc.account_code_raw))`, `classify_rows(..., header_account=doc.account_code_raw or None)`. `total_matches_root`: нет корня ACCOUNT — если `_level_zero(rows)` не пуст, `_sums_match(_values(total), _sum_of(roots))`, иначе прежняя проверка нулей.
|
||
- [ ] **Step 4: PASS; `make golden-update`** — ожидаемо `s40`, `s41` (`done`, `header_account`); реальные снимки — только если у файла есть строки до первой строки-счёта (получат `account_code_raw` шапки, R-4) — каждое такое изменение перечислить в отчёте; иные изменения — стоп.
|
||
- [ ] **Step 5: `make check`; commit** — `feat(osv): стратегия header_account — плоский список под счётом шапки (#59)`; fixtures — `Снимки: плоский список под счётом шапки (tabular-parser#59)`.
|
||
|
||
---
|
||
|
||
### Task 5: Контрагенты расчётных счетов без названия вида
|
||
|
||
> Перед задачей: если трек A (`feat/*` в `.wt/osv-indicators/tabular-parser`, AURORA-1613) уже слит в main — `git -C …/tabular-parser fetch origin && git -C …/tabular-parser rebase origin/main`, `make golden-update`, разрешить конфликты в `classify._classify_row` (трек A меняет определение `data_type` в той же функции; сохранить обе правки). Не слит — работать как есть.
|
||
|
||
**Files:**
|
||
- Modify: `api/rules/osv_groups.yaml` (`counterparty_accounts: ['58', '60', '62', '66', '67', '71', '73', '75', '76', '80']` с комментарием про AURORA-1661 и OQ-158)
|
||
- Modify: `api/src/parser/rules.py` (`OsvGroupRules.counterparty_accounts: list[str] = []`, проверка кодов как у `OsvCheckRules`, `is_counterparty_account`; общий хелпер сопоставления «код или его субсчёт» для обоих классов)
|
||
- Modify: `api/src/parser/classify.py` (`_classify_row`, `_Kind`, `ClassifiedRow.counterparty_by_account: bool = False`)
|
||
- Modify: `api/src/parser/osv.py` (заметка `OSV_ROWS`)
|
||
- Test: `api/tests/unit/parser/test_classify.py`, `api/tests/unit/parser/test_rules.py`, `api/tests/unit/parser/test_osv_flat_form.py`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `classify_rows(..., header_account=...)` задачи 4.
|
||
- Produces: `OsvGroupRules.is_counterparty_account(account_code: str | None) -> bool` (`"76"` → `76`, `76.2`, `76.Н.1`; не `760`); `ClassifiedRow.counterparty_by_account` — строка получила COUNTERPARTY по плану счетов.
|
||
|
||
- [ ] **Step 1: Тесты (падают)**
|
||
- `test_rules.py::test_counterparty_accounts_match_code_and_subaccounts`: `is_counterparty_account("76.Н.1")`, `("60.01")` — `True`; `("760")`, `("600")`, `("10.01")`, `(None)` — `False` (Review Focus 5).
|
||
- `test_classify.py::test_unnamed_first_level_on_settlement_account_is_counterparty`: схема `[ACCOUNT("Субконто")]`, «60.01» → «ИП Знаменских А.И.» (уровень 1) → COUNTERPARTY, `group_name is None`, `counterparty_name == "ИП Знаменских А.И."`, `counterparty_by_account is True`.
|
||
- `test_unnamed_second_level_stays_other`: под таким контрагентом строка уровня 2 → OTHER.
|
||
- `test_named_other_group_is_not_reclassified`: схема `[ACCOUNT, OTHER("Работники организаций")]` на 71 → OTHER.
|
||
- `test_non_settlement_account_stays_other`: счёт «10.01» → OTHER, `counterparty_by_account is False`.
|
||
- `test_osv_flat_form.py::test_s43_counterparties_with_warning`: разбор `s43` → строки «ИП Знаменских А.И.», «ООО "ЭСТЕТ ОМСК"» — COUNTERPARTY; в `warnings` ровно одно `OSV_ROWS` с текстом «вид субконто в шапке не назван: строки аналитики счёта 60.01 приняты за контрагентов по плану счетов — проверьте»; `status == "done"`.
|
||
- `test_s44_materials_stay_other`: разбор `s44` → строки OTHER, предупреждения «вид субконто…» нет.
|
||
- `test_s41_flat_counterparties`: разбор `s41` → строки данных COUNTERPARTY, `account_code_raw == "60"`, предупреждение для счёта «60».
|
||
- [ ] **Step 2: Запуск — FAIL.**
|
||
- [ ] **Step 3: Реализация.** В `_classify_row` после `_schema_group`: если `position == 1`, `position >= len(schema)` и `rules.groups.is_counterparty_account(code)` — COUNTERPARTY без имени, ИНН — по общей ветке COUNTERPARTY (колонка, строка ИНН, хвост названия); флаг `counterparty_by_account`. В `osv.py` — по одной заметке `OsvNote(OSV_ROWS, …)` на каждый различный `account_code_raw` строк с флагом, в порядке листа.
|
||
- [ ] **Step 4: PASS; `make golden-update`** — ожидаемо `s41`, `s43`; реальные снимки, где схема — одна ACCOUNT на расчётном счёте с не-счётными строками (пакет ДКБ `osv-dkb/**`), — OTHER → COUNTERPARTY и новое `OSV_ROWS`: перечислить файлы в отчёте; статусы снимков не должны меняться (если меняются — стоп).
|
||
- [ ] **Step 5: `make check`; commit** — `feat(osv): контрагенты расчётных счетов без названия вида — по плану счетов (#59)`; fixtures — `Снимки: контрагенты расчётных счетов по плану счетов (tabular-parser#59)`.
|
||
|
||
---
|
||
|
||
### Task 6: Правила, открытые вопросы, README, приёмка на реальных файлах
|
||
|
||
**Files:**
|
||
- Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/docs/context/05-domain-rules.md` (§1.1, §1.3, §1.4)
|
||
- Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/docs/context/07-open-questions.md`
|
||
- Modify: `README.md` (корень tabular-parser, раздел «Парсер ОСВ»: каскад стратегий + `header_account`), `CLAUDE.md` tabular-parser (строка каскада в «Модули» — `outline → indent → account_pattern → header_account → sums`)
|
||
- Modify: `openspec/changes/osv-flat-form/tasks.md` (отметить пункты), `design.md` (раздел «Decisions made without discussion» — дописать рулинги исполнения, если были)
|
||
|
||
- [ ] **Step 1: §1 правил (docs).** §1.1: код счёта после «по счету» — и через двоеточие; счёт шапки важнее строки таблицы; строка вида субконто между названием и периодом — не организация, известные виды — группировки после ACCOUNT; организация из «План счетов <орг>»; строка таблицы «Нормализация значений шапки» — без изменений. §1.3: стратегия `header_account` (условия, место в каскаде); счёт шапки у строк до первой строки-счёта; правило расчётных счетов (список — данные `osv_groups.yaml`, позиция 1, без имени, предупреждение; риск OQ-158). §1.4: «Итого развернутое» в любом порядке; «Итого» без корневой ACCOUNT = сумма уровня 0. Ссылки — AURORA-1661, tabular-parser#59, решения владельца 16.10/19.10.2026.
|
||
- [ ] **Step 2: Открытые вопросы (docs)** — следующие свободные номера OQ на момент правки (трек A может занять номера раньше — сверить с `origin/main` docs): «Учредители» на 80 (именованная OTHER, гудвилл §4 п. 2); сузить список расчётных счетов (71, 73); «Организации» в строке вида субконто — что это за вид в 1С. Каждый — «открыт», с допущением текущего поведения.
|
||
- [ ] **Step 3: Приёмка на реальных файлах контура (без коммита данных).**
|
||
|
||
Run:
|
||
```bash
|
||
T=$(mktemp -d) && cd "$T" && unzip -q /Users/andrey.epifancev/projects/awp-ca/result_dir.zip 'files/26e4c90d*' 'files/638ecb3d*' 'files/f2e0e04e*' 'files/7e49f8b5*' 'files/45e06f7d*' 'files/cb7e6eb0*' 'files/270b41da*' 'files/738bfcf5*' 'files/8c7e95d9*' && cp "/Users/andrey.epifancev/projects/awp-ca/ОСВ сч. 60 за 2 кв. 2026.xlsx" files/ && echo "$T"
|
||
cd /Users/andrey.epifancev/projects/awp-ca/.wt/osv-flat/tabular-parser/api && for f in "$T"/files/*; do ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "$f"; done
|
||
```
|
||
Expected (записать таблицей в отчёт задачи): ни одного `INVALID_FORMAT`; у плоских файлов (`26e4c90d`, `638ecb3d`, `f2e0e04e`, `7e49f8b5`, `45e06f7d`, `cb7e6eb0`, письмо) — `OSV_HIERARCHY_BY_HEADER_ACCOUNT`, `account_code_raw` из шапки («76.Н.1», «76.АВ», «76.2», «60»), организация «ООО "Товарное хозяйство"» / «ООО Хреновская Нива», строки — COUNTERPARTY; `270b41da`/`738bfcf5` — `account_code_raw = "76"`, «Итого развернутое» — TOTAL, нарушение остаётся только у валютного «76.55» (трек A); `8c7e95d9` лист «60 сч.» — контрагенты COUNTERPARTY с предупреждением. Отклонение — описать, не чинить молча. После — `rm -rf "$T"`.
|
||
|
||
- [ ] **Step 4: `make check-full`** в tabular-parser (нужен стек compose на портах `.env.test`; если порты заняты демо-стендом — стоп и вопрос контроллеру); `make check` в docs.
|
||
- [ ] **Step 5: `openspec validate osv-flat-form --strict`** (tabular-parser) — без ошибок.
|
||
- [ ] **Step 6: Commit** — docs: `§1 правил ОСВ: плоская форма 1С — счёт из шапки, «Итого развернутое», header_account, контрагенты по плану счетов (tabular-parser#59, AURORA-1661)`; tabular-parser: `docs: README, CLAUDE.md и change osv-flat-form (#59)`.
|