mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04:52 +02:00
feat: unify claim activity view
This commit is contained in:
@@ -23,7 +23,8 @@
|
||||
"Ein Akten-Preflight sperrt bei beschädigten Mitglieder-, Beitrags- oder Eventdateien alle Regeln für die betroffene Akte.",
|
||||
"Der Hausmeister-Tab zeigt den vollständigen Inhalt eines markierten Vorgangs in einem mehrzeiligen Detailbereich.",
|
||||
"Hausmeister-Tasks lassen sich manuell löschen; Einträge entfernter Mitgliederakten werden beim nächsten Lauf bereinigt.",
|
||||
"Forderungen besitzen eigene Tabs mit Positionen, Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
||||
"Forderungen besitzen eigene Arbeitsansichten mit Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
||||
"Positionen, Zahlungen und Mahnungen einer Forderung werden gemeinsam in einer farblich gruppierten Übersicht dargestellt.",
|
||||
"Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.",
|
||||
"Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.",
|
||||
"Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.",
|
||||
|
||||
+126
-106
@@ -77,17 +77,7 @@ class ClaimTab(ttk.Frame):
|
||||
self.summary_vars[key] = variable
|
||||
ttk.Label(card, textvariable=variable, style="CardValue.TLabel").pack(anchor="w", pady=(5, 0))
|
||||
|
||||
notebook = ttk.Notebook(self)
|
||||
notebook.grid(row=2, column=0, sticky="nsew")
|
||||
positions = ttk.Frame(notebook, padding=12)
|
||||
payments = ttk.Frame(notebook, padding=12)
|
||||
reminders = ttk.Frame(notebook, padding=12)
|
||||
notebook.add(positions, text="Positionen")
|
||||
notebook.add(payments, text="Zahlungen")
|
||||
notebook.add(reminders, text="Mahnungen")
|
||||
self._build_positions(positions)
|
||||
self._build_payments(payments)
|
||||
self._build_reminders(reminders)
|
||||
self._build_ledger()
|
||||
|
||||
footer = ttk.Frame(self)
|
||||
footer.grid(row=3, column=0, sticky="ew", pady=(10, 0))
|
||||
@@ -97,80 +87,64 @@ class ClaimTab(ttk.Frame):
|
||||
self.cancel_button = ttk.Button(footer, text="Forderung stornieren", command=self._cancel_claim)
|
||||
self.cancel_button.grid(row=0, column=1, sticky="e")
|
||||
|
||||
def _build_positions(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(0, weight=1)
|
||||
self.positions = ttk.Treeview(
|
||||
parent, columns=("type", "description", "quantity", "unit", "amount"), show="headings"
|
||||
def _build_ledger(self) -> None:
|
||||
ledger = ttk.Frame(self, padding=12)
|
||||
ledger.grid(row=2, column=0, sticky="nsew")
|
||||
ledger.columnconfigure(0, weight=1)
|
||||
ledger.rowconfigure(0, weight=1)
|
||||
self.ledger = ttk.Treeview(
|
||||
ledger,
|
||||
columns=("date", "description", "quantity", "unit", "amount", "status", "reference"),
|
||||
show="tree headings",
|
||||
)
|
||||
self.ledger.heading("#0", text="Bereich / Element")
|
||||
self.ledger.column("#0", width=190, minwidth=140, anchor="w")
|
||||
for key, title, width in (
|
||||
("type", "Typ", 100),
|
||||
("description", "Beschreibung", 360),
|
||||
("quantity", "Menge", 80),
|
||||
("date", "Datum", 120),
|
||||
("description", "Beschreibung", 300),
|
||||
("quantity", "Menge", 75),
|
||||
("unit", "Einzelpreis", 100),
|
||||
("amount", "Betrag", 100),
|
||||
("amount", "Betrag", 105),
|
||||
("status", "Status / Details", 180),
|
||||
("reference", "Referenz", 210),
|
||||
):
|
||||
self.positions.heading(key, text=title)
|
||||
self.positions.column(key, width=width, anchor="w")
|
||||
self.positions.grid(row=0, column=0, sticky="nsew")
|
||||
ttk.Button(parent, text="Position hinzufügen", command=self._add_item).grid(
|
||||
row=1, column=0, sticky="e", pady=(10, 0)
|
||||
self.ledger.heading(key, text=title)
|
||||
self.ledger.column(key, width=width, minwidth=60, anchor="w")
|
||||
self.ledger.grid(row=0, column=0, sticky="nsew")
|
||||
vertical_scroll = ttk.Scrollbar(ledger, orient="vertical", command=self.ledger.yview)
|
||||
vertical_scroll.grid(row=0, column=1, sticky="ns")
|
||||
horizontal_scroll = ttk.Scrollbar(ledger, orient="horizontal", command=self.ledger.xview)
|
||||
horizontal_scroll.grid(row=1, column=0, sticky="ew")
|
||||
self.ledger.configure(
|
||||
yscrollcommand=vertical_scroll.set,
|
||||
xscrollcommand=horizontal_scroll.set,
|
||||
)
|
||||
self.ledger.tag_configure("position-group", background="#1261a0", foreground="#ffffff")
|
||||
self.ledger.tag_configure("position", background="#234d70", foreground="#ffffff")
|
||||
self.ledger.tag_configure("payment-group", background="#237a3b", foreground="#ffffff")
|
||||
self.ledger.tag_configure("payment", background="#285b3b", foreground="#ffffff")
|
||||
self.ledger.tag_configure("reminder-group", background="#b85f00", foreground="#ffffff")
|
||||
self.ledger.tag_configure("reminder", background="#70451f", foreground="#ffffff")
|
||||
self.ledger.bind("<<TreeviewSelect>>", lambda _event: self._update_reminder_buttons())
|
||||
|
||||
def _build_payments(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(0, weight=1)
|
||||
self.payments = ttk.Treeview(
|
||||
parent, columns=("date", "allocated", "payment", "gnucash", "reference"), show="headings"
|
||||
)
|
||||
for key, title, width in (
|
||||
("date", "Datum", 110),
|
||||
("allocated", "Zugeordnet", 100),
|
||||
("payment", "Zahlung gesamt", 110),
|
||||
("gnucash", "GnuCash-ID", 180),
|
||||
("reference", "Referenz", 280),
|
||||
):
|
||||
self.payments.heading(key, text=title)
|
||||
self.payments.column(key, width=width, anchor="w")
|
||||
self.payments.grid(row=0, column=0, sticky="nsew")
|
||||
buttons = ttk.Frame(parent)
|
||||
buttons.grid(row=1, column=0, sticky="e", pady=(10, 0))
|
||||
buttons = ttk.Frame(ledger)
|
||||
buttons.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
||||
ttk.Button(buttons, text="Position hinzufügen", command=self._add_item).pack(side="left")
|
||||
ttk.Separator(buttons, orient="vertical").pack(side="left", fill="y", padx=10)
|
||||
ttk.Button(buttons, text="Vorhandene Zahlung zuordnen", command=self._allocate_payment).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(buttons, text="Zahlung erfassen", command=self._record_payment).pack(side="left")
|
||||
|
||||
def _build_reminders(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(0, weight=1)
|
||||
self.reminders = ttk.Treeview(
|
||||
parent,
|
||||
columns=("created", "level", "name", "status", "deadline", "fee"),
|
||||
show="headings",
|
||||
)
|
||||
for key, title, width in (
|
||||
("created", "Erstellt", 150),
|
||||
("level", "Stufe", 60),
|
||||
("name", "Mahnung", 230),
|
||||
("status", "Status", 130),
|
||||
("deadline", "Zahlungsfrist", 120),
|
||||
("fee", "Gebühr", 90),
|
||||
):
|
||||
self.reminders.heading(key, text=title)
|
||||
self.reminders.column(key, width=width, anchor="w")
|
||||
self.reminders.grid(row=0, column=0, sticky="nsew")
|
||||
self.reminders.bind("<<TreeviewSelect>>", lambda _event: self._update_reminder_buttons())
|
||||
buttons = ttk.Frame(parent)
|
||||
buttons.grid(row=1, column=0, sticky="e", pady=(10, 0))
|
||||
ttk.Separator(buttons, orient="vertical").pack(side="left", fill="y", padx=10)
|
||||
self.discard_reminder_button = ttk.Button(
|
||||
buttons, text="Entwurf verwerfen", command=self._discard_reminder, state="disabled"
|
||||
)
|
||||
self.discard_reminder_button.pack(side="left", padx=(0, 8))
|
||||
self.discard_reminder_button.pack(side="right", padx=(8, 0))
|
||||
self.send_reminder_button = ttk.Button(
|
||||
buttons, text="Als versandt markieren", command=self._send_reminder, state="disabled"
|
||||
)
|
||||
self.send_reminder_button.pack(side="left", padx=(0, 8))
|
||||
ttk.Button(buttons, text="Mahnung vorbereiten", command=self._add_reminder).pack(side="left")
|
||||
self.send_reminder_button.pack(side="right", padx=(8, 0))
|
||||
ttk.Button(buttons, text="Mahnung vorbereiten", command=self._add_reminder).pack(side="right")
|
||||
|
||||
def refresh(self) -> None:
|
||||
try:
|
||||
@@ -199,66 +173,111 @@ class ClaimTab(ttk.Frame):
|
||||
self.summary_vars["status"].set(CLAIM_STATUS_LABELS.get(status, status.upper()))
|
||||
self.cancel_button.configure(state="disabled" if status == "cancelled" else "normal")
|
||||
self.hold_button.configure(text="Mahnsperre aufheben" if hold.get("active") else "Mahnsperre setzen")
|
||||
self._render_positions()
|
||||
self._render_payments()
|
||||
self._render_reminders()
|
||||
self._render_ledger()
|
||||
|
||||
def _render_positions(self) -> None:
|
||||
self.positions.delete(*self.positions.get_children())
|
||||
for item in claim_items(self.claim):
|
||||
self.positions.insert(
|
||||
def _render_ledger(self) -> None:
|
||||
self.ledger.delete(*self.ledger.get_children())
|
||||
items = claim_items(self.claim)
|
||||
position_group = self.ledger.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=str(item.get("item_id", "")),
|
||||
iid="group:positions",
|
||||
text=f"Positionen ({len(items)})",
|
||||
values=("", "", "", "", f"{money_text(claim_total(self.claim))} EUR", "", ""),
|
||||
tags=("position-group",),
|
||||
open=True,
|
||||
)
|
||||
for item in items:
|
||||
self.ledger.insert(
|
||||
position_group,
|
||||
"end",
|
||||
iid=f"item:{item.get('item_id', '')}",
|
||||
text=display_label(CLAIM_ITEM_TYPE_LABELS, str(item.get("type", ""))),
|
||||
values=(
|
||||
display_label(CLAIM_ITEM_TYPE_LABELS, str(item.get("type", ""))),
|
||||
str(item.get("created_at", ""))[:10],
|
||||
item.get("description", ""),
|
||||
item.get("quantity", "1"),
|
||||
item.get("unit_price", item.get("amount", "")),
|
||||
item.get("amount", ""),
|
||||
f"{item.get('amount', '')} EUR",
|
||||
"",
|
||||
"",
|
||||
),
|
||||
tags=("position",),
|
||||
)
|
||||
|
||||
def _render_payments(self) -> None:
|
||||
self.payments.delete(*self.payments.get_children())
|
||||
payment_by_id = {str(item.get("payment_id")): item for item in self.data.payments}
|
||||
for allocation in self.data.allocations:
|
||||
if str(allocation.get("claim_id", "")) != self.claim_id:
|
||||
continue
|
||||
payment = payment_by_id.get(str(allocation.get("payment_id", "")), {})
|
||||
self.payments.insert(
|
||||
allocations = [
|
||||
allocation
|
||||
for allocation in self.data.allocations
|
||||
if str(allocation.get("claim_id", "")) == self.claim_id
|
||||
]
|
||||
payment_group = self.ledger.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=str(allocation.get("allocation_id", "")),
|
||||
iid="group:payments",
|
||||
text=f"Zahlungen ({len(allocations)})",
|
||||
values=("", "", "", "", f"-{money_text(allocated_total(self.data, self.claim_id))} EUR", "", ""),
|
||||
tags=("payment-group",),
|
||||
open=True,
|
||||
)
|
||||
for allocation in allocations:
|
||||
payment = payment_by_id.get(str(allocation.get("payment_id", "")), {})
|
||||
payment_total = str(payment.get("amount", ""))
|
||||
gnucash_id = str(payment.get("gnucash_transaction_id", ""))
|
||||
self.ledger.insert(
|
||||
payment_group,
|
||||
"end",
|
||||
iid=f"allocation:{allocation.get('allocation_id', '')}",
|
||||
text="Zahlung",
|
||||
values=(
|
||||
format_date_for_display(str(payment.get("date", ""))),
|
||||
allocation.get("amount", ""),
|
||||
payment.get("amount", ""),
|
||||
payment.get("gnucash_transaction_id", ""),
|
||||
payment.get("reference", ""),
|
||||
"",
|
||||
"",
|
||||
f"-{allocation.get('amount', '')} EUR",
|
||||
f"Zahlung gesamt: {payment_total} EUR",
|
||||
f"GnuCash: {gnucash_id}" if gnucash_id else "",
|
||||
),
|
||||
tags=("payment",),
|
||||
)
|
||||
|
||||
def _render_reminders(self) -> None:
|
||||
self.reminders.delete(*self.reminders.get_children())
|
||||
items = {str(item.get("item_id")): item for item in claim_items(self.claim)}
|
||||
for reminder in self.data.reminders:
|
||||
if str(reminder.get("claim_id", "")) != self.claim_id:
|
||||
continue
|
||||
fee_item = items.get(str(reminder.get("fee_item_id", "")), {})
|
||||
status = str(reminder.get("status", "draft"))
|
||||
self.reminders.insert(
|
||||
reminders = [
|
||||
reminder
|
||||
for reminder in self.data.reminders
|
||||
if str(reminder.get("claim_id", "")) == self.claim_id
|
||||
]
|
||||
reminder_group = self.ledger.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=str(reminder.get("reminder_id", "")),
|
||||
iid="group:reminders",
|
||||
text=f"Mahnungen ({len(reminders)})",
|
||||
tags=("reminder-group",),
|
||||
open=True,
|
||||
)
|
||||
for reminder in reminders:
|
||||
status = str(reminder.get("status", "draft"))
|
||||
deadline = format_date_for_display(str(reminder.get("payment_deadline") or ""))
|
||||
channel = display_label(REMINDER_CHANNEL_LABELS, str(reminder.get("channel", "")))
|
||||
detail = str(reminder.get("detail", ""))
|
||||
self.ledger.insert(
|
||||
reminder_group,
|
||||
"end",
|
||||
iid=f"reminder:{reminder.get('reminder_id', '')}",
|
||||
text=f"Mahnstufe {reminder.get('level', '')}",
|
||||
values=(
|
||||
str(reminder.get("created_at", ""))[:16],
|
||||
reminder.get("level", ""),
|
||||
reminder.get("name", f"Mahnung Stufe {reminder.get('level', '')}"),
|
||||
"",
|
||||
"",
|
||||
f"{reminder.get('fee', '0.00')} EUR",
|
||||
display_label(REMINDER_STATUS_LABELS, status),
|
||||
format_date_for_display(str(reminder.get("payment_deadline") or "")),
|
||||
fee_item.get("amount", reminder.get("fee", "0.00")),
|
||||
" · ".join(
|
||||
part
|
||||
for part in (channel, f"Frist: {deadline}" if deadline else "", detail)
|
||||
if part
|
||||
),
|
||||
),
|
||||
tags=("reminder",),
|
||||
)
|
||||
self._update_reminder_buttons()
|
||||
|
||||
@@ -289,11 +308,12 @@ class ClaimTab(ttk.Frame):
|
||||
ReminderDialog(self, self.repository, self.member_id, self.claim_id, self._changed)
|
||||
|
||||
def _selected_reminder(self) -> dict | None:
|
||||
selected = self.reminders.selection()
|
||||
if not selected:
|
||||
selected = self.ledger.selection()
|
||||
if not selected or not selected[0].startswith("reminder:"):
|
||||
return None
|
||||
reminder_id = selected[0].removeprefix("reminder:")
|
||||
return next(
|
||||
(item for item in self.data.reminders if str(item.get("reminder_id", "")) == selected[0]),
|
||||
(item for item in self.data.reminders if str(item.get("reminder_id", "")) == reminder_id),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user