Плоская форма ОСВ 1С: счёт из шапки, «Итого развернутое», плоский список, контрагенты расчётных счетов (#59) #63
@ -94,14 +94,22 @@ def balance_identity(
|
||||
Строки счетов без сальдо по аналитике (`exempt`, OQ-132) не проверяются. Вторая итоговая строка
|
||||
(`secondary_totals`, «Итого развернутое») пропускается, только если у неё нет оборотов (обе колонки пусты) и она не
|
||||
единственная TOTAL листа: «Итого развернутое» перед «Итого» несёт одни развёрнутые сальдо (#59, R-19). С оборотами
|
||||
или единственная — проверяется как обычная строка.
|
||||
или единственная — проверяется как обычная строка. Итоговые строки-показатели («НУ»), идущие блоком сразу за
|
||||
пропущенной второй итоговой строкой, пропускаются на тех же условиях (R-21); в счётчик TOTAL показатели не входят.
|
||||
"""
|
||||
violations: list[int] = []
|
||||
totals = sum(1 for item in rows if item.group_type == "TOTAL")
|
||||
totals = sum(1 for item in rows if item.group_type == "TOTAL" and not item.indicator)
|
||||
in_skipped_block = False
|
||||
for item in rows:
|
||||
if item.group_type == "TOTAL" and item.indicator:
|
||||
if in_skipped_block and not _has_turnovers(item):
|
||||
continue
|
||||
else:
|
||||
in_skipped_block = False
|
||||
if _no_analytic_balance(item, exempt):
|
||||
continue
|
||||
if _is_secondary_total(item, secondary_totals) and totals > 1 and not _has_turnovers(item):
|
||||
in_skipped_block = True
|
||||
continue
|
||||
sums = _values(item)
|
||||
expected = _net(sums[_OPENING]) + _net(sums[_TURNOVER])
|
||||
|
||||
@ -201,13 +201,16 @@ def _trailing_totals(sheet: Sheet, header: TableHeader, rows: Sequence[int], tot
|
||||
def _leading_totals(sheet: Sheet, header: TableHeader, rows: Sequence[int], total: int, rules: Rules) -> int:
|
||||
"""Номер самой верхней из вторых итоговых строк сразу перед основной «Итого» (она сама, если их нет).
|
||||
|
||||
Плоская форма 1С (#59): «Итого развернутое» стоит перед «Итого» — вместе они итоговый блок.
|
||||
Плоская форма 1С (#59): «Итого развернутое» стоит перед «Итого» — вместе они итоговый блок. Строки-показатели
|
||||
(«НУ» под «Итого развернутое») шагу не мешают, но в блок входят, только если выше них найдена вторая итоговая
|
||||
строка (R-21): «НУ» под последней строкой данных в блок не втягивается.
|
||||
"""
|
||||
first = total
|
||||
for row in reversed(rows[: rows.index(total)]):
|
||||
if not _is_secondary_total(sheet.text_at(row, header.text_col), rules):
|
||||
break
|
||||
if _is_secondary_total(sheet.text_at(row, header.text_col), rules):
|
||||
first = row
|
||||
elif not is_indicator_total(sheet, row, header, rules):
|
||||
break
|
||||
return first
|
||||
|
||||
|
||||
|
||||
@ -186,8 +186,8 @@ def test_flat_list_with_broken_total_needs_check(rules: Rules) -> None:
|
||||
def test_flat_list_with_indicator_column_and_second_total(rules: Rules) -> None:
|
||||
"""Стык треков A и B (#58, #59): колонка «Показатели», «Итого развернутое» перед «Итого» и строка НУ после неё.
|
||||
|
||||
Показатели не становятся контрагентами (R-6, R-18), не попадают в тождество строки (R-19) и не сбивают сверку
|
||||
«Итого» (она идёт по основной «Итого» с меткой БУ).
|
||||
Показатели не становятся контрагентами (R-6, R-18) и не сбивают сверку «Итого» (она идёт по основной «Итого» с
|
||||
меткой БУ); тождество строки у них проверяется, как у всех (спека трека A) — строки НУ его соблюдают.
|
||||
"""
|
||||
rows: list[list[object]] = [
|
||||
[],
|
||||
@ -228,3 +228,87 @@ def test_flat_list_with_indicator_column_and_second_total(rules: Rules) -> None:
|
||||
"TOTAL",
|
||||
"TOTAL",
|
||||
]
|
||||
|
||||
|
||||
def _indicator_flat_sheet(tail: list[list[object]]) -> list[list[object]]:
|
||||
"""Плоская форма 1С с колонкой «Показатели»: два контрагента с БУ/НУ, дальше — итоговый блок `tail`."""
|
||||
return [
|
||||
[],
|
||||
["Оборотно-сальдовая ведомость по счету: 76"],
|
||||
["Контрагенты"],
|
||||
["за 2025 г."],
|
||||
['ООО "Тест"'],
|
||||
[],
|
||||
[
|
||||
"Субконто",
|
||||
"Показатели",
|
||||
M("Сальдо на начало периода", 2),
|
||||
M("Обороты за период", 2),
|
||||
M("Сальдо на конец периода", 2),
|
||||
],
|
||||
[None, None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
|
||||
["Август - Агро ООО", "БУ", 1000, None, 500, 300, 1200, None],
|
||||
[None, "НУ", 900, None, 400, 300, 1000, None],
|
||||
["АВИС ООО", "БУ", None, 200, 100, 400, None, 500],
|
||||
[None, "НУ", None, 150, 100, 350, None, 400],
|
||||
*tail,
|
||||
]
|
||||
|
||||
|
||||
def test_indicator_under_second_total_before_total_joins_total_block(rules: Rules) -> None:
|
||||
"""I-6, R-21: НУ без объединения под «Итого развернутое» перед «Итого» — итоговый блок, не фиктивный контрагент."""
|
||||
rows = _indicator_flat_sheet(
|
||||
[
|
||||
["Итого развернутое", "БУ", 1000, 200, None, None, 1200, 500],
|
||||
[None, "НУ", 900, 150, None, None, 1000, 400],
|
||||
["Итого", "БУ", 1000, 200, 600, 700, 1200, 500],
|
||||
[None, "НУ", 900, 150, 500, 650, 1000, 400],
|
||||
]
|
||||
)
|
||||
|
||||
document, _ = _parse(rules, rows)
|
||||
|
||||
assert document.status == "done", [note.message for note in document.warnings]
|
||||
assert [item.row_value for item in document.rows if item.group_type == "COUNTERPARTY"] == [
|
||||
"Август - Агро ООО",
|
||||
"АВИС ООО",
|
||||
]
|
||||
secondary = next(item for item in document.rows if item.row_value == "Итого развернутое")
|
||||
assert secondary.group_type == "TOTAL"
|
||||
assert document.checks is not None
|
||||
assert document.checks.balance_identity_violations == []
|
||||
|
||||
|
||||
def test_indicator_under_last_data_row_is_not_pulled_into_total_block(rules: Rules) -> None:
|
||||
"""R-21, негативный: НУ под последней строкой данных перед «Итого» остаётся показателем этой строки."""
|
||||
rows = _indicator_flat_sheet(
|
||||
[
|
||||
["Итого", "БУ", 1000, 200, 600, 700, 1200, 500],
|
||||
[None, "НУ", 900, 150, 500, 650, 1000, 400],
|
||||
]
|
||||
)
|
||||
|
||||
document, _ = _parse(rules, rows)
|
||||
|
||||
nu_under_data = [item for item in document.rows if item.data_type == "NU" and item.group_type != "TOTAL"]
|
||||
assert len(nu_under_data) == NU_ROWS - 1
|
||||
assert document.status == "done", [note.message for note in document.warnings]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("secondary_first", [True, False])
|
||||
def test_indicators_of_second_total_without_turnovers_skip_identity(rules: Rules, secondary_first: bool) -> None:
|
||||
"""I-7, R-21: «Итого развернутое» без оборотов со своим НУ не даёт нарушений тождества (до и после «Итого»)."""
|
||||
secondary: list[list[object]] = [
|
||||
["Итого развернутое", "БУ", 1000, 200, None, None, 1200, 500],
|
||||
[None, "НУ", 900, 150, None, None, 1000, 400],
|
||||
]
|
||||
main: list[list[object]] = [
|
||||
["Итого", "БУ", 1000, 200, 600, 700, 1200, 500],
|
||||
[None, "НУ", 900, 150, 500, 650, 1000, 400],
|
||||
]
|
||||
|
||||
document, _ = _parse(rules, _indicator_flat_sheet([*secondary, *main] if secondary_first else [*main, *secondary]))
|
||||
|
||||
assert document.status == "done", [note.message for note in document.warnings]
|
||||
assert document.checks is not None
|
||||
assert document.checks.balance_identity_violations == []
|
||||
|
||||
@ -100,6 +100,7 @@ r11 Август - Агро ООО | … суммы … ← с
|
||||
| 17 | R-17 (заменяет R-10) | 19.10.2026 | Синоним вида субконто сужен до множественного числа: `^организации\b` → COUNTERPARTY; единственное «Организация» остаётся OTHER и в схему не добавляется | «Организация» в шапке вариантов 1/2 — собственная организация учёта (измерение отчитывающейся организации); на контуре `dae56120` (сч. 83) и `02c5df51` (сч. 80) схема `[Счет, Организация]` из-за широкого паттерна стала `[ACCOUNT, COUNTERPARTY]` (финальное ревью I-1) | Прочие формы написания («Организации-контрагенты» и т. п.) не узнаются; вид во множественном числе с иным смыслом на нерасчётном счёте даст COUNTERPARTY (OQ-171) |
|
||||
| 18 | R-18 | 19.10.2026 | Правило расчётных счетов (`counterparty_accounts`) не применяется к строке, которую словарь групп явно знает как не-COUNTERPARTY; паттерн валюты расширен до `^(в\s+)?валют` | Строки «Валюта», «Валюта CNY/EUR/USD/руб», «В валюте» под 76.55 становились контрагентами (`270b41da`, `738bfcf5`; финальное ревью I-2) | Контрагент с названием на «Валют…» («Валютные решения ООО») остаётся OTHER без имени и не попадёт в сопоставление |
|
||||
| 19 | R-19 | 19.10.2026 | Тождество у второй итоговой строки пропускается, только если обе колонки оборотов пусты и на листе есть другая TOTAL; иначе строка проверяется как обычная | На контуре «Итого развернутое» перед «Итого» — без оборотов, после «Итого» — с оборотами и сходящимся тождеством; R-16 пропускал обе (финальное ревью I-4) | Вторая итоговая без оборотов и с ошибочным сальдо не ловится; единственная итоговая без оборотов проверяется, и пустое тождество может дать нарушение |
|
||||
| 20 | R-21 | 19.10.2026 | Шаг вверх от «Итого» проходит строки-показатели, но включает их в итоговый блок, только если выше найдена вторая итоговая строка; показатели второй итоговой строки без оборотов наследуют пропуск тождества (R-19); счётчик TOTAL в тождестве не считает показатели | 1С штатно даёт «Показатели БУ, НУ» + «Итого развернутое»: без шага «Итого развернутое» оставалась в теле и становилась фиктивным контрагентом, а НУ давала нарушение тождества (ревью интеграции I-6, I-7, M-13) | Ошибка сальдо показателя второй итоговой строки не ловится |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
пробел или его отсутствие, R-18; цифры — только 0–9; название после запятой может занимать несколько строк ячейки — M-1);
|
||||
`account_code_raw` — код как в файле, без названия после запятой. Не-счёт — группировка из `group_schema` по позиции
|
||||
(число не-счётных предков после последнего ACCOUNT + 1), при выходе за схему — OTHER, кроме **расчётных счетов**: если позиция 1 (первая не-счётная) выходит за схему, а счёт ветки — из списка `counterparty_accounts` `rules/osv_groups.yaml` (начальный состав 58, 60, 62, 66, 67, 75, 76, 80; запись `76` покрывает `76`, `76.2`, `76.Н.1`, но не `760`), строка SHALL получить COUNTERPARTY без имени группировки, а документ — предупреждение `OSV_ROWS` «вид субконто в шапке не назван: строки аналитики счёта <код> приняты за контрагентов по плану счетов — проверьте» (один раз на счёт); счёт ветки строк до первой строки-счёта — код счёта шапки; строка ИНН без пары — OTHER;
|
||||
строка «Итого» и вторые итоговые строки («Итого развернутое») сразу перед ней или сразу за ней — TOTAL.
|
||||
строка «Итого» и вторые итоговые строки («Итого развернутое») в итоговом блоке сразу перед ней или сразу за ней, включая строки-показатели итоговых строк, — TOTAL; строки-показатели входят в блок перед «Итого», только если выше них стоит вторая итоговая строка.
|
||||
|
||||
Для COUNTERPARTY `counterparty_name` SHALL быть текстом строки без ИНН в конце и разделителя перед ним (`row_value` —
|
||||
исходный текст; OQ-100), а `counterparty_inn` — первым найденным из:
|
||||
@ -254,6 +254,12 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
||||
- **WHEN** «Итого развернутое» стоит после «Итого», у неё заполнены обороты, и сальдо на конец не сходится с началом и оборотами
|
||||
- **THEN** её `row_number` попадает в `balance_identity_violations`; при сходящемся тождестве нарушения нет
|
||||
|
||||
#### Scenario: Показатели второй итоговой строки без оборотов
|
||||
- **WHEN** колонка «Показатели», «Итого развернутое» без оборотов (до или после «Итого») и под ней строка «НУ» без оборотов,
|
||||
на листе есть основная «Итого»
|
||||
- **THEN** строка «НУ» блока пропускает тождество строки так же, как «Итого развернутое»; `balance_identity_violations`
|
||||
пуст, `status = done`; строка «НУ» под последней строкой данных в итоговый блок не входит
|
||||
|
||||
#### Scenario: «Итого» плоского списка
|
||||
- **WHEN** разбирается синтетическая ОСВ `s40` («Итого» = сумма контрагентов, сальдо «Итого» свёрнуто)
|
||||
- **THEN** `total_row_matches_root = true`, `status = done`; если сумма одного контрагента испорчена — `false`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user