feat: unify claim activity view

This commit is contained in:
Marcel Peterkau
2026-06-21 21:51:47 +02:00
parent 2c89732228
commit b34135b34a
2 changed files with 129 additions and 108 deletions
+127 -107
View File
@@ -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="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=str(item.get("item_id", "")),
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
allocations = [
allocation
for allocation in self.data.allocations
if str(allocation.get("claim_id", "")) == self.claim_id
]
payment_group = self.ledger.insert(
"",
"end",
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", "")), {})
self.payments.insert(
"",
payment_total = str(payment.get("amount", ""))
gnucash_id = str(payment.get("gnucash_transaction_id", ""))
self.ledger.insert(
payment_group,
"end",
iid=str(allocation.get("allocation_id", "")),
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", "")), {})
reminders = [
reminder
for reminder in self.data.reminders
if str(reminder.get("claim_id", "")) == self.claim_id
]
reminder_group = self.ledger.insert(
"",
"end",
iid="group:reminders",
text=f"Mahnungen ({len(reminders)})",
tags=("reminder-group",),
open=True,
)
for reminder in reminders:
status = str(reminder.get("status", "draft"))
self.reminders.insert(
"",
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=str(reminder.get("reminder_id", "")),
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,
)