mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-25 15:05:18 +02:00
Allow deleting claims, creating standalone payments, and add a donations tab
Claims could previously only be cancelled (stornieren), which blocks once a payment is allocated. Add a hard delete that releases any linked payments/credits back to being unallocated instead of destroying them. Payments could only be created from within a claim, forcing immediate allocation. Add a bare payment creation flow in the Zahlungen tab so incoming transfers can be logged first and allocated later. Add a per-member Spenden tab (donations, backed by a new donations list on ContributionData) so amounts paid beyond the membership fee can be tracked and existing free payments allocated to them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a61ea3cb57
commit
42fb4c4224
+177
-1
@@ -9,8 +9,12 @@ from tkinter import messagebox, ttk
|
||||
|
||||
from ccma.domain.contributions import (
|
||||
CLAIM_STATUS_LABELS,
|
||||
DONATION_STATUS_LABELS,
|
||||
claim_status,
|
||||
claim_total,
|
||||
donation_allocated_total,
|
||||
donation_balance,
|
||||
donation_status,
|
||||
money_text,
|
||||
payment_allocated_total,
|
||||
)
|
||||
@@ -20,10 +24,15 @@ from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
from ccma.ui.dialogs import IntegrityWarningDialog
|
||||
from ccma.ui.document_dialog import DocumentTemplateDialog
|
||||
from ccma.ui.donation_dialog import (
|
||||
AllocateDonationPaymentDialog,
|
||||
DonationEditDialog,
|
||||
DonationPaymentDialog,
|
||||
)
|
||||
from ccma.ui.file_open import open_path
|
||||
from ccma.ui.labels import display_label, storage_key
|
||||
from ccma.ui.messages import MessageAction, MessageBannerList, TabMessage
|
||||
from ccma.ui.payment_dialog import PaymentEditDialog
|
||||
from ccma.ui.payment_dialog import PaymentCreateDialog, PaymentEditDialog
|
||||
from ccma.ui.scrolling import ScrollableFrame
|
||||
|
||||
CLAIM_TABLE_COLUMNS = (
|
||||
@@ -154,10 +163,12 @@ class MemberTab(ttk.Frame):
|
||||
).grid(row=0, column=0, sticky="e")
|
||||
contribution_tab = ttk.Frame(notebook, padding=16)
|
||||
payments_tab = ttk.Frame(notebook, padding=16)
|
||||
donations_tab = ttk.Frame(notebook, padding=16)
|
||||
assets_tab = ttk.Frame(notebook, padding=16)
|
||||
documents_tab = ttk.Frame(notebook, padding=16)
|
||||
notebook.add(contribution_tab, text="Forderungen")
|
||||
notebook.add(payments_tab, text="Zahlungen")
|
||||
notebook.add(donations_tab, text="Spenden")
|
||||
notebook.add(assets_tab, text="Assets")
|
||||
notebook.add(documents_tab, text="Dokumente")
|
||||
|
||||
@@ -305,6 +316,9 @@ class MemberTab(ttk.Frame):
|
||||
self.payments.bind("<Return>", lambda _event: self._edit_selected_payment())
|
||||
payment_actions = ttk.Frame(payments_tab)
|
||||
payment_actions.grid(row=2, column=0, sticky="e", pady=(8, 0))
|
||||
ttk.Button(payment_actions, text="Zahlung anlegen", command=self._create_payment).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(payment_actions, text="Zahlung bearbeiten", command=self._edit_selected_payment).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
@@ -312,6 +326,50 @@ class MemberTab(ttk.Frame):
|
||||
side="left"
|
||||
)
|
||||
|
||||
donations_tab.columnconfigure(0, weight=1)
|
||||
donations_tab.rowconfigure(1, weight=1)
|
||||
self.donation_summary = tk.StringVar()
|
||||
ttk.Label(donations_tab, textvariable=self.donation_summary, style="Mono.TLabel").grid(
|
||||
row=0, column=0, sticky="w", pady=(0, 10)
|
||||
)
|
||||
self.donations = ttk.Treeview(
|
||||
donations_tab,
|
||||
columns=("date", "amount", "allocated", "balance", "status", "reference"),
|
||||
show="headings",
|
||||
selectmode="browse",
|
||||
)
|
||||
for key, title, width in (
|
||||
("date", "Datum", 100),
|
||||
("amount", "Betrag", 90),
|
||||
("allocated", "Zugeordnet", 90),
|
||||
("balance", "Offen", 90),
|
||||
("status", "Status", 150),
|
||||
("reference", "Referenz / Zweck", 260),
|
||||
):
|
||||
self.donations.heading(key, text=title)
|
||||
self.donations.column(key, width=width, anchor="w")
|
||||
self.donations.grid(row=1, column=0, sticky="nsew")
|
||||
self.donations.bind("<Double-1>", lambda _event: self._edit_selected_donation())
|
||||
self.donations.bind("<Return>", lambda _event: self._edit_selected_donation())
|
||||
donation_actions = ttk.Frame(donations_tab)
|
||||
donation_actions.grid(row=2, column=0, sticky="e", pady=(8, 0))
|
||||
ttk.Button(donation_actions, text="Spende anlegen", command=self._create_donation).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(donation_actions, text="Spende bearbeiten", command=self._edit_selected_donation).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(donation_actions, text="Spende löschen", command=self._delete_selected_donation).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Separator(donation_actions, orient="vertical").pack(side="left", fill="y", padx=(0, 8))
|
||||
ttk.Button(
|
||||
donation_actions, text="Vorhandene Zahlung zuordnen", command=self._allocate_donation_payment
|
||||
).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(donation_actions, text="Zahlung erfassen", command=self._record_donation_payment).pack(
|
||||
side="left"
|
||||
)
|
||||
|
||||
assets_tab.columnconfigure(0, weight=1)
|
||||
assets_tab.rowconfigure(1, weight=1)
|
||||
self.assets_summary = tk.StringVar()
|
||||
@@ -499,6 +557,7 @@ class MemberTab(ttk.Frame):
|
||||
self._clear_dirty()
|
||||
self._refresh_events()
|
||||
self._refresh_contributions()
|
||||
self._refresh_donations()
|
||||
self._refresh_assets()
|
||||
self._refresh_documents()
|
||||
|
||||
@@ -573,6 +632,44 @@ class MemberTab(ttk.Frame):
|
||||
f"Frei {money_text(max(total_amount - total_allocated, Decimal('0')))} EUR"
|
||||
)
|
||||
|
||||
def _refresh_donations(self) -> None:
|
||||
self.donations.delete(*self.donations.get_children())
|
||||
try:
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
except RepositoryError as exc:
|
||||
self.donation_summary.set(f"FEHLER: {exc}")
|
||||
return
|
||||
for donation in sorted(
|
||||
data.donations,
|
||||
key=lambda item: (str(item.get("date", "")), str(item.get("created_at", ""))),
|
||||
reverse=True,
|
||||
):
|
||||
donation_id = str(donation.get("donation_id", ""))
|
||||
amount = donation.get("amount", "0")
|
||||
allocated = donation_allocated_total(data, donation_id)
|
||||
balance = donation_balance(data, donation)
|
||||
status = donation_status(data, donation)
|
||||
reference = " · ".join(
|
||||
part for part in (donation.get("reference", ""), donation.get("purpose", "")) if part
|
||||
)
|
||||
self.donations.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=donation_id,
|
||||
values=(
|
||||
format_date_for_display(str(donation.get("date", ""))),
|
||||
f"{money_text(amount)} EUR",
|
||||
f"{money_text(allocated)} EUR",
|
||||
f"{money_text(balance)} EUR",
|
||||
DONATION_STATUS_LABELS.get(status, status.upper()),
|
||||
reference,
|
||||
),
|
||||
)
|
||||
total_amount = sum(
|
||||
(Decimal(str(item.get("amount", "0"))) for item in data.donations), Decimal("0")
|
||||
)
|
||||
self.donation_summary.set(f"{len(data.donations)} Spenden · Gesamt {money_text(total_amount)} EUR")
|
||||
|
||||
def _toggle_claim_sort(self, column: str) -> None:
|
||||
if self.claim_sort_column == column:
|
||||
self.claim_sort_descending = not self.claim_sort_descending
|
||||
@@ -633,10 +730,89 @@ class MemberTab(ttk.Frame):
|
||||
return
|
||||
self._payment_changed()
|
||||
|
||||
def _create_payment(self) -> None:
|
||||
PaymentCreateDialog(self, self.repository, self.member_id, self._payment_changed)
|
||||
|
||||
def _payment_changed(self) -> None:
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
|
||||
def _selected_donation(self) -> dict | None:
|
||||
selected = self.donations.selection()
|
||||
if not selected:
|
||||
return None
|
||||
try:
|
||||
_data, donation = self.repository.get_donation(self.member_id, selected[0])
|
||||
except RepositoryError:
|
||||
return None
|
||||
return donation
|
||||
|
||||
def _create_donation(self) -> None:
|
||||
DonationEditDialog(self, self.repository, self.member_id, self._donation_changed)
|
||||
|
||||
def _edit_selected_donation(self) -> None:
|
||||
donation = self._selected_donation()
|
||||
if not donation:
|
||||
messagebox.showinfo("Spende auswählen", "Bitte eine Spende auswählen.", parent=self)
|
||||
return
|
||||
DonationEditDialog(self, self.repository, self.member_id, self._donation_changed, donation)
|
||||
|
||||
def _delete_selected_donation(self) -> None:
|
||||
donation = self._selected_donation()
|
||||
if not donation:
|
||||
messagebox.showinfo("Spende auswählen", "Bitte eine Spende auswählen.", parent=self)
|
||||
return
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
allocated = donation_allocated_total(data, str(donation.get("donation_id", "")))
|
||||
detail = "Diese Spende wirklich endgültig löschen? Das kann nicht rückgängig gemacht werden."
|
||||
if allocated:
|
||||
detail += (
|
||||
f"\n\nZugeordnete Zahlungen in Höhe von {money_text(allocated)} EUR werden dabei "
|
||||
"gelöst und stehen danach wieder frei zur Verfügung."
|
||||
)
|
||||
if not messagebox.askyesno("Spende löschen", detail, parent=self):
|
||||
return
|
||||
try:
|
||||
self.repository.delete_donation(self.member_id, str(donation.get("donation_id", "")))
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Löschen fehlgeschlagen", str(exc), parent=self)
|
||||
return
|
||||
self._donation_changed()
|
||||
|
||||
def _record_donation_payment(self) -> None:
|
||||
donation = self._selected_donation()
|
||||
if not donation:
|
||||
messagebox.showinfo("Spende auswählen", "Bitte eine Spende auswählen.", parent=self)
|
||||
return
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
DonationPaymentDialog(
|
||||
self,
|
||||
self.repository,
|
||||
self.member_id,
|
||||
str(donation.get("donation_id", "")),
|
||||
donation_balance(data, donation),
|
||||
self._donation_changed,
|
||||
)
|
||||
|
||||
def _allocate_donation_payment(self) -> None:
|
||||
donation = self._selected_donation()
|
||||
if not donation:
|
||||
messagebox.showinfo("Spende auswählen", "Bitte eine Spende auswählen.", parent=self)
|
||||
return
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
AllocateDonationPaymentDialog(
|
||||
self,
|
||||
self.repository,
|
||||
self.member_id,
|
||||
str(donation.get("donation_id", "")),
|
||||
donation_balance(data, donation),
|
||||
self._donation_changed,
|
||||
)
|
||||
|
||||
def _donation_changed(self) -> None:
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
|
||||
def _refresh_documents(self) -> None:
|
||||
self.documents.delete(*self.documents.get_children())
|
||||
self.document_paths.clear()
|
||||
|
||||
Reference in New Issue
Block a user