diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 0f4fcbb..e034dfb 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -17,10 +17,10 @@ Node 24, uv 0.10): от пустого каталога до экрана адм - Журнал аудита в БД (`dkb.audit_event`). - Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление - файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. + файла (раздел 6.6); проверка парсера на произвольных форматах ОСВ — раздел 6.7. Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. - Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых - документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7). UI консолидации — другой - исполнитель. + документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7; полный прогон на данных РБ и + контрольные цифры — 7.1–7.3). UI консолидации — другой исполнитель. **Нет (это не баги):** @@ -306,6 +306,84 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n (системное событие) и `description`, где перечислены документы заменённого файла; U12 — `FILE_DELETE` с карточкой файла в `before_value`. Смены статусов обмена (`PROCESSING`, `PROCESSED`, `ERROR`) аудитом не фиксируются — так задумано. +### 6.7 Проверка парсинга ОСВ (xlsx/xls/xlsb) на произвольных файлах + +Парсер (`tabular-parser`) не привязан к координатам: лист ОСВ, заголовок таблицы, колонки сумм, колонка ИНН и иерархия +строк ищутся по содержимому (`05-domain-rules.md` §1.0). Поэтому фикстуры РБ и Счастливочной — только примеры, и +основная задача тестирования — **файлы других форматов**: иные конфигурации 1С, другие учётные системы, файлы после +ручной правки бухгалтером. Два способа прогнать файл. + +**Способ А — через стенд** (как в 6.6): загрузить файл на ЮЛ, дождаться `PROCESSED`, смотреть `GET /documents/{id}` и +`…/entries`. Так проверяется весь путь, включая маппинг в ядре. + +**Способ Б — парсер без стенда** (быстрее, виден «сырой» результат парсера до ядра; нужен клон `tabular-parser` и +`make venv` в нём): + +```bash +cd ../tabular-parser/api +ENV_FILE=.env.test .venv/bin/python -m src.parser.cli "/путь/к/файлу.xlsx" > result.json; echo "exit=$?" +``` + +Код возврата: `0` — `done`, `1` — `needs_check` (контрольные суммы не сошлись, причины в `metrics.warnings`), `2` — ошибка +(`error.code`: `INVALID_DOCUMENT` — не найден лист ОСВ, `INVALID_FORMAT` — нет заголовка таблицы или обязательных групп +сумм, `UNREADABLE`, `UNSUPPORTED_FORMAT`, `TIMEOUT`), `3` — файл не найден. Для расширения не из `.xls/.xlsx/.xlsb` +добавьте `--file-type XLSX`. В `result.json` — то же, что ядро кладёт в `document`/`osv_entry`: `documents[].header` +(организация, ИНН, счёт, период, нарастающий итог, БУ/НУ, валюта), `group_schema`, `rows[]` (уровень, тип строки, +счёт, контрагент, ИНН, шесть сумм, `extra`), `checks`, `metrics.warnings`. + +**Что сверять с Excel (чек-лист на каждый файл).** Откройте файл рядом с результатом и проверьте: + +| № | Что | Ожидание | +|---|---|---| +| P1 | Лист | найден каждый лист с текстом «Оборотно-сальдовая ведомость» (один `documents[]` на лист, `locator = sheet:<имя>`); листы без ОСВ (справки, формы ББ/ОФР) пропущены с `warnings` `skipped: …`, а не ошибка | +| P2 | Шапка | `organization_name`, `organization_inn` (если есть в шапке), `account_code_raw`, `period_text` → `period_year`, `period_quarter`, `is_cumulative` соответствуют тексту над таблицей. Поле не найдено → `null` + предупреждение, не ошибка | +| P3 | Период | «1 квартал» → `Q1`, нарастающий; «2 квартал» → `Q2`, квартальный; «полугодие», «6 месяцев», «январь – июнь» → `Q2`, нарастающий; «9 месяцев», «3 квартал» → `Q3`; «2024 г.», «12 месяцев» → `Q4`, нарастающий; римские кварталы («I кв.») тоже | +| P4 | Колонки сумм | шесть сумм разнесены верно: сальдо на начало Дт/Кт, обороты Дт/Кт, сальдо на конец Дт/Кт. Проверьте 2–3 строки с ненулевыми значениями во всех шести колонках и строку «Итого» | +| P5 | Иерархия | уровни строк (`level`, `parent_row_number`) повторяют дерево в Excel: счёт → субсчёт → контрагент → договор. Стратегия — в `warnings` `hierarchy=outline|indent|account_pattern|sums`; если `sums` — дерево восстановлено по равенству сумм, посмотрите внимательнее | +| P6 | Типы строк | `group_type`: `ACCOUNT` (код счёта), `COUNTERPARTY`, `CONTRACT`, `DEPARTMENT`, `OTHER`, `TOTAL` (строка «Итого», одна на лист). Строки «Вал.», «Кол.», «USD» → `data_type` `CURRENCY`/`QUANTITY`; «НУ» → `NU` | +| P7 | ИНН | если в таблице есть колонка ИНН (по заголовку или по содержимому, когда ≥ 80 % значений — 10/12 цифр), `counterparty_inn` заполнен у контрагентов и **не** заполнен у счетов и договоров; КПП и номера договоров за ИНН не принимаются | +| P8 | Числа | суммы — строки с двумя знаками (`"1234567.89"`); текстовые числа «1 234 567,89», «(1 234,00)», «—» разобраны как число, отрицательное, `null`; пустая ячейка — `null`, не `0` | +| P9 | Контрольные суммы | `status = done`: сальдо строки сходится (начало + обороты = конец), «сумма детей = родитель», «Итого» = корень. `needs_check` при расхождении, в `checks` — какие строки; «подгонки» под сходимость быть не должно | +| P10 | Лишние колонки | «КПП», «Код», «Количество» и прочие попали в `extra` строки, суммы не сдвинулись | + +**Готовые кейсы нестандартных форматов** — синтетика `../fixtures/osv/synthetic/s01…s16` с описанием каждого файла и +ожиданием в `../fixtures/osv/synthetic/README.md`: шапка варианта 2 («Период: …», «Субконто»), уровни без outline (только +отступами, только по кодам счетов), строки валюты и количества, числа текстом, два листа ОСВ и посторонний лист, нет +сальдо на начало и свёрнутое сальдо на конец, колонка КПП, названия субсчетов, сломанная сумма (`needs_check`), повтор +заголовка внутри таблицы, лист ББ рядом с ОСВ, xlsb, синонимы заголовков. Прогоните их способом Б и сверьте с README. + +**Как делать свои файлы.** Возьмите любую реальную ОСВ и меняйте по одному признаку, после каждого — прогон и чек-лист: + +- сдвинуть таблицу: вставить 5–10 пустых строк и 2–3 колонки слева/сверху; перенести шапку организации ниже или выше; +- переименовать лист; добавить лист «Справка» перед листом ОСВ; положить две ОСВ по разным счетам на разные листы; +- убрать колонку ИНН совсем; вставить колонку «КПП» между ИНН и суммами; переставить блоки «Обороты» и «Сальдо на конец»; +- разбить заголовок таблицы на 1–4 строки с объединёнными ячейками; заменить «Сальдо на начало периода» на «Нач. сальдо», + «Дебет/Кредит» на «Дт/Кт»; +- убрать группировку outline (скопировать значения в новую книгу) — уровни должны восстановиться по отступам, затем по кодам + счетов; убрать и отступы — по шаблону кодов и по суммам; +- суммы текстом с пробелами и запятой, отрицательные в скобках, прочерки вместо нулей; +- сохранить как `.xls`, `.xlsx`, `.xlsb` (xlsb — обязательно из Excel, см. ниже); +- сломать одну сумму на 1 рубль — ожидается `needs_check` с указанием строки, не `done`. + +**Что не считать багом (известные границы):** + +- Заголовок таблицы без групп «Обороты» или «Сальдо на конец» → `INVALID_FORMAT`: без них ОСВ не ОСВ. Отсутствовать может + только «Сальдо на начало». +- Заголовок с незнакомым синонимом («Нач. остаток») даёт `needs_check`, суммы уходят в `extra`. Это не ошибка кода, а + словарь: присылайте файл, синоним добавляется строкой в `rules/osv_headers.yaml`. +- ИНН внутри текста контрагента («Полиграф, 3528315710») и ИНН в ячейке над названием сейчас не извлекаются — задача + RSHB/arm-tabular-parser#9 (OQ-100, OQ-130). До неё строка уходит в ядро с именем целиком, `counterparty_inn` пуст. +- xlsb читается без outline level и форматирования: уровни только по отступам пробелами и кодам счетов. Настоящего xlsb из + Excel в фикстурах нет (OQ-102) — если у вас есть Excel, сохраните `s02_no_outline_indent.xlsx` как xlsb и прогоните. +- Строка «Итого» нужна: без неё `needs_check`. +- Файл больше 50 МиБ, распакованный xlsx больше 500 МиБ или разбор дольше 120 с → `UNSUPPORTED_FORMAT` / `TIMEOUT`. +- Парсер не знает ядра: признак «нарастающая» для ОСВ за Q2–Q4 ядро перепроверяет по данным Q1 (6.6, `is_cumulative`), ОСВ за + Q1 ядро считает и нарастающей, и квартальной (OQ-124). + +Баг парсера — это: суммы в не тех колонках, потерянные или задвоенные строки, неверный уровень при наличии outline или +отступов, `done` при реально не сходящихся суммах, падение (`RECOGNITION_FAILED`) на любом файле. К багу прикладывайте +файл (или его обезличенную копию) и `result.json`. + ## 7. Консолидация через API (Срез 4) Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001` @@ -366,6 +444,98 @@ curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/ K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокировка не истекает по времени — освободить её можно только `DELETE …/lock`, перехватом или аннулированием версии (OQ-119). +### 7.1 Полный прогон на данных РБ за 2024 (скрипт) + +Для сводной ОСВ нужны все квартальные файлы двух ЮЛ. Их 22: РБ (`../fixtures/osv/rb/2024-Q{1,2,3}/`, 2 + 2 + 7) и +Счастливочная (`../fixtures/osv/schastl/2024-Q{1,2,3}/`, 3 + 3 + 5). Загружать по одному через Swagger долго, поэтому цикл +(`ka_go`, `$H` из 6.6; `LE_RB` и `LE_SCH` — id ЮЛ «АО "Русский бисквит"» и «ООО "Пекарня Счастливочная"», обе в одной ГСК +ГК из раздела 7; `V` — версия за 2024 Q3, созданная и взятая в работу по K1/K4 **до** загрузки — тогда переходы статусов +видны на K8/K9): + +```bash +F=../fixtures/osv +for f in "$F"/rb/2024-Q1/* "$F"/rb/2024-Q2/* "$F"/rb/2024-Q3/*; do + curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_RB/files?consolidation_version_id=$V" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' +done +for f in "$F"/schastl/2024-Q1/* "$F"/schastl/2024-Q2/* "$F"/schastl/2024-Q3/*; do + curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_SCH/files?consolidation_version_id=$V" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' +done +# подождать ~30 с, затем убедиться, что все файлы PROCESSED, и подтвердить все документы +for LE in $LE_RB $LE_SCH; do + curl -s "localhost:8000/api/v1/legal-entities/$LE/files?limit=50" -H "$H" | python3 -c ' +import sys,json +for f in json.load(sys.stdin)["items"]: + print(f["status"], f["original_name"], [ (d["id"], d["osv_account_code"], d["period_quarter"], d["status"]) for d in f["documents"]])' +done +# подтверждение: для каждого document id из вывода выше +for DOC in …; do curl -s -o /dev/null -w "%{http_code} $DOC\n" -X POST "localhost:8000/api/v1/documents/$DOC/confirm" -H "$H"; done +``` + +Ожидание после подтверждения: `GET …/versions/{V}` — `status = DATA_CORRECTION`; в чек-листе у РБ ОСВ 60 и 62 `CONFIRMED` с +тремя `document_ids`, у Счастливочной — 60, 62, 76; ОСВ 58 у РБ не `CONFIRMED` (есть только 58.03); остальные ячейки ОСВ +Q3-only — `missing_quarters = ["Q1","Q2"]`. + +Если файл загружен **до** создания версии — не страшно: подтверждённые документы подхватываются при создании версии +(«подхват»), только переходы `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` по загрузке тогда не увидеть. + +### 7.2 Контрольные цифры сводной ОСВ (версия РБ 2024 Q3) + +Значения ниже посчитаны вручную по файлам фикстур и совпадают со сквозным тестом `test_consolidation_fixtures`. Они +проверяют самое важное — **приведение квартальных ОСВ к 9 месяцам** (`05-domain-rules.md` §2): сальдо на начало берётся из +ОСВ за Q1, обороты складываются за Q1 + Q2 + Q3, сальдо на конец — из ОСВ за Q3. + +`GET …/osv/summary`: + +| Поле | Ожидание | +|---|---| +| `rows_total` | 1385 | +| `unmapped_total` | 1381 до ручного сопоставления: 4 строки Счастливочной с ИНН 3528052718 (`60.01`, `60.02`, `62.01`, `62.02`) сопоставлены автоматически | +| `documents_total` | 22 | +| `gaps` | ровно 7: РБ — `01`, `58.03`, `66`, `67`, `76`; Счастливочная — `01`, `02`; у всех `missing_quarters = ["Q1","Q2"]` | + +Строки свода (`GET …/osv?legal_entity_id=…&q=…`), суммы в рублях: + +| Компания, счёт, контрагент | Сальдо нач. Дт | Сальдо нач. Кт | Обороты Дт | Обороты Кт | Сальдо кон. Дт | Сальдо кон. Кт | Признак | +|---|---|---|---|---|---|---|---| +| Счастливочная, `60.01`, «Русский бисквит», ИНН 3528052718 | — | 4 454 151.93 | 128 058 143.65 | 125 950 450.77 | — | 2 346 459.05 | `GSK`, `match_kind = INN` | +| Счастливочная, `60.02`, «Русский бисквит» | 0 | 0 | 9 745 019.62 | 9 745 019.62 | 0 | 0 | `GSK`; контрагента нет в Q2 — квартал считается нулями | +| РБ, `60.01`, «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ», без ИНН | — | 38 806 758.00 | 307 986 284.00 | 375 490 642.46 | — | 106 311 116.46 | `EXTERNAL` до сопоставления, после K14 — `GSK` | +| Счастливочная, `62.01`, «Русский бисквит» | 40 211 938.00 | — | 398 018 012.46 | 330 517 504.00 | 107 712 446.46 | — | `GSK`; в каждом квартальном файле под `62.01` **две** строки с этим контрагентом — свод их суммирует, `entry_ids` = 6 | +| Счастливочная, `62.02`, «Русский бисквит» | — | 24 100 000.00 | 9 900 000.00 | 0 | — | 14 200 000.00 | `GSK` | + +Как проверить любую строку самому: откройте три квартальных файла этого ЮЛ по этому счёту, найдите контрагента под тем же +субсчётом и сложите обороты по трём файлам; сальдо на начало сравните с первым файлом, на конец — с третьим. Тождество +«начало + обороты Кт − обороты Дт = конец» (для кредитового сальдо) должно сходиться: 4 454 151.93 + 125 950 450.77 − +128 058 143.65 = 2 346 459.05. Строка свода содержит `entry_ids` всех трёх исходных записей и `is_cumulative = false`; +`period = "2024 Q3"`. + +Строка РБ по `60.01` зеркальна одной из двух позиций Счастливочной под `62.01` (в файлах: обороты 375 490 642.46 / +307 986 284.00, сальдо на конец 106 311 116.46) — внутригрупповые обороты сходятся копейка в копейку. В своде эта позиция +видна только в сумме с соседней (строка `62.01` выше); разнесение по позициям и сопоставление ВГО — Срез 5. + +Что ещё проверить руками: + +- контрагент, которого нет в одном из кварталов (как «Русский бисквит» на `60.02`): в своде одна строка, пропущенный квартал + дал нули, `entry_ids` — две записи; +- `GET …/osv?match_kind=INN` — только строки с ИНН в файле (у Счастливочной есть колонка ИНН, у РБ нет), `match_stored = true`; +- `GET …/osv?unmapped=true&legal_entity_id=$LE_RB&q=счастлив` — две строки (`60.01`, `62.…`) до сопоставления; после K14 — + ни одной, а `summary.unmapped_total` уменьшился ровно на число изменившихся строк свода (на этих данных — на 4, при + `propagated = 17` записей `osv_entry`: записи кварталов и пар-пробелов 66/76 считаются отдельно); +- пробелы не зависят от фильтров свода: `gaps` одинаковы при любом `GET …/osv/summary`. + +### 7.3 Сверка экспорта с API и Excel + +1. `GET …/osv/export` без фильтров → `svod.xlsx`; лист «Сводная ОСВ»: число строк без заголовка = `rows_total`, порядок — как + в API (ЮЛ по имени, счёт, номер строки). Суммы — числа с двумя знаками (не текст): выделите колонку «Обороты Дт» и + посмотрите сумму в строке состояния Excel; формат ячейки `#,##0.00`. +2. Найдите строку «Русский бисквит» `60.01` Счастливочной и сверьте шесть сумм с таблицей из 7.2 и с ответом `GET …/osv`. +3. Колонки «Сопоставлено с» и «Способ сопоставления»: для сопоставленных — имя ЮЛ из справочника и `INN`/`MANUAL`/`PROPAGATED`, + для несопоставленных — пусто. +4. Лист «Пробелы»: 7 строк, как в `summary.gaps`; с фильтром `?legal_entity_id=` в экспорте лист «Пробелы» всё равно полный. +5. Экспорт с `?unmapped=true` после полного сопоставления — лист «Сводная ОСВ» только с заголовком, лист «Пробелы» заполнен. +6. Текст контрагента, начинающийся с `=`, `+`, `-`, `@` (сделайте такой в своём файле), в Excel показывается как текст, а не + вычисляется как формула. + ## 8. Известные ограничения (не заводить как баги) - UI только для администрирования; справочники — через API. @@ -384,7 +554,9 @@ K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокиров - Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только при точном совпадении нормализованного имени (OQ-127). -Полный список отложенных находок — Gitea `RSHB/arm-core`, issue «Follow-ups from #5». +- Парсер ОСВ: границы и «не баги» — в конце раздела 6.7 (ИНН в тексте строки — arm-tabular-parser#9). + +Полный список отложенных находок — Gitea `RSHB/arm-core`, issues «Follow-ups from #5» и «Follow-ups from #18» (#21). ## 9. Сброс стенда