mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-25 06:55:17 +02:00
feat: extend inventory and administration workflows
This commit is contained in:
+131
-7
@@ -3,10 +3,17 @@ from __future__ import annotations
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from ccma.domain.contributions import CLAIM_STATUS_LABELS, claim_status, claim_total, money_text
|
||||
from ccma.domain.contributions import (
|
||||
CLAIM_STATUS_LABELS,
|
||||
claim_status,
|
||||
claim_total,
|
||||
money_text,
|
||||
payment_allocated_total,
|
||||
)
|
||||
from ccma.domain.dates import age_label, date_input_hint, format_date_for_display
|
||||
from ccma.domain.models import ASSET_STATUS_LABELS, Event
|
||||
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
|
||||
@@ -16,6 +23,7 @@ from ccma.ui.document_dialog import DocumentTemplateDialog
|
||||
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.scrolling import ScrollableFrame
|
||||
|
||||
CLAIM_TABLE_COLUMNS = (
|
||||
@@ -145,9 +153,11 @@ class MemberTab(ttk.Frame):
|
||||
command=self._save,
|
||||
).grid(row=0, column=0, sticky="e")
|
||||
contribution_tab = ttk.Frame(notebook, padding=16)
|
||||
payments_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(assets_tab, text="Assets")
|
||||
notebook.add(documents_tab, text="Dokumente")
|
||||
|
||||
@@ -162,6 +172,7 @@ class MemberTab(ttk.Frame):
|
||||
(f"Geburtsdatum ({date_input_hint()})", "birth_date"),
|
||||
(f"Aufnahmebeschluss ({date_input_hint()})", "accepted_at"),
|
||||
(f"Mitglied seit ({date_input_hint()})", "membership_started_at"),
|
||||
(f"Austrittsdatum ({date_input_hint()})", "membership_ended_at"),
|
||||
]
|
||||
for row, (label, key) in enumerate(fields):
|
||||
variable = tk.StringVar()
|
||||
@@ -256,6 +267,39 @@ class MemberTab(ttk.Frame):
|
||||
self.claims.bind("<Double-1>", lambda _event: self._open_selected_claim())
|
||||
self.claims.bind("<Return>", lambda _event: self._open_selected_claim())
|
||||
|
||||
payments_tab.columnconfigure(0, weight=1)
|
||||
payments_tab.rowconfigure(1, weight=1)
|
||||
self.payment_summary = tk.StringVar()
|
||||
ttk.Label(payments_tab, textvariable=self.payment_summary, style="Mono.TLabel").grid(
|
||||
row=0, column=0, sticky="w", pady=(0, 10)
|
||||
)
|
||||
self.payments = ttk.Treeview(
|
||||
payments_tab,
|
||||
columns=("date", "amount", "allocated", "available", "reference"),
|
||||
show="headings",
|
||||
selectmode="browse",
|
||||
)
|
||||
for key, title, width in (
|
||||
("date", "Datum", 100),
|
||||
("amount", "Betrag", 90),
|
||||
("allocated", "Zugeordnet", 90),
|
||||
("available", "Frei", 90),
|
||||
("reference", "Referenz", 320),
|
||||
):
|
||||
self.payments.heading(key, text=title)
|
||||
self.payments.column(key, width=width, anchor="w")
|
||||
self.payments.grid(row=1, column=0, sticky="nsew")
|
||||
self.payments.bind("<Double-1>", lambda _event: self._edit_selected_payment())
|
||||
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 bearbeiten", command=self._edit_selected_payment).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(payment_actions, text="Zahlung löschen", command=self._delete_selected_payment).pack(
|
||||
side="left"
|
||||
)
|
||||
|
||||
assets_tab.columnconfigure(0, weight=1)
|
||||
assets_tab.rowconfigure(1, weight=1)
|
||||
self.assets_summary = tk.StringVar()
|
||||
@@ -304,9 +348,7 @@ class MemberTab(ttk.Frame):
|
||||
style="Accent.TButton",
|
||||
command=self._create_document,
|
||||
).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(document_buttons, text="Dateiordner öffnen", command=self._open_files).pack(
|
||||
side="left"
|
||||
)
|
||||
ttk.Button(document_buttons, text="Dateiordner öffnen", command=self._open_files).pack(side="left")
|
||||
self.documents = ttk.Treeview(
|
||||
documents_tab,
|
||||
columns=("name", "type", "modified", "size"),
|
||||
@@ -424,6 +466,7 @@ class MemberTab(ttk.Frame):
|
||||
"birth_date",
|
||||
"accepted_at",
|
||||
"membership_started_at",
|
||||
"membership_ended_at",
|
||||
"mandate_signed_at",
|
||||
"mandate_revoked_at",
|
||||
}
|
||||
@@ -456,10 +499,12 @@ class MemberTab(ttk.Frame):
|
||||
|
||||
def _refresh_contributions(self) -> None:
|
||||
self.claims.delete(*self.claims.get_children())
|
||||
self.payments.delete(*self.payments.get_children())
|
||||
try:
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
except RepositoryError as exc:
|
||||
self.contribution_summary.set(f"FEHLER: {exc}")
|
||||
self.payment_summary.set(f"FEHLER: {exc}")
|
||||
return
|
||||
claims = sorted(
|
||||
data.claims,
|
||||
@@ -481,7 +526,37 @@ class MemberTab(ttk.Frame):
|
||||
CLAIM_STATUS_LABELS.get(status, status.upper()),
|
||||
),
|
||||
)
|
||||
for payment in sorted(
|
||||
data.payments,
|
||||
key=lambda item: (str(item.get("date", "")), str(item.get("created_at", ""))),
|
||||
reverse=True,
|
||||
):
|
||||
payment_id = str(payment.get("payment_id", ""))
|
||||
amount = payment.get("amount", "0")
|
||||
allocated = payment_allocated_total(data, payment_id)
|
||||
available = max(Decimal(str(amount)) - allocated, Decimal("0"))
|
||||
self.payments.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=payment_id,
|
||||
values=(
|
||||
format_date_for_display(str(payment.get("date", ""))),
|
||||
f"{money_text(amount)} EUR",
|
||||
f"{money_text(allocated)} EUR",
|
||||
f"{money_text(available)} EUR",
|
||||
payment.get("reference", ""),
|
||||
),
|
||||
)
|
||||
self.contribution_summary.set(f"{len(data.claims)} Forderungen · {len(data.payments)} Zahlungen")
|
||||
total_amount = sum((Decimal(str(item.get("amount", "0"))) for item in data.payments), Decimal("0"))
|
||||
total_allocated = sum(
|
||||
(payment_allocated_total(data, str(item.get("payment_id", ""))) for item in data.payments),
|
||||
Decimal("0"),
|
||||
)
|
||||
self.payment_summary.set(
|
||||
f"{len(data.payments)} Zahlungen · Gesamt {money_text(total_amount)} EUR · "
|
||||
f"Frei {money_text(max(total_amount - total_allocated, Decimal('0')))} EUR"
|
||||
)
|
||||
|
||||
def _toggle_claim_sort(self, column: str) -> None:
|
||||
if self.claim_sort_column == column:
|
||||
@@ -507,6 +582,46 @@ class MemberTab(ttk.Frame):
|
||||
if selected and not selected[0].startswith("missing-id-"):
|
||||
self.on_open_claim(self.member_id, selected[0])
|
||||
|
||||
def _edit_selected_payment(self) -> None:
|
||||
selected = self.payments.selection()
|
||||
if not selected:
|
||||
messagebox.showinfo("Zahlung auswählen", "Bitte eine Zahlung auswählen.", parent=self)
|
||||
return
|
||||
try:
|
||||
PaymentEditDialog(
|
||||
self,
|
||||
self.repository,
|
||||
self.member_id,
|
||||
selected[0],
|
||||
self._payment_changed,
|
||||
)
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Zahlung konnte nicht geöffnet werden", str(exc), parent=self)
|
||||
|
||||
def _delete_selected_payment(self) -> None:
|
||||
selected = self.payments.selection()
|
||||
if not selected:
|
||||
messagebox.showinfo("Zahlung auswählen", "Bitte eine Zahlung auswählen.", parent=self)
|
||||
return
|
||||
values = self.payments.item(selected[0], "values")
|
||||
label = f"{values[0]} · {values[1]}" if values else selected[0]
|
||||
if not messagebox.askyesno(
|
||||
"Zahlung wirklich löschen?",
|
||||
f"Die Zahlung {label} und alle ihre Zuordnungen werden endgültig gelöscht.",
|
||||
parent=self,
|
||||
):
|
||||
return
|
||||
try:
|
||||
self.repository.delete_payment(self.member_id, selected[0])
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Zahlung konnte nicht gelöscht werden", str(exc), parent=self)
|
||||
return
|
||||
self._payment_changed()
|
||||
|
||||
def _payment_changed(self) -> None:
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
|
||||
def _refresh_documents(self) -> None:
|
||||
self.documents.delete(*self.documents.get_children())
|
||||
self.document_paths.clear()
|
||||
@@ -536,8 +651,11 @@ class MemberTab(ttk.Frame):
|
||||
|
||||
def _refresh_assets(self) -> None:
|
||||
self.assets_tree.delete(*self.assets_tree.get_children())
|
||||
assets = self.repository.list_member_assets(self.member_id)
|
||||
self.assets_summary.set(f"{len(assets)} ausgegebene Assets")
|
||||
held_assets = self.repository.list_member_assets(self.member_id)
|
||||
owned_assets = self.repository.list_member_owned_assets(self.member_id)
|
||||
assets_by_id = {asset.asset_id: asset for asset in [*held_assets, *owned_assets]}
|
||||
assets = list(assets_by_id.values())
|
||||
self.assets_summary.set(f"{len(owned_assets)} im Eigentum · {len(held_assets)} vom Verein erhalten")
|
||||
for asset in assets:
|
||||
self.assets_tree.insert(
|
||||
"",
|
||||
@@ -547,7 +665,13 @@ class MemberTab(ttk.Frame):
|
||||
asset.label,
|
||||
asset.category,
|
||||
asset.inventory_number,
|
||||
ASSET_STATUS_LABELS.get(asset.status, asset.status),
|
||||
(
|
||||
"EIGENTUM · DEM VEREIN ÜBERLASSEN"
|
||||
if asset.owner_member_id == self.member_id and asset.custody_type == "club"
|
||||
else "EIGENTUM"
|
||||
if asset.owner_member_id == self.member_id
|
||||
else ASSET_STATUS_LABELS.get(asset.status, asset.status)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user