Merge pull request 'ОСВ: «Итого» свёрнута при развёрнутом корне сверяется по нетто' (#76) from feat/73-osv-expanded-total into main
This commit is contained in:
commit
b128ce2bd1
@ -12,7 +12,8 @@
|
|||||||
разными сторонами сальдо);
|
разными сторонами сальдо);
|
||||||
3. строка «Итого» (TOTAL) = корневая строка ACCOUNT (первая строка уровня 0) либо сумма всех строк уровня 0, если их
|
3. строка «Итого» (TOTAL) = корневая строка ACCOUNT (первая строка уровня 0) либо сумма всех строк уровня 0, если их
|
||||||
несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03; OQ-106, OQ-133; сальдо —
|
несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03; OQ-106, OQ-133; сальдо —
|
||||||
по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя в п. 2); корня нет
|
по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя в п. 2; то же для единственного корня, когда
|
||||||
|
корень и «Итого развернутое» развёрнуты, а «Итого» свёрнута, AURORA-1708); корня нет
|
||||||
(пустая ОСВ, R-13) — «Итого» должна быть пустой или нулевой; «Итого» не найдена (R-20) — сверка невозможна,
|
(пустая ОСВ, R-13) — «Итого» должна быть пустой или нулевой; «Итого» не найдена (R-20) — сверка невозможна,
|
||||||
результат `false`. Строки-показатели (валюта, количество, НУ) в сумму уровня 0 не входят: это другие единицы.
|
результат `false`. Строки-показатели (валюта, количество, НУ) в сумму уровня 0 не входят: это другие единицы.
|
||||||
|
|
||||||
@ -225,14 +226,51 @@ def total_matches_root(rows: Sequence[ClassifiedRow], secondary_totals: Sequence
|
|||||||
if roots:
|
if roots:
|
||||||
return _sums_match(_values(total), _sum_of(roots))
|
return _sums_match(_values(total), _sum_of(roots))
|
||||||
return all(abs(value) <= TOLERANCE for value in _values(total))
|
return all(abs(value) <= TOLERANCE for value in _values(total))
|
||||||
if not _differs(_values(total), _values(root)):
|
if _matches_single_root(rows, total, root, secondary_totals):
|
||||||
return True
|
return True
|
||||||
roots = _level_zero(rows)
|
roots = _level_zero(rows)
|
||||||
# Сумма уровня 0 — «ребёнок» итоговой строки: те же допуски, что у родителя и детей (R-21): 1С показывает «Итого»
|
# Сумма уровня 0 — «ребёнок» итоговой строки: те же допуски (R-21)
|
||||||
# свёрнутым сальдо, а субсчета — развёрнутым. Обороты по-прежнему по колонкам.
|
|
||||||
return len(roots) > 1 and _sums_match(_values(total), _sum_of(roots))
|
return len(roots) > 1 and _sums_match(_values(total), _sum_of(roots))
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_single_root(
|
||||||
|
rows: Sequence[ClassifiedRow], total: ClassifiedRow, root: ClassifiedRow, secondary: Sequence[Pattern]
|
||||||
|
) -> bool:
|
||||||
|
"""«Итого» = корень: по колонкам; корень — «ребёнок» итоговой строки, те же допуски, что у родителя и детей
|
||||||
|
(R-21, AURORA-1708): 1С показывает «Итого» свёрнутым сальдо, а счёт (и «Итого развернутое») — развёрнутым, поэтому
|
||||||
|
при свёрнутом сальдо «Итого» оно сверяется по нетто. «Итого развернутое» = корню по колонкам — основная «Итого»
|
||||||
|
сверяется с корнем по нетто сальдо и оборотам по колонкам."""
|
||||||
|
if not _differs(_values(total), _values(root)) or _sums_match(_values(total), _values(root)):
|
||||||
|
return True
|
||||||
|
expanded = _expanded_total(rows, secondary)
|
||||||
|
return (
|
||||||
|
expanded is not None
|
||||||
|
and not _differs(_values(expanded), _values(root))
|
||||||
|
and _net_matches(_values(total), _values(root))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _expanded_total(rows: Sequence[ClassifiedRow], secondary: Sequence[Pattern]) -> ClassifiedRow | None:
|
||||||
|
"""Вторая итоговая строка («Итого развернутое»): первая TOTAL не-показатель, чей текст матчит `secondary`."""
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
for item in rows
|
||||||
|
if item.group_type == "TOTAL" and not item.indicator and any(p.search(item.body.text) for p in secondary)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _net_matches(left: Sequence[Decimal], right: Sequence[Decimal]) -> bool:
|
||||||
|
"""Обороты по колонкам, сальдо начала и конца по нетто «дебет − кредит» (±0.01)."""
|
||||||
|
return (
|
||||||
|
abs(_net(left[_OPENING]) - _net(right[_OPENING])) <= TOLERANCE
|
||||||
|
and not _differs(left[_TURNOVER], right[_TURNOVER])
|
||||||
|
and abs(_net(left[_CLOSING]) - _net(right[_CLOSING])) <= TOLERANCE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _rows_text(rows: Iterable[int]) -> str:
|
def _rows_text(rows: Iterable[int]) -> str:
|
||||||
numbers = list(rows)
|
numbers = list(rows)
|
||||||
shown = ", ".join(str(number) for number in numbers[:WARNING_ROWS_SHOWN])
|
shown = ", ".join(str(number) for number in numbers[:WARNING_ROWS_SHOWN])
|
||||||
|
|||||||
@ -635,3 +635,52 @@ def test_total_skips_leading_indicator_total() -> None:
|
|||||||
rows.insert(0, row(2, ("9", None, None, None, "9", None), group_type="TOTAL", data_type="NU", indicator=True))
|
rows.insert(0, row(2, ("9", None, None, None, "9", None), group_type="TOTAL", data_type="NU", indicator=True))
|
||||||
|
|
||||||
assert total_matches_root(rows) is True
|
assert total_matches_root(rows) is True
|
||||||
|
|
||||||
|
|
||||||
|
# AURORA-1708 (#73): корень счёта развёрнут (Дт и Кт), основная «Итого» свёрнута, «Итого развернутое» = корню
|
||||||
|
_EXPANDED_ROOT: Sums = ("10", "4", "100", "70", "50", "20")
|
||||||
|
_COLLAPSED_TOTAL: Sums = ("6", None, "100", "70", "30", None)
|
||||||
|
_SECONDARY = [Pattern.compile(r"^итого\s+развернут")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsed_total_matches_expanded_root_by_net() -> None:
|
||||||
|
"""(б) Только свёрнутая «Итого»: обороты по колонкам, сальдо по нетто — корень из одной строки тоже сходится."""
|
||||||
|
rows = [account(7, _EXPANDED_ROOT), total(8, _COLLAPSED_TOTAL)]
|
||||||
|
|
||||||
|
assert total_matches_root(rows) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_expanded_secondary_total_equal_to_root_with_collapsed_total() -> None:
|
||||||
|
"""(а) «Итого развернутое» = корню по колонкам, основная «Итого» свёрнута: предупреждения нет."""
|
||||||
|
rows = [account(7, _EXPANDED_ROOT), total(8, _COLLAPSED_TOTAL), _secondary(9, _EXPANDED_ROOT)]
|
||||||
|
_, warnings = run_checks(rows, secondary_totals=_SECONDARY)
|
||||||
|
|
||||||
|
assert total_matches_root(rows, _SECONDARY) is True
|
||||||
|
assert not [w for w in warnings if "Итого" in w.message]
|
||||||
|
|
||||||
|
|
||||||
|
def test_expanded_secondary_total_before_collapsed_total() -> None:
|
||||||
|
"""(а) То же в плоской форме: «Итого развернутое» стоит перед «Итого»."""
|
||||||
|
rows = [account(7, _EXPANDED_ROOT), _secondary(8, _EXPANDED_ROOT), total(9, _COLLAPSED_TOTAL)]
|
||||||
|
|
||||||
|
assert total_matches_root(rows, _SECONDARY) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_collapsed_total_with_real_net_difference_still_fails() -> None:
|
||||||
|
"""(в) Нетто «Итого» не совпало с корнем — расхождение остаётся, даже если «Итого развернутое» = корню."""
|
||||||
|
wrong_net: Sums = ("7", None, "100", "70", "30", None)
|
||||||
|
wrong_turnover: Sums = ("6", None, "101", "70", "30", None)
|
||||||
|
|
||||||
|
assert total_matches_root([account(7, _EXPANDED_ROOT), total(8, wrong_net)]) is False
|
||||||
|
assert total_matches_root([account(7, _EXPANDED_ROOT), total(8, wrong_turnover)]) is False
|
||||||
|
rows = [account(7, _EXPANDED_ROOT), total(8, wrong_net), _secondary(9, _EXPANDED_ROOT)]
|
||||||
|
assert total_matches_root(rows, _SECONDARY) is False
|
||||||
|
_, warnings = run_checks(rows, secondary_totals=_SECONDARY)
|
||||||
|
assert any("≠ корневому счёту" in w.message for w in warnings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_expanded_total_differing_from_expanded_root_still_fails() -> None:
|
||||||
|
"""Развёрнутая «Итого» (обе стороны) с другим распределением Дт/Кт нетто не спасает: свёрнутость — у «Итого»."""
|
||||||
|
shifted: Sums = ("16", "10", "100", "70", "50", "20")
|
||||||
|
|
||||||
|
assert total_matches_root([account(7, _EXPANDED_ROOT), total(8, shifted)]) is False
|
||||||
|
|||||||
@ -231,7 +231,7 @@ turnover_credit = closing_debit − closing_credit` (±0.01, `null` = 0); по
|
|||||||
`data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у
|
`data_type` (±0.01), причём **обороты** сверяются по колонкам, а **сальдо** (начало, конец) — по колонкам либо, если у
|
||||||
родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто,
|
родителя сальдо свёрнуто (не больше одной ненулевой стороны), по нетто «дебет − кредит» (R-21: 1С показывает счёт нетто,
|
||||||
а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка
|
а субсчета и контрагентов — развёрнуто; остаточные риски перечислены ниже); строки-показатели (требование «Строки-показатели и колонка
|
||||||
«Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0) либо сумма строк уровня 0; основная «Итого» — первая строка TOTAL без признака показателя, текст которой не «Итого развернутое» (`total_row_secondary`), в каком бы порядке они ни стояли; итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если корневой ACCOUNT нет, а строки уровня 0 есть (плоский список), «Итого» SHALL сверяться с их суммой (те же допуски R-21); если строк данных нет (пустая ОСВ, R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в
|
«Показатели»») в суммирование не входят и сами родителями не проверяются; строка TOTAL = корневая строка ACCOUNT (первая строка уровня 0) либо сумма строк уровня 0; основная «Итого» — первая строка TOTAL без признака показателя, текст которой не «Итого развернутое» (`total_row_secondary`), в каком бы порядке они ни стояли; итоговая строка-показатель («Итого» с меткой «НУ», «Кол.») в эту сверку не входит. Если «Итого» свёрнута (не больше одной ненулевой стороны сальдо), а корневая строка развёрнута (в 1С так бывает у счёта с развёрнутым сальдо; «Итого развернутое» равна корню), «Итого» SHALL сверяться с единственным корнем так же, как с родителем и детьми (R-21): обороты по колонкам, сальдо по нетто «дебет − кредит» (AURORA-1708, #73); расхождение по нетто или оборотам остаётся нарушением. Если корневой ACCOUNT нет, а строки уровня 0 есть (плоский список), «Итого» SHALL сверяться с их суммой (те же допуски R-21); если строк данных нет (пустая ОСВ, R-13), «Итого» SHALL быть пустой или нулевой. Если строка «Итого» не найдена, `total_row_matches_root = false` и в
|
||||||
`warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root,
|
`warnings` — «строка «Итого» не найдена» (R-20). Результат — `checks{total_row_matches_root,
|
||||||
balance_identity_violations, parent_sum_violations}`. Результат проверки SHALL записываться в документ листа (контракт v2.0): `status` листа — `done`, когда контрольные суммы
|
balance_identity_violations, parent_sum_violations}`. Результат проверки SHALL записываться в документ листа (контракт v2.0): `status` листа — `done`, когда контрольные суммы
|
||||||
§1.4 сошлись (и строки ИНН без нарушений), иначе `needs_check` (прежние условия §1.6, применённые к листу); любое
|
§1.4 сошлись (и строки ИНН без нарушений), иначе `needs_check` (прежние условия §1.6, применённые к листу); любое
|
||||||
@ -314,6 +314,10 @@ balance_identity_violations, parent_sum_violations}`. Результат про
|
|||||||
- **THEN** обе итоговые строки — TOTAL уровня 0 без родителя, «Итого развернутое» не ребёнок «76.Н.1»;
|
- **THEN** обе итоговые строки — TOTAL уровня 0 без родителя, «Итого развернутое» не ребёнок «76.Н.1»;
|
||||||
`total_row_matches_root = true` (сверка по «Итого»), нарушений нет, `status = done`
|
`total_row_matches_root = true` (сверка по «Итого»), нарушений нет, `status = done`
|
||||||
|
|
||||||
|
#### Scenario: «Итого» свёрнута, корень и «Итого развернутое» развёрнуты
|
||||||
|
- **WHEN** корневая строка счёта несёт развёрнутое сальдо (Дт и Кт), «Итого» свёрнута, «Итого развернутое» равна корню по колонкам, а обороты и нетто сальдо «Итого» совпадают с корнем (либо «Итого развернутое» нет, «Итого» свёрнута и совпадает с корнем по нетто)
|
||||||
|
- **THEN** `total_row_matches_root = true`, предупреждения `OSV_TOTAL` нет; при расхождении нетто сальдо или оборотов `total_row_matches_root = false` (AURORA-1708, #73)
|
||||||
|
|
||||||
#### Scenario: «Итого развернутое» без оборотов
|
#### Scenario: «Итого развернутое» без оборотов
|
||||||
- **WHEN** у «Итого развернутое» заполнены только сальдо (обороты пусты), а у основной «Итого» тождество строки выполняется
|
- **WHEN** у «Итого развернутое» заполнены только сальдо (обороты пусты), а у основной «Итого» тождество строки выполняется
|
||||||
- **THEN** `balance_identity_violations` не содержит «Итого развернутое», `status = done`; при нарушении тождества у основной
|
- **THEN** `balance_identity_violations` не содержит «Итого развернутое», `status = done`; при нарушении тождества у основной
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user