fix(osv): вторые итоговые строки («Итого развернутое») вне тождества строки, R-11 отменён R-16 (#59)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69564ffcbd
commit
835083ca4d
@ -185,7 +185,7 @@ ParseOutcome`. Структура ОСВ ищется **по содержимо
|
||||
**«Итого».** Основная итоговая строка — первая TOTAL, не являющаяся показателем; она сверяется с первым корнем ACCOUNT либо, если корней
|
||||
уровня 0 несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03), с суммой строк уровня 0
|
||||
(OQ-106, OQ-133; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя выше). Вторая итоговая
|
||||
строка сразу за «Итого» («Итого развернутое», `total_row_secondary`) — тоже `TOTAL`, в сверку не входит. Строка сумм
|
||||
строка сразу за «Итого» («Итого развернутое», `total_row_secondary`) — тоже `TOTAL`, в сверку «Итого» и в тождество строки не входит (в 1С у неё только сальдо, без оборотов). Строка сумм
|
||||
без подписи в конце таблицы (1С без «Итого») принимается за итоговую с предупреждением.
|
||||
|
||||
**ИНН контрагента** (OQ-100, OQ-130): `counterparty_inn` берётся из колонки ИНН (в том числе склеенного «ИНН/КПП»), затем из
|
||||
|
||||
@ -75,14 +75,24 @@ def _no_analytic_balance(item: ClassifiedRow, exempt: OsvCheckRules | None) -> b
|
||||
return exempt is not None and exempt.has_no_analytic_balance(item.account_code_raw)
|
||||
|
||||
|
||||
def balance_identity(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = None) -> list[int]:
|
||||
def _is_secondary_total(item: ClassifiedRow, secondary: Sequence[Pattern]) -> bool:
|
||||
return item.group_type == "TOTAL" and any(p.search(item.body.text) for p in secondary)
|
||||
|
||||
|
||||
def balance_identity(
|
||||
rows: Sequence[ClassifiedRow],
|
||||
exempt: OsvCheckRules | None = None,
|
||||
secondary_totals: Sequence[Pattern] = (),
|
||||
) -> list[int]:
|
||||
"""Номера строк, где начало + обороты ≠ конец (±0.01, пусто = 0); проверяются все строки, включая «Итого».
|
||||
|
||||
Строки счетов без сальдо по аналитике (`exempt`, OQ-132) не проверяются.
|
||||
Строки счетов без сальдо по аналитике (`exempt`, OQ-132) не проверяются. Вторые итоговые строки
|
||||
(`secondary_totals`, «Итого развернутое») не проверяются: в 1С они несут только развёрнутые сальдо, без оборотов
|
||||
(#59, R-16); основная «Итого» проверяется.
|
||||
"""
|
||||
violations: list[int] = []
|
||||
for item in rows:
|
||||
if _no_analytic_balance(item, exempt):
|
||||
if _no_analytic_balance(item, exempt) or _is_secondary_total(item, secondary_totals):
|
||||
continue
|
||||
sums = _values(item)
|
||||
expected = _net(sums[_OPENING]) + _net(sums[_TURNOVER])
|
||||
@ -240,9 +250,10 @@ def run_checks(
|
||||
"""Три проверки §1.4 и заметки с кодами `OSV_*` и номерами строк (пустой список — всё сошлось).
|
||||
|
||||
`exempt` — счета без сальдо по аналитике (`rules.checks`, OQ-132); без него проверяются все строки полностью.
|
||||
`secondary_totals` — вторые итоговые строки («Итого развернутое»): «Итого» для сверки — не они (#59).
|
||||
`secondary_totals` — вторые итоговые строки («Итого развернутое»): «Итого» для сверки — не они, и тождество
|
||||
строки на них не проверяется (#59, R-16).
|
||||
"""
|
||||
identity = balance_identity(rows, exempt)
|
||||
identity = balance_identity(rows, exempt, secondary_totals)
|
||||
parents = parent_sums(rows, exempt)
|
||||
matches = total_matches_root(rows, secondary_totals)
|
||||
warnings: list[OsvNote] = []
|
||||
|
||||
@ -902,7 +902,7 @@ def _s40() -> list[SheetSpec]:
|
||||
("Август - Агро ООО", ("1000.00", None, "500.00", "300.00", "1200.00", None), 0),
|
||||
("АВИС ООО", (None, "200.00", "100.00", "400.00", None, "500.00"), 0),
|
||||
("Агроаспект ООО", (None, None, "50.00", "50.00", None, None), 0),
|
||||
("Итого развернутое", ("1000.00", "200.00", "650.00", "750.00", "1200.00", "500.00"), 0),
|
||||
("Итого развернутое", ("1000.00", "200.00", None, None, "1200.00", "500.00"), 0),
|
||||
("Итого", ("800.00", None, "650.00", "750.00", "700.00", None), 0),
|
||||
]
|
||||
return [_flat_sheet(texts, 9, rows)]
|
||||
@ -918,7 +918,7 @@ def _s41() -> list[SheetSpec]:
|
||||
rows: list[FlatRow] = [
|
||||
("Автодор-Платные Дороги ООО", ("820.21", None, "5000.00", "4991.00", "829.21", None), 0),
|
||||
("АГРОПРОФИ ООО", (None, "2763.79", None, None, None, "2763.79"), 0),
|
||||
("Итого развернутое", ("820.21", "2763.79", "5000.00", "4991.00", "829.21", "2763.79"), 0),
|
||||
("Итого развернутое", ("820.21", "2763.79", None, None, "829.21", "2763.79"), 0),
|
||||
("Итого", (None, "1943.58", "5000.00", "4991.00", None, "1934.58"), 0),
|
||||
]
|
||||
return [_flat_sheet(texts, 5, rows)]
|
||||
@ -938,7 +938,7 @@ def _s42() -> list[SheetSpec]:
|
||||
("76.5", (None, "300.00", "700.00", "600.00", None, "200.00"), 0),
|
||||
("76.Н", (None, None, "5000.00", "5000.00", None, None), 0),
|
||||
("76.Н.1", (None, None, "5000.00", "5000.00", None, None), 0),
|
||||
("Итого развернутое", ("1000.00", "300.00", "5700.00", "6600.00", None, "200.00"), 0),
|
||||
("Итого развернутое", ("1000.00", "300.00", None, None, None, "200.00"), 0),
|
||||
("Итого", ("700.00", None, "5700.00", "6600.00", None, "200.00"), 0),
|
||||
]
|
||||
return [_flat_sheet(texts, 8, rows)]
|
||||
|
||||
@ -257,6 +257,39 @@ def test_total_check_uses_main_total_when_secondary_comes_first() -> None:
|
||||
assert not any(note.code == "OSV_TOTAL" for note in notes)
|
||||
|
||||
|
||||
SECONDARY = [Pattern.compile(r"^итого\s+развернут")]
|
||||
# «Итого развернутое» в 1С несёт только развёрнутые сальдо, без оборотов: тождество строки на ней не выполняется
|
||||
SECONDARY_ROW: Sums = ("100", "10", None, None, "120", "5")
|
||||
|
||||
|
||||
def _secondary(number: int, sums: Sums) -> ClassifiedRow:
|
||||
return row(number, sums, group_type="TOTAL", text="Итого развернутое")
|
||||
|
||||
|
||||
def test_balance_identity_skips_secondary_total_without_turnovers() -> None:
|
||||
rows = [*tree(), _secondary(11, SECONDARY_ROW)]
|
||||
|
||||
assert balance_identity(rows, secondary_totals=SECONDARY) == []
|
||||
checks, notes = run_checks(rows, secondary_totals=SECONDARY)
|
||||
assert checks.balance_identity_violations == []
|
||||
assert not any(note.code == "OSV_BALANCE_IDENTITY" for note in notes)
|
||||
|
||||
|
||||
def test_balance_identity_still_checks_main_total_with_secondary_present() -> None:
|
||||
broken: Sums = ("100", None, "50", "30", "121", None)
|
||||
rows = [account(7, ROOT), total(8, broken), _secondary(9, SECONDARY_ROW)]
|
||||
|
||||
assert balance_identity(rows, secondary_totals=SECONDARY) == [8]
|
||||
|
||||
|
||||
def test_balance_identity_without_secondary_patterns_checks_every_total() -> None:
|
||||
rows = [*tree(), _secondary(11, SECONDARY_ROW)]
|
||||
|
||||
assert balance_identity(rows) == [11]
|
||||
checks, _ = run_checks(rows)
|
||||
assert checks.balance_identity_violations == [11]
|
||||
|
||||
|
||||
def test_total_matches_first_root_account() -> None:
|
||||
rows = tree()
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ def test_flat_list_s40(rules: Rules) -> None:
|
||||
["Август - Агро ООО", 1000, None, 500, 300, 1200, None],
|
||||
["АВИС ООО", None, 200, 100, 400, None, 500],
|
||||
["Агроаспект ООО", None, None, 50, 50, None, None],
|
||||
["Итого развернутое", 1000, 200, 650, 750, 1200, 500],
|
||||
["Итого развернутое", 1000, 200, None, None, 1200, 500],
|
||||
["Итого", 800, None, 650, 750, 700, None],
|
||||
]
|
||||
|
||||
@ -73,7 +73,7 @@ def test_flat_list_s41(rules: Rules) -> None:
|
||||
*_table_head(),
|
||||
["Автодор-Платные Дороги ООО", 820, None, 5000, 4991, 829, None],
|
||||
["АГРОПРОФИ ООО", None, 2764, None, None, None, 2764],
|
||||
["Итого развернутое", 820, 2764, 5000, 4991, 829, 2764],
|
||||
["Итого развернутое", 820, 2764, None, None, 829, 2764],
|
||||
["Итого", None, 1944, 5000, 4991, None, 1935],
|
||||
]
|
||||
|
||||
|
||||
@ -89,11 +89,12 @@ r11 Август - Агро ООО | … суммы … ← с
|
||||
| 8 | R-8 | 19.10.2026 | «Учредители» на счёте 80 не трогаем: именованная группировка остаётся OTHER (R-6 сохраняется) | Принято по рекомендации сессии при одобрении запуска владельцем; именованные группировки правило расчётных счетов не переопределяет | Учредители не станут COUNTERPARTY автоматически — гудвилл (§4 п. 2) потребует ручного сопоставления |
|
||||
| 9 | R-9 | 19.10.2026 | Счета 71 и 73 исключены из `counterparty_accounts`; итог: 58, 60, 62, 66, 67, 75, 76, 80 | Подотчётные и персонал — физлица, а не контрагенты (OQ-158); принято по рекомендации сессии | Контрагенты на 71/73 без названия вида останутся OTHER и не попадут в сопоставление |
|
||||
| 10 | R-10 | 19.10.2026 | «Организации» — синоним COUNTERPARTY в `osv_groups.yaml` (`^организаци`) | Вид субконто письма (сч. 60) — организации-контрагенты; принято по рекомендации сессии | Другой смысл вида «Организации» на иных счетах даст COUNTERPARTY; видно в golden/предупреждениях |
|
||||
| 11 | R-11 | 19.10.2026 | Данные синтетики s40, s41, s42 исправлены: у «Итого развернутое» добавлены обороты, равные суммам строк (s40: 650/750, s41: 5000/4991, s42: 5700/6600) | В плане у этой строки обороты пусты, поэтому тождество строки (начало + обороты = конец) не сходилось, и файлы не могли стать `done` независимо от кода; бриф плана ошибочен | s40–s42 расходятся с исходным текстом плана. **Приёмка Task 6 показала:** в реальных файлах контура у «Итого развернутое» обороты тоже пусты (`45e06f7d`, `638ecb3d`, `7e49f8b5`, `f2e0e04e`, `270b41da`, `738bfcf5`, письмо), тождество на этой строке остаётся нарушенным, и файл уходит в `needs_check`: правка данных синтетики скрыла дефект кода, а не данных; решается отдельно (см. отчёт task-6) |
|
||||
| 11 | R-11 (отменён R-16) | 19.10.2026 | Данные синтетики s40, s41, s42 исправлены: у «Итого развернутое» добавлены обороты, равные суммам строк (s40: 650/750, s41: 5000/4991, s42: 5700/6600) | В плане у этой строки обороты пусты, поэтому тождество строки (начало + обороты = конец) не сходилось, и файлы не могли стать `done` независимо от кода; бриф плана ошибочен | s40–s42 расходятся с исходным текстом плана. **Приёмка Task 6 показала:** в реальных файлах контура у «Итого развернутое» обороты тоже пусты (`45e06f7d`, `638ecb3d`, `7e49f8b5`, `f2e0e04e`, `270b41da`, `738bfcf5`, письмо), тождество на этой строке остаётся нарушенным, и файл уходит в `needs_check`: правка данных синтетики скрыла дефект кода, а не данных; решается отдельно (см. отчёт task-6). **Отменён рулингом R-16:** данные s40–s42 возвращены к исходному тексту плана |
|
||||
| 12 | R-12 | 19.10.2026 | Строка вида субконто не даёт группировку из ячеек-меток шапки («Субконто», «Счет»), меток валюты («Валюта БУ, НУ») и ячеек, на которые срабатывает паттерн `currency` | Ячейка между названием и периодом может быть меткой шапки, а не видом субконто; иначе в схему попали бы ложные группировки (T2.2, ревью задачи 2) | Метка, совпавшая с настоящим видом субконто, не станет группировкой; вид «Организации» уже учтён (R-10) |
|
||||
| 13 | R-13 | 19.10.2026 | Предупреждение `OSV_ROWS` «вид субконто в шапке не назван…» — одно на каждый различный `account_code_raw` строк, принятых по плану счетов | План требовал «один раз на документ»; у листа с несколькими субсчетами (62.01, 62.02) заметка с точным кодом полезнее одной общей | Больше заметок на листах с несколькими субсчетами (лист `67 сч.` — четыре); на статус не влияют (`info`) |
|
||||
| 14 | R-14 | 19.10.2026 | `Expect.data_type` в `test_synthetic_files.py` стал `str \| None`: у s40–s44 ожидается `None` | В шапке нет БУ/НУ; умолчание BU — зона трека A (`data_type`) | При слиянии трека A ожидания s40–s44 придётся поправить на `BU` |
|
||||
| 15 | R-15 | 19.10.2026 | Предупреждение `OSV_TOTAL` при несходимости «Итого» без корня ACCOUNT, но со строками уровня 0, говорит «≠ сумме N строк уровня 0» (T4.1) | Прежний текст «корневого счёта нет» вводил в заблуждение для плоского списка | Текст в `metrics.warnings` изменился; снимков с прежним текстом нет |
|
||||
| 16 | R-16 | 19.10.2026 | Вторые итоговые строки (`total_row_secondary`, «Итого развернутое») исключены из проверки тождества строки; сверка по основной «Итого» сохранена; R-11 отменён | В 1С «Итого развернутое» содержит только развёрнутые сальдо, без оборотов (подтверждено 7 реальными файлами контура: `638ecb3d`, `45e06f7d`, `f2e0e04e`, `7e49f8b5`, `270b41da`, `738bfcf5`, письмо); решение владельца: «Итого развернутое» — итоговая строка в любом порядке | Тождество на вторых итогах не проверяется: ошибка в сальдо этой строки не будет поймана; основная «Итого» по-прежнему проверяется тождеством и сверкой с корнем/суммой уровня 0 |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
|
||||
@ -161,6 +161,9 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
||||
`OSV_XLSB_NO_OUTLINE`); строк вида `sheet=<имя> <текст>` в `metrics.warnings` больше нет. Парсер MUST NOT подгонять
|
||||
структуру под сходимость.
|
||||
|
||||
Вторые итоговые строки (`total_row_secondary`, «Итого развернутое») SHALL NOT участвовать в проверке тождества строки:
|
||||
в 1С они несут только развёрнутые сальдо, без оборотов (R-16); основная «Итого» проверяется тождеством как прежде.
|
||||
|
||||
Исключение — **счета без сальдо по аналитике** (решение владельца 07.10.2026, OQ-132): у 91 и 99 строки аналитики несут
|
||||
только обороты, сальдо закрыто на субсчёт закрытия (91.09). Список счетов — данные `rules/osv_checks.yaml`
|
||||
(`accounts_without_analytic_balance`, начальный состав `91`, `99`; 90 не входит). Строка относится к такому счёту, если
|
||||
@ -228,6 +231,11 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
||||
- **THEN** обе итоговые строки — TOTAL уровня 0 без родителя, «Итого развернутое» не ребёнок «76.Н.1»;
|
||||
`total_row_matches_root = true` (сверка по «Итого»), нарушений нет, `status = done`
|
||||
|
||||
#### Scenario: «Итого развернутое» без оборотов
|
||||
- **WHEN** у «Итого развернутое» заполнены только сальдо (обороты пусты), а у основной «Итого» тождество строки выполняется
|
||||
- **THEN** `balance_identity_violations` не содержит «Итого развернутое», `status = done`; при нарушении тождества у основной
|
||||
«Итого» её `row_number` в списке остаётся
|
||||
|
||||
#### Scenario: «Итого» плоского списка
|
||||
- **WHEN** разбирается синтетическая ОСВ `s40` («Итого» = сумма контрагентов, сальдо «Итого» свёрнуто)
|
||||
- **THEN** `total_row_matches_root = true`, `status = done`; если сумма одного контрагента испорчена — `false`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user