Only list open or already-linked claims/donations in the payment dialog

Show a claim or donation in the Zahlung anlegen/bearbeiten allocation
table only if it still has an open balance, or is already linked to this
payment -- fully settled ones no longer clutter the list.

Visibility is decided from a snapshot taken when the dialog opens (or
when a donation is created inline), not recomputed live, so a row never
disappears just because the user temporarily unassigned it with
"Zuordnung lösen" -- it stays there to add back with "Zuordnung setzen"
until the dialog is saved.

Also fixes a capacity display bug this surfaced: "Maximal zuordenbar"
was computed by mixing the persisted claim/donation balance with the
live, currently-edited allocation amount, so it inflated every time the
amount was changed during a session. It's now computed against a fixed
baseline captured at dialog open, so it stays accurate throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-14 21:28:52 +02:00
co-authored by Claude Sonnet 5
parent a5d9abd59a
commit 4dd625c09f
+39 -13
View File
@@ -21,10 +21,16 @@ from ccma.ui.donation_dialog import DonationEditDialog
class _AllocationTable: class _AllocationTable:
"""Embeds a combined claims + donations allocation tree into a payment dialog. """Embeds a combined claims + donations allocation tree into a payment dialog.
Only claims/donations that are still open (or already linked to this payment) are
listed, so fully-settled ones don't clutter the picture. A row's visibility is
decided once, from a snapshot taken when the dialog opens (or when a new donation
is created inline) -- it never disappears again for the rest of the editing
session just because the user unassigned it. That way "Zuordnung lösen" followed
by "Zuordnung setzen" always works on the same row instead of the target vanishing.
Mutates the ``claim_allocations``/``donation_allocations`` dicts it is given in Mutates the ``claim_allocations``/``donation_allocations`` dicts it is given in
place, so the owning dialog can read them back at save time. Lets the user create place, so the owning dialog can read them back at save time. Nothing is written to
a new donation on the fly and allocate against it immediately, which is the whole the repository until the dialog's own Save button does so.
point of surfacing this inside the payment dialog rather than as a separate step.
""" """
def __init__( def __init__(
@@ -42,33 +48,53 @@ class _AllocationTable:
self.claim_allocations = claim_allocations self.claim_allocations = claim_allocations
self.donation_allocations = donation_allocations self.donation_allocations = donation_allocations
self.on_change = on_change self.on_change = on_change
# Fixed snapshot of what this payment already covered when the dialog opened.
# Capacities are computed against this baseline rather than the live, editable
# dicts above -- otherwise "Maximal zuordenbar" would inflate every time the
# user changes an amount, since claim_balance() reflects only persisted state.
self._baseline_claim_allocations = dict(claim_allocations)
self._baseline_donation_allocations = dict(donation_allocations)
self._visible_claim_ids: set[str] = set(claim_allocations)
self._visible_donation_ids: set[str] = set(donation_allocations)
self._load_targets() self._load_targets()
self._build(parent) self._build(parent)
self._refresh() self._refresh()
def _load_targets(self) -> None: def _load_targets(self) -> None:
self.data = self.repository.get_contributions(self.member_id) self.data = self.repository.get_contributions(self.member_id)
self._visible_claim_ids |= {
str(claim.get("claim_id", ""))
for claim in self.data.claims
if str(claim.get("claim_id", ""))
and claim_status(self.data, claim) != "cancelled"
and claim_balance(self.data, claim) > 0
}
self._visible_donation_ids |= {
str(item.get("donation_id", ""))
for item in self.data.donations
if str(item.get("donation_id", "")) and donation_balance(self.data, item) > 0
}
self.claims_by_id = { self.claims_by_id = {
str(claim.get("claim_id", "")): claim str(claim.get("claim_id", "")): claim
for claim in self.data.claims for claim in self.data.claims
if str(claim.get("claim_id", "")) if str(claim.get("claim_id", "")) in self._visible_claim_ids
and ( }
claim_status(self.data, claim) != "cancelled" self.donations_by_id = {
or str(claim.get("claim_id", "")) in self.claim_allocations str(item.get("donation_id", "")): item
) for item in self.data.donations
if str(item.get("donation_id", "")) in self._visible_donation_ids
} }
self.donations_by_id = {str(item.get("donation_id", "")): item for item in self.data.donations}
def _capacities(self) -> None: def _capacities(self) -> None:
self.claim_capacity: dict[str, Decimal] = {} self.claim_capacity: dict[str, Decimal] = {}
for claim_id, claim in self.claims_by_id.items(): for claim_id, claim in self.claims_by_id.items():
current = decimal_value(self.claim_allocations.get(claim_id, "0")) baseline = decimal_value(self._baseline_claim_allocations.get(claim_id, "0"))
self.claim_capacity[claim_id] = max(claim_balance(self.data, claim) + current, Decimal("0")) self.claim_capacity[claim_id] = max(claim_balance(self.data, claim) + baseline, Decimal("0"))
self.donation_capacity: dict[str, Decimal] = {} self.donation_capacity: dict[str, Decimal] = {}
for donation_id, donation in self.donations_by_id.items(): for donation_id, donation in self.donations_by_id.items():
current = decimal_value(self.donation_allocations.get(donation_id, "0")) baseline = decimal_value(self._baseline_donation_allocations.get(donation_id, "0"))
self.donation_capacity[donation_id] = max( self.donation_capacity[donation_id] = max(
donation_balance(self.data, donation) + current, Decimal("0") donation_balance(self.data, donation) + baseline, Decimal("0")
) )
def _build(self, parent: tk.Misc) -> None: def _build(self, parent: tk.Misc) -> None: