fix(#237): разнесение — без строк вне периметра и «сам на себя» (AURORA-2034)

§5 п. 2a: в агрегаты и нераспределённые идут только строки между разными компаниями
периметра версии; строка с ручным решением аналитика остаётся видна. На копии стенда
нераспределённых РБ 2025 К1 — 37 → 3, РБ 2024 К3 — 31 → 3 (вместе с «76-исключениями»).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-10 13:04:10 +04:00
parent d13a9554a1
commit 3296e4a849
2 changed files with 52 additions and 7 deletions

View File

@ -100,17 +100,23 @@ def distribute_vgo_rows(data: DistributionInput) -> Distribution:
for row in data.rows:
if not used.isdisjoint(row.entry_ids):
continue
rule_ids = rules_for(data.owners, row.account_id, row_side(row.closing_debit, row.closing_credit))
if (
row.entry_ids not in data.manual_keys
and len(rule_ids) == 1
and (
manual = row.entry_ids in data.manual_keys
# §5 п. 2a: разносятся только строки между разными компаниями периметра; строка с контрагентом вне периметра
# или «сам на себя» в таблицу не попадает, если по ней нет ручного решения аналитика (AURORA-2034)
in_perimeter = (
row.legal_entity_id in data.members
and row.counterparty_id in data.members
and (row.closing_debit not in (None, ZERO) or row.closing_credit not in (None, ZERO))
and row.legal_entity_id != row.counterparty_id
)
if not manual and not in_perimeter:
continue
rule_ids = rules_for(data.owners, row.account_id, row_side(row.closing_debit, row.closing_credit))
if (
not manual
and len(rule_ids) == 1
and (row.closing_debit not in (None, ZERO) or row.closing_credit not in (None, ZERO))
):
auto_rules[row.entry_ids] = next(iter(rule_ids))
elif rule_ids or row.entry_ids in data.manual_keys:
elif rule_ids or manual:
unassigned.add(row.entry_ids)
return Distribution(selected, auto_rules, frozenset(unassigned), frozenset(used))

View File

@ -131,6 +131,45 @@ def test_credit_row_is_matched_by_credit_side_rules_only() -> None:
assert distribution.unassigned == {debit_row.entry_ids}
def test_rows_outside_perimeter_or_to_itself_are_not_unallocated() -> None:
"""Разносятся только строки между компаниями периметра (§5 п. 2a): контрагент вне периметра и «сам на себя»
в нераспределённые не попадают; ручное решение аналитика по такой строке сохраняется (AURORA-2034)."""
rule_5, rule_6, account, company_a, company_b, outsider = (uuid.uuid4() for _ in range(6))
inside = _row(uuid.uuid4(), company_a, company_b, 1)
outside = _row(uuid.uuid4(), company_a, outsider, 2)
to_itself = _row(uuid.uuid4(), company_a, company_a, 3)
to_itself_single = _row(uuid.uuid4(), company_b, company_b, 4)
outside_manual = _row(uuid.uuid4(), company_a, outsider, 5)
for row in (inside, outside, to_itself, to_itself_single, outside_manual):
row.account_id = account
owners = account_owners([(rule_5, {account}, set()), (rule_6, {account}, {account})])
single = account_owners([(rule_6, {account}, set())])
distribution = distribute_vgo_rows(
DistributionInput(
rows=[inside, outside, to_itself, outside_manual],
exact_ids=set(),
diff_ids=[],
manual_keys={outside_manual.entry_ids},
owners=owners,
members={company_a, company_b},
)
)
assert distribution.unassigned == {inside.entry_ids, outside_manual.entry_ids}
assert distribution.auto_rules == {}
single_distribution = distribute_vgo_rows(
DistributionInput(
rows=[to_itself_single],
exact_ids=set(),
diff_ids=[],
manual_keys=set(),
owners=single,
members={company_a, company_b},
)
)
assert single_distribution.auto_rules == {}
assert single_distribution.unassigned == frozenset()
async def test_unallocated_credit_row_offers_only_credit_side_rules(monkeypatch: pytest.MonkeyPatch) -> None:
"""Меню «Отнести к правилу» Кт-строки — только правила, где счёт на стороне кредита (AURORA-2034)."""
version_id, company_a, company_b, rule_1, rule_2, rule_3, account = (uuid.uuid4() for _ in range(7))