113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""Упаковка встречных строк по одному правилу без БД (§5)."""
|
|
|
|
import uuid
|
|
from decimal import Decimal
|
|
|
|
from src.domain.elimination_packing import MAX_SIDE_ROWS, PackRow, best_subsets, calculate_rule_totals, pack_rule_rows
|
|
|
|
A = uuid.uuid4()
|
|
B = uuid.uuid4()
|
|
|
|
|
|
def _row(amount: str, *, debit: bool) -> PackRow:
|
|
return PackRow((uuid.uuid4(),), A if debit else B, B if debit else A, Decimal(amount))
|
|
|
|
|
|
def test_knapsack_finds_exact_subset_with_fixed_size_and_keeps_ties() -> None:
|
|
first, second, third = (_row(amount, debit=False) for amount in ("40", "60", "60"))
|
|
|
|
choice = best_subsets([first, second, third], size=2, capacity=Decimal("100"))
|
|
|
|
assert choice is not None
|
|
assert choice.deviation == 0
|
|
assert choice.unique_rows is None
|
|
assert choice.involved_keys == {first.key, second.key, third.key}
|
|
|
|
|
|
def test_knapsack_keeps_both_nearest_sums_and_the_five_percent_lower_boundary() -> None:
|
|
below, above = (_row(amount, debit=False) for amount in ("99.00", "101.00"))
|
|
|
|
tie = best_subsets([below, above], size=1, capacity=Decimal("100.00"))
|
|
assert tie is not None
|
|
assert tie.deviation == Decimal("1.00")
|
|
assert tie.unique_rows is None
|
|
assert tie.involved_keys == {below.key, above.key}
|
|
|
|
boundary = best_subsets([_row("100.00", debit=False)], size=1, capacity=Decimal("105.00"))
|
|
assert boundary is not None
|
|
assert boundary.deviation == Decimal("5.00")
|
|
|
|
|
|
def test_exact_one_to_one_is_reserved_before_one_to_many() -> None:
|
|
debit = [_row("100", debit=True), _row("50", debit=True)]
|
|
credit = [_row("100", debit=False), _row("30", debit=False), _row("20", debit=False)]
|
|
|
|
result = pack_rule_rows(debit, credit)
|
|
|
|
assert [(len(group.debit_rows), len(group.credit_rows)) for group in result.groups] == [(1, 1), (1, 2)]
|
|
assert result.used_keys == {row.key for row in [*debit, *credit]}
|
|
assert result.ambiguous_keys == frozenset()
|
|
|
|
|
|
def test_equal_exact_groups_sharing_a_row_go_to_manual() -> None:
|
|
debit = [_row("100", debit=True)]
|
|
credit = [_row("100", debit=False), _row("100", debit=False)]
|
|
|
|
result = pack_rule_rows(debit, credit)
|
|
|
|
assert result.groups == ()
|
|
assert result.ambiguous_keys == {row.key for row in [*debit, *credit]}
|
|
|
|
|
|
def test_five_percent_boundary_and_no_many_to_many() -> None:
|
|
debit = [_row("100", debit=True)]
|
|
credit = [_row("21", debit=False) for _ in range(5)]
|
|
|
|
result = pack_rule_rows(debit, credit)
|
|
|
|
assert len(result.groups) == 1
|
|
assert len(result.groups[0].credit_rows) == MAX_SIDE_ROWS
|
|
assert result.groups[0].deviation_percent == Decimal("5")
|
|
|
|
many_to_many = pack_rule_rows(
|
|
[_row("40", debit=True), _row("60", debit=True)],
|
|
[_row("30", debit=False), _row("70", debit=False)],
|
|
)
|
|
assert many_to_many.groups == () # суммы сторон равны, но допустимой группы 1:N или N:1 нет
|
|
|
|
over_threshold = pack_rule_rows([_row("100", debit=True)], [_row("106", debit=False)])
|
|
assert over_threshold.groups == ()
|
|
|
|
|
|
def test_rule_totals_eliminate_minimum_within_five_percent() -> None:
|
|
totals = calculate_rule_totals(Decimal("110429113.15"), Decimal("106311116.46"), manual=False)
|
|
|
|
assert totals.amount_a == Decimal("110429113.15")
|
|
assert totals.amount_b == Decimal("106311116.46")
|
|
assert totals.deviation_amount == Decimal("4117996.69")
|
|
assert totals.deviation_percent is not None
|
|
assert totals.deviation_percent.quantize(Decimal("0.01")) == Decimal("3.87")
|
|
assert totals.eliminated_amount == Decimal("106311116.46")
|
|
assert totals.exceeds_threshold is False
|
|
|
|
|
|
def test_rule_totals_over_five_percent_and_manual_eliminate_zero() -> None:
|
|
over = calculate_rule_totals(Decimal("106"), Decimal("100"), manual=False)
|
|
assert over.deviation_amount == Decimal("6")
|
|
assert over.deviation_percent == Decimal("6")
|
|
assert over.exceeds_threshold is True
|
|
assert over.eliminated_amount == 0
|
|
|
|
manual = calculate_rule_totals(Decimal("100"), Decimal("100"), manual=True)
|
|
assert manual.exceeds_threshold is False
|
|
assert manual.eliminated_amount == 0
|
|
|
|
boundary = calculate_rule_totals(Decimal("105"), Decimal("100"), manual=False)
|
|
assert boundary.exceeds_threshold is False
|
|
assert boundary.eliminated_amount == Decimal("100")
|
|
|
|
one_sided = calculate_rule_totals(Decimal("100"), Decimal("0"), manual=False)
|
|
assert one_sided.deviation_percent is None
|
|
assert one_sided.exceeds_threshold is True
|
|
assert one_sided.eliminated_amount == 0
|