ОСВ: ложное «Требует проверки» — периоды, нетто-ребёнок, иерархия по отступам (AURORA-1823, 1863, 1858) #79
@ -9,7 +9,10 @@
|
||||
Обороты сверяются по колонкам. Сальдо (начало, конец) — по колонкам (развёрнутое сальдо) либо, если у родителя
|
||||
сальдо свёрнуто (не больше одной ненулевой стороны), по нетто `дебет − кредит`: 1С показывает счёт свёрнутым
|
||||
сальдо, а субсчета и контрагентов — развёрнутым (так во всех реальных файлах, где у счёта есть субсчета с
|
||||
разными сторонами сальдо);
|
||||
разными сторонами сальдо). Нетто допустимо и у развёрнутого родителя, если среди его прямых детей есть
|
||||
«нетто-ребёнок» — ребёнок, прошедший сверку только по нетто, или имеющий такого потомка (1С схлопнула Дт/Кт
|
||||
субсчёта в нетто, а в родителе они остались развёрнутыми, AURORA-1863); признак идёт вверх по дереву. Развёрнутый
|
||||
родитель без нетто-ребёнка сверяется по колонкам;
|
||||
3. строка «Итого» (TOTAL) = корневая строка ACCOUNT (первая строка уровня 0) либо сумма всех строк уровня 0, если их
|
||||
несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03; OQ-106, OQ-133; сальдо —
|
||||
по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя в п. 2; то же для единственного корня, когда
|
||||
@ -119,23 +122,35 @@ def balance_identity(
|
||||
return violations
|
||||
|
||||
|
||||
def _balance_matches(parent: Sequence[Decimal], children: Sequence[Decimal]) -> bool:
|
||||
"""Пара «дебет, кредит» сальдо: равна по колонкам или, при свёрнутом сальдо родителя, по нетто."""
|
||||
def _balance_matches(parent: Sequence[Decimal], children: Sequence[Decimal], *, net_child: bool = False) -> bool:
|
||||
"""Пара «дебет, кредит» сальдо: равна по колонкам или по нетто — при свёрнутом сальдо родителя либо когда среди
|
||||
его детей есть свёрнутый в нетто (`net_child`, AURORA-1863)."""
|
||||
if not _differs(parent, children):
|
||||
return True
|
||||
is_collapsed = any(abs(value) <= TOLERANCE for value in parent)
|
||||
return is_collapsed and abs(_net(parent) - _net(children)) <= TOLERANCE
|
||||
return (is_collapsed or net_child) and abs(_net(parent) - _net(children)) <= TOLERANCE
|
||||
|
||||
|
||||
def _sums_match(parent: Sequence[Decimal], children: Sequence[Decimal], *, with_balance: bool = True) -> bool:
|
||||
def _sums_match(
|
||||
parent: Sequence[Decimal],
|
||||
children: Sequence[Decimal],
|
||||
*,
|
||||
with_balance: bool = True,
|
||||
net_child: bool = False,
|
||||
) -> bool:
|
||||
"""Суммы родителя = Σ детей; `with_balance=False` — только обороты (сальдо детей не несёт данных)."""
|
||||
return (
|
||||
(not with_balance or _balance_matches(parent[_OPENING], children[_OPENING]))
|
||||
(not with_balance or _balance_matches(parent[_OPENING], children[_OPENING], net_child=net_child))
|
||||
and not _differs(parent[_TURNOVER], children[_TURNOVER])
|
||||
and (not with_balance or _balance_matches(parent[_CLOSING], children[_CLOSING]))
|
||||
and (not with_balance or _balance_matches(parent[_CLOSING], children[_CLOSING], net_child=net_child))
|
||||
)
|
||||
|
||||
|
||||
def _balance_by_net(parent: Sequence[Decimal], children: Sequence[Decimal]) -> bool:
|
||||
"""Сальдо родителя (начало или конец) не равно Σ детей по колонкам: сверка прошла только по нетто."""
|
||||
return _differs(parent[_OPENING], children[_OPENING]) or _differs(parent[_CLOSING], children[_CLOSING])
|
||||
|
||||
|
||||
def parent_sums(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = None) -> list[int]:
|
||||
"""Номера родителей, чьи суммы ≠ Σ детей того же `data_type` (±0.01): обороты — по колонкам, сальдо — по колонкам
|
||||
или по нетто при свёрнутом сальдо родителя (см. описание модуля).
|
||||
@ -149,6 +164,7 @@ def parent_sums(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = No
|
||||
"""
|
||||
by_row = {item.body.row: item for item in rows}
|
||||
totals: dict[int, list[Decimal]] = defaultdict(lambda: [_ZERO] * len(SUM_FIELDS))
|
||||
children_of: dict[int, list[int]] = defaultdict(list)
|
||||
has_subaccounts: set[int] = set()
|
||||
for item in rows:
|
||||
if item.parent is None or _is_indicator(item):
|
||||
@ -159,18 +175,22 @@ def parent_sums(rows: Sequence[ClassifiedRow], exempt: OsvCheckRules | None = No
|
||||
accumulated = totals[item.parent]
|
||||
for index, value in enumerate(_values(item)):
|
||||
accumulated[index] += value
|
||||
children_of[item.parent].append(item.body.row)
|
||||
if item.group_type == "ACCOUNT":
|
||||
has_subaccounts.add(item.parent)
|
||||
return [
|
||||
row
|
||||
for row in by_row
|
||||
if row in totals
|
||||
and not _sums_match(
|
||||
_values(by_row[row]),
|
||||
totals[row],
|
||||
with_balance=row in has_subaccounts or not _no_analytic_balance(by_row[row], exempt),
|
||||
)
|
||||
]
|
||||
violations: set[int] = set()
|
||||
# «Свёрнутые в нетто» строки: прошли сверку с детьми только по нетто либо имеют такого потомка. Потомки идут по
|
||||
# большим номерам строк, поэтому родители обходятся от больших номеров к меньшим.
|
||||
netted: set[int] = set()
|
||||
for row in sorted(totals, reverse=True):
|
||||
item = by_row[row]
|
||||
with_balance = row in has_subaccounts or not _no_analytic_balance(item, exempt)
|
||||
net_child = any(child in netted for child in children_of[row])
|
||||
if not _sums_match(_values(item), totals[row], with_balance=with_balance, net_child=net_child):
|
||||
violations.add(row)
|
||||
elif net_child or (with_balance and _balance_by_net(_values(item), totals[row])):
|
||||
netted.add(row)
|
||||
return [row for row in by_row if row in violations]
|
||||
|
||||
|
||||
def _total(rows: Sequence[ClassifiedRow], secondary: Sequence[Pattern] = ()) -> ClassifiedRow | None:
|
||||
|
||||
@ -186,6 +186,60 @@ def test_expanded_parent_balance_must_match_by_columns() -> None:
|
||||
assert parent_sums(rows) == [12]
|
||||
|
||||
|
||||
def netted_child_tree(parent_opening: tuple[str, str] = ("130", "70"), parent_turnover: tuple[str, str] = ("10", "10")):
|
||||
"""Развёрнутый счёт 12 над свёрнутым субсчётом 13 (нетто 60 Дт), чьи внуки 14/15 развёрнуты (90 Дт / 30 Кт):
|
||||
1С схлопнула Дт/Кт субсчёта в нетто, в родителе они остались развёрнутыми (AURORA-1863)."""
|
||||
opening_debit, opening_credit = parent_opening
|
||||
turnover_debit, turnover_credit = parent_turnover
|
||||
return [
|
||||
account(12, (opening_debit, opening_credit, turnover_debit, turnover_credit, opening_debit, opening_credit)),
|
||||
row(13, ("60", None, "10", "10", "60", None), level=1, parent=12, group_type="ACCOUNT"),
|
||||
row(14, ("90", None, "10", None, "90", None), level=2, parent=13, group_type="ACCOUNT"),
|
||||
row(15, (None, "30", None, "10", None, "30"), level=2, parent=13, group_type="ACCOUNT"),
|
||||
]
|
||||
|
||||
|
||||
def test_expanded_parent_over_netted_child_is_compared_by_net() -> None:
|
||||
"""AURORA-1863: у ребёнка сальдо свёрнуто относительно его детей (прошёл только по нетто) — развёрнутый
|
||||
родитель над ним тоже сверяется по нетто."""
|
||||
assert parent_sums(netted_child_tree()) == []
|
||||
|
||||
|
||||
def test_expanded_parent_over_netted_child_with_wrong_net_is_violation() -> None:
|
||||
assert parent_sums(netted_child_tree(parent_opening=("131", "70"))) == [12]
|
||||
|
||||
|
||||
def test_expanded_parent_over_netted_child_turnovers_still_by_columns() -> None:
|
||||
assert parent_sums(netted_child_tree(parent_turnover=("11", "11"))) == [12]
|
||||
|
||||
|
||||
def test_expanded_parent_without_netted_child_stays_violation_on_rounding_gap() -> None:
|
||||
"""Узость правила: расхождение 0.05 на обеих сторонах при равном нетто у развёрнутого родителя без свёрнутого
|
||||
ребёнка остаётся нарушением."""
|
||||
rows = [
|
||||
account(12, ("9977809.90", "8829025.95", None, None, "9977809.90", "8829025.95")),
|
||||
row(13, ("9977809.85", None, None, None, "9977809.85", None), level=1, parent=12, group_type="ACCOUNT"),
|
||||
row(14, (None, "8829025.90", None, None, None, "8829025.90"), level=1, parent=12, group_type="ACCOUNT"),
|
||||
]
|
||||
|
||||
assert parent_sums(rows) == [12]
|
||||
|
||||
|
||||
def test_netted_flag_propagates_up_through_column_matched_level() -> None:
|
||||
"""Признак «есть свёрнутый потомок» идёт вверх: счёт 11 развёрнут, субсчёт 12 сошёлся по колонкам, но над ним
|
||||
свёрнутый 13."""
|
||||
rows = [
|
||||
row(11, ("300", "240", "10", "10", "300", "240"), group_type="ACCOUNT", text="60"),
|
||||
row(12, ("130", "70", "10", "10", "130", "70"), level=1, parent=11, group_type="ACCOUNT"),
|
||||
row(13, ("60", None, "10", "10", "60", None), level=2, parent=12, group_type="ACCOUNT"),
|
||||
row(14, ("90", None, "10", None, "90", None), level=3, parent=13, group_type="ACCOUNT"),
|
||||
row(15, (None, "30", None, "10", None, "30"), level=3, parent=13, group_type="ACCOUNT"),
|
||||
row(16, ("70", "70", "0", "0", "70", "70"), level=2, parent=12, group_type="ACCOUNT"),
|
||||
]
|
||||
|
||||
assert parent_sums(rows) == []
|
||||
|
||||
|
||||
def test_turnovers_are_compared_by_columns_even_when_net_matches() -> None:
|
||||
rows = [
|
||||
account(12, ("16", None, "135", "112", "39", None)),
|
||||
|
||||
@ -303,6 +303,14 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
||||
- **THEN** нарушения «сумма детей = родитель» по сальдо нет; обороты при этом сверяются по колонкам, а неверное нетто
|
||||
или расхождение оборотов — нарушение
|
||||
|
||||
#### Scenario: Развёрнутый родитель над свёрнутым в нетто ребёнком
|
||||
- **WHEN** сальдо родителя развёрнуто (обе стороны), а среди его прямых детей есть «нетто-ребёнок» — ребёнок, прошедший
|
||||
сверку со своими детьми только по нетто, либо имеющий такого потомка (признак передаётся вверх по дереву), и нетто детей
|
||||
равно нетто родителя
|
||||
- **THEN** нарушения по сальдо нет (1С схлопнула Дт/Кт субсчёта в нетто, а в родителе они остались развёрнутыми,
|
||||
AURORA-1863); обороты сверяются по колонкам, неверное нетто или расхождение оборотов — нарушение; развёрнутый родитель
|
||||
без нетто-ребёнка по-прежнему сверяется по колонкам (расхождение даже на 0,05 при равном нетто — нарушение)
|
||||
|
||||
#### Scenario: Счёт 91 — аналитика только с оборотами
|
||||
- **WHEN** разбирается ОСВ по счёту 91: строки аналитики 91.01, 91.02, 91.09 несут только обороты, сальдо закрыто на
|
||||
субсчёт закрытия (`fixtures/osv-dkb/tsimlyansky/2025-Q3/91_ 9мес.2025.xls`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user