Колонка «Показатели», строки-показатели, «В валюте», BU по умолчанию (AURORA-1613, AURORA-1661 — валютная часть). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
23 KiB
Markdown
193 lines
23 KiB
Markdown
## ADDED Requirements
|
||
|
||
### Requirement: Строки-показатели и колонка «Показатели»
|
||
Строка-показатель — строка, которая несёт вид сумм предыдущей строки данных, а не уровень аналитики. Парсер SHALL считать
|
||
строку показателем, если:
|
||
1. в таблице есть колонка «Показатели» (заголовок блока по словарю `osv_headers.yaml` — `service_columns.indicator`,
|
||
«Показатели», «Показа-тели»), ячейка строки в ней непуста и метка не «БУ»: «НУ» → `data_type = NU`, «Кол.» →
|
||
QUANTITY, «Вал.» → CURRENCY (словарь `data_types.yaml` — `indicator_column`); «ПР», «ВР» (`indicator_column_untyped`)
|
||
→ `data_type = null`; метка не из словарей → `data_type = null` и предупреждение `OSV_ROWS` «строка N: метка «X» в
|
||
колонке «Показатели» не распознана — строка не участвует в проверках»;
|
||
2. иначе — текст строки матчит `data_types.yaml` — `row` («Вал.», «Кол.», «Кол-во», «В валюте» → CURRENCY/QUANTITY) или
|
||
равен коду валюты из `currency_codes`.
|
||
|
||
Строка «Валюта <код>» («Валюта USD») MUST NOT считаться показателем: это строка аналитики с рублёвыми суммами; код валюты
|
||
не хранится. Колонка «Показатели» MUST NOT быть колонкой текста строки (как колонка ИНН) и SHALL оставаться в `extra`
|
||
строки под своим заголовком. `row_value` строки с меткой колонки — сама метка.
|
||
|
||
Строки-показатели MUST NOT передаваться в каскад иерархии. Родитель строки-показателя — ближайшая строка выше, которая
|
||
сама не показатель; уровень — на один глубже родителя; показатель без строки данных выше — уровень 0 без родителя.
|
||
Строки-показатели MUST NOT входить в сверку «сумма детей = родитель» (ни как дети, ни как родители) и в сверку «Итого»;
|
||
тождество строки (начало + обороты = конец) проверяется у них, как у всех строк. Строки NU хранятся как пришли
|
||
(`ASSUMPTION(OQ-45)`); собственных проверок по NU нет (AURORA-1669).
|
||
|
||
Итоговые строки, следующие сразу за основной «Итого» и помеченные в колонке «Показатели» меткой показателя, SHALL
|
||
оставаться в таблице как TOTAL (`data_type` по метке) и MUST NOT уходить в подвал или давать предупреждение «ниже
|
||
таблицы есть числа»; сверка «Итого = корень» берёт первую итоговую строку, которая не показатель.
|
||
|
||
#### Scenario: Колонка «Показатели» БУ/НУ
|
||
- **WHEN** разбирается синтетическая ОСВ `s40` (счёт 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` (колонка «Показатели» с метками «Кол.», «Вал.», «ПР», «ВР» и одной
|
||
неизвестной меткой)
|
||
- **THEN** «Кол.» — QUANTITY, «Вал.» — CURRENCY, «ПР»/«ВР»/неизвестная — `data_type = null`, все — показатели вне
|
||
сверки родителя; одно предупреждение `OSV_ROWS` о нераспознанной метке; статус `done`
|
||
|
||
#### Scenario: «В валюте» под «Валюта <код>»
|
||
- **WHEN** разбирается синтетическая ОСВ `s42` (под контрагентом пары «Валюта USD» / «В валюте», «Валюта EUR» /
|
||
«В валюте»)
|
||
- **THEN** «В валюте» — CURRENCY, родитель — «Валюта …» над ней; контрагент = Σ строк «Валюта …» по рублям;
|
||
статус `done`
|
||
|
||
#### Scenario: Строки «Вал.»/«Кол.» в колонке текста не меняются
|
||
- **WHEN** разбираются s04, s30 и все реальные фикстуры `../fixtures/osv/**`
|
||
- **THEN** golden-снимки совпадают с прежними (родитель показателя — строка над ним, как и было)
|
||
|
||
## MODIFIED Requirements
|
||
|
||
### Requirement: Классификация строк
|
||
Классификация SHALL выполняться над деревом, из которого уже убраны склеенные строки ИНН (требование «Строки ИНН рядом с
|
||
контрагентом»). Каждая строка данных SHALL получить `group_type`: ACCOUNT — текст матчит `ACCOUNT_RE` (регистр
|
||
учитывается, IGNORECASE нет): две-три цифры и до двух сегментов субсчёта через точку, где сегмент — `\d{1,2}` **или** 1–3
|
||
заглавные буквы кириллицы/латиницы («76.АВ», «76.ВА», «76.ОТ», R-16), далее опционально запятая и название:
|
||
`^\d{2,3}(\.(\d{1,2}|[А-ЯЁA-Z]{1,3})){0,2}(,\s*.*)?$` с флагами `re.ASCII | re.DOTALL` (после запятой допустим любой
|
||
пробел или его отсутствие, R-18; цифры — только 0–9; название после запятой может занимать несколько строк ячейки — M-1);
|
||
`account_code_raw` — код как в файле, без названия после запятой. Не-счёт — группировка из `group_schema` по позиции
|
||
(число не-счётных предков после последнего ACCOUNT + 1), при выходе за схему — OTHER; строка ИНН без пары — OTHER;
|
||
последняя строка уровня 0 с текстом из словаря «Итого» — TOTAL.
|
||
|
||
Для COUNTERPARTY `counterparty_name` SHALL быть текстом строки без ИНН в конце и разделителя перед ним (`row_value` —
|
||
исходный текст; OQ-100), а `counterparty_inn` — первым найденным из:
|
||
1. колонки ИНН: цифры текстовой ячейки («ИНН 352819549855» → «352819549855», R-15), склеенная «ИНН/КПП» (`inn_kpp_cell`)
|
||
→ ИНН; результат только при 10 или 12 цифрах, иначе `null`; число из 9/11 цифр (ИНН регионов 01–09 с потерянным
|
||
ведущим нулём) дополняется нулём слева (R-24);
|
||
2. строки ИНН, склеенной с этой строкой (требование «Строки ИНН рядом с контрагентом»);
|
||
3. конца текста названия: 10 или 12 цифр после запятой, скобки, «/», пробела или метки «ИНН»
|
||
(`counterparty_inn_suffix`; «Полиграф, 3528315710», «ООО Альфа (ИНН 7701234567)»).
|
||
Контрольная сумма ИНН из колонки и из названия 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: Подразделение, контрагент с ИНН, физлицо
|
||
- **WHEN** разбирается `schastl/2024-Q3/…62 за 3 квартал 2024 г..xls`
|
||
- **THEN** «БП Аренда» → DEPARTMENT; «МАЛЕЧКИНСКАЯ ПТИЦЕФАБРИКА ООО» → COUNTERPARTY с `counterparty_inn` из 10 цифр;
|
||
«Ведомость в счет з/платы» → COUNTERPARTY с `counterparty_inn = null`
|
||
|
||
#### Scenario: ИНН в конце названия
|
||
- **WHEN** разбирается синтетическая ОСВ `s17` (ИНН в тексте названия с разными разделителями)
|
||
- **THEN** у контрагентов `counterparty_name` без ИНН, `counterparty_inn` — ИНН из текста, `row_value` — исходный текст
|
||
|
||
#### Scenario: «ИНН/КПП» в одной ячейке колонки ИНН
|
||
- **WHEN** разбирается синтетическая ОСВ `s19`
|
||
- **THEN** `counterparty_inn` — 10 цифр до «/»
|
||
|
||
#### Scenario: Название субсчёта в строке счёта
|
||
- **WHEN** в синтетической ОСВ строка «60.01, Расчеты с поставщиками»
|
||
- **THEN** `group_type = ACCOUNT`, `account_code_raw = "60.01"`, `row_value` — исходный текст
|
||
|
||
#### Scenario: Буквенный субсчёт
|
||
- **WHEN** в ОСВ 76 строка «76.АВ» (реальные файлы РБ и Счастливочной) либо «60.АВ, …» (синтетика)
|
||
- **THEN** `group_type = ACCOUNT`, строка наследуется детьми как счёт ветки; «76.ав» (строчные) счётом не считается
|
||
|
||
#### Scenario: Строки Вал./Кол.
|
||
- **WHEN** в синтетической ОСВ под контрагентом строки «USD» и «Кол.»
|
||
- **THEN** `group_type = OTHER`, `data_type = CURRENCY` и `QUANTITY`, `counterparty_name`/`counterparty_inn` равны `null`,
|
||
`account_code_raw` совпадает с родителем, строки исключены из проверки «сумма детей = родитель», статус `done`
|
||
|
||
#### Scenario: Шапка без вида данных
|
||
- **WHEN** разбирается синтетическая ОСВ `s43` (шапка без «Выводимые данные», колонки «Показатели» нет)
|
||
- **THEN** `header.data_type = null`, у всех строк данных и «Итого» `data_type = BU`, в `warnings` одно `OSV_HEADER` «вид
|
||
данных в шапке не указан — строки без метки приняты как БУ», статус `done`
|
||
|
||
#### Scenario: «Валюта <код>» — обычная строка
|
||
- **WHEN** в синтетической ОСВ `s42` под контрагентом строка «Валюта USD» с рублёвыми суммами
|
||
- **THEN** `group_type = OTHER`, `data_type` шапки, строка участвует в сверке «сумма детей = родитель»
|
||
|
||
### Requirement: Контрольные суммы определяют статус
|
||
Для каждого документа парсер SHALL проверить: по каждой строке `opening_debit − opening_credit + turnover_debit −
|
||
turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по каждому родителю суммы = Σ детей с тем же
|
||
`data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у
|
||
родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто,
|
||
а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка
|
||
«Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT
|
||
(первая строка уровня 0); итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если корня нет (пустая ОСВ,
|
||
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, применённые к листу); любое
|
||
нарушение → `status = needs_check` документа и запись причины в `warnings[]` документа. Предупреждения листа SHALL быть
|
||
структурами `{code, severity, message}` с `severity = info` и кодом с префиксом `OSV_` (`OSV_HIERARCHY_BY_<СТРАТЕГИЯ>`,
|
||
`OSV_HEADER`, `OSV_ROWS`, `OSV_INN_ROWS`, `OSV_NO_DATA_ROWS`, `OSV_BALANCE_IDENTITY`, `OSV_PARENT_SUM`, `OSV_TOTAL`,
|
||
`OSV_XLSB_NO_OUTLINE`); строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Парсер MUST NOT подгонять
|
||
структуру под сходимость.
|
||
|
||
Исключение — **счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 91 и 99 строки аналитики несут
|
||
только обороты, сальдо закрыто на субсчёт закрытия (91.09). Список счетов — данные `rules/osv_checks.yaml`
|
||
(`accounts_without_analytic_balance`, начальный состав `91`, `99`; 90 не входит). Строка относится к такому счёту, если
|
||
код ближайшего счёта её ветки (`account_code_raw`) совпадает с записью списка либо является её субсчётом
|
||
(`91`, `91.01`, но не `910`). Для таких строк тождество строки SHALL NOT проверяться; сверка «сумма детей = родитель»
|
||
сохраняется: обороты — всегда по колонкам, сальдо — только у родителя с дочерними субсчетами (корень → субсчета), а у
|
||
родителя с одной лишь аналитикой сальдо не сверяется (сальдо аналитики пусто по определению); «Итого» сверяется как обычно.
|
||
|
||
Остаточные риски (известные слепые зоны, не ловятся проверками §1.4):
|
||
- RR-1 — у свёрнутого родителя сверяется только нетто сальдо: распределение сальдо детей между дебетом и кредитом
|
||
(перестановка сторон при совпавшем нетто) не проверяется.
|
||
- RR-2 — «свёрнутость» определяется по самому родителю: развёрнутое сальдо родителя, у которого в файле потеряна одна
|
||
сторона, неотличимо от нетто-показа 1С и проходит, если нетто совпало.
|
||
- RR-3 — нетто допускается на любом уровне дерева (счёт, субсчёт, контрагент, договор), а не только на уровне «счёт →
|
||
субсчета», где его показывает 1С: ошибка распределения сальдо между договорами под свёрнутым контрагентом не ловится.
|
||
- RR-4 — тождество строки сверяет только нетто строки, «Итого = корень» — только корень: ошибка, сохраняющая нетто
|
||
строки, ловится лишь сверкой с родителем по колонкам, а под свёрнутым родителем — ничем (см. слепую зону ниже).
|
||
- Слепая зона: обе стороны сальдо (на начало или на конец) развёрнутого ребёнка увеличены на одно и то же X, а его
|
||
непосредственный родитель свёрнут — не ловится ничем: достаточно свёрнутого прямого родителя (сверка с ним — по нетто,
|
||
а нетто ребёнка не изменилось; значения родителя и выше не затронуты; тождество строки и «Итого = корень» сохраняются).
|
||
- Родитель с сальдо (0, 0) при детях (X Дт, X Кт) принимается: нулевое сальдо — свёрнутое, нетто детей X − X = 0
|
||
(закреплено тестом как осознанное поведение R-21).
|
||
- Вне R-21: текст вида «12.ООО» (2–3 цифры, точка, до трёх заглавных букв) принимается за счёт (R-16) и меняет
|
||
`group_type` строк ветки; суммы и проверки это не затрагивает.
|
||
|
||
#### Scenario: Все реальные файлы сходятся
|
||
- **WHEN** разбирается каждый файл `fixtures/osv/**`
|
||
- **THEN** `status = done`, `checks.total_row_matches_root = true`, оба списка нарушений пусты
|
||
|
||
#### Scenario: Испорченная сумма
|
||
- **WHEN** в синтетической ОСВ у одного контрагента `closing_debit` увеличен на 1
|
||
- **THEN** `status = needs_check` документа, `balance_identity_violations` содержит его `row_number`, `parent_sum_violations` —
|
||
`row_number` родителя, в `warnings[]` документа — `OSV_BALANCE_IDENTITY` / `OSV_PARENT_SUM` (`info`) с номерами строк
|
||
|
||
#### Scenario: Сальдо родителя свёрнуто нетто
|
||
- **WHEN** у родителя сальдо показано одной стороной (нетто), а у детей — обеими сторонами, и нетто детей равно нетто родителя
|
||
- **THEN** нарушения «сумма детей = родитель» по сальдо нет; обороты при этом сверяются по колонкам, а неверное нетто
|
||
или расхождение оборотов — нарушение
|
||
|
||
#### Scenario: Счёт 91 — аналитика только с оборотами
|
||
- **WHEN** разбирается ОСВ по счёту 91: строки аналитики 91.01, 91.02, 91.09 несут только обороты, сальдо закрыто на
|
||
субсчёт закрытия (`fixtures/osv-dkb/tsimlyansky/2025-Q3/91_ 9мес.2025.xls`)
|
||
- **THEN** `status = done`, оба списка нарушений пусты; обороты аналитики при этом сходятся с оборотами субсчёта, а
|
||
расхождение оборотов, сальдо субсчетов с корнем или «Итого» осталось бы нарушением
|
||
|
||
#### Scenario: «Итого» не найдена
|
||
- **WHEN** в таблице нет строки с текстом из словаря «Итого»
|
||
- **THEN** `total_row_matches_root = false`, `status = needs_check`, в `warnings[]` — `OSV_TOTAL` (`info`) «строка «Итого» не найдена»
|
||
|
||
#### Scenario: Пустая ОСВ
|
||
- **WHEN** разбираются реальные пустые ОСВ (в фикстурах две: `schastl/2025-Q2/…58 за 2 квартал 2025 г..xlsx` и `…66 за 2
|
||
квартал 2025 г..xlsx`: заголовок и сразу «Итого») либо любой файл, где кроме «Итого» строк данных нет
|
||
- **THEN** `status = done` документа, `rows` содержит только строку TOTAL, `metrics.rows = 0` (строки данных без TOTAL),
|
||
`hierarchy=trivial`, `checks.total_row_matches_root = true` (корня нет, суммы TOTAL пусты), предупреждение
|
||
`OSV_NO_DATA_ROWS` «нет строк данных»
|
||
|
||
#### Scenario: Предупреждение ОСВ
|
||
- **WHEN** на листе ОСВ есть строка без родителя
|
||
- **THEN** у документа ОСВ предупреждение `{code: "OSV_…", severity: "info", message}`, статус документа по §1.6;
|
||
в `metrics.warnings` конверта строки `sheet=…` нет
|