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:
@@ -115,6 +115,10 @@ class AssetTab(ttk.Frame):
|
||||
("Inventarnummer", "inventory_number"),
|
||||
("Seriennummer", "serial_number"),
|
||||
("Kaution (EUR)", "deposit_amount_default"),
|
||||
("Standort", "location"),
|
||||
("Übergabedatum", "handed_over_at"),
|
||||
("Zustand bei Übergabe", "condition"),
|
||||
("Wert / Kaufpreis", "estimated_value"),
|
||||
]
|
||||
for row, (label, key) in enumerate(fields):
|
||||
self.variables[key] = tk.StringVar()
|
||||
@@ -151,15 +155,22 @@ class AssetTab(ttk.Frame):
|
||||
)
|
||||
self.holder_label.grid(row=holder_row + 1, column=1, sticky="w", pady=5)
|
||||
self.holder_label.bind("<Button-1>", lambda _event: self._open_holder_member(), add="+")
|
||||
self.owner_var = tk.StringVar()
|
||||
ttk.Label(data_tab, text="Eigentümer").grid(
|
||||
row=holder_row + 2, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
self.owner_label = ttk.Label(data_tab, textvariable=self.owner_var, style="TimelineHeader.TLabel")
|
||||
self.owner_label.grid(row=holder_row + 2, column=1, sticky="w", pady=5)
|
||||
self.owner_label.bind("<Button-1>", lambda _event: self._open_owner_member(), add="+")
|
||||
ttk.Label(data_tab, text="Interne Notiz").grid(
|
||||
row=holder_row + 2,
|
||||
row=holder_row + 3,
|
||||
column=0,
|
||||
sticky="nw",
|
||||
pady=5,
|
||||
padx=(0, 12),
|
||||
)
|
||||
self.notes_text = tk.Text(data_tab, width=42, height=6, wrap="word")
|
||||
self.notes_text.grid(row=holder_row + 2, column=1, sticky="ew", pady=5)
|
||||
self.notes_text.grid(row=holder_row + 3, column=1, sticky="ew", pady=5)
|
||||
data_tab.columnconfigure(1, weight=1)
|
||||
self.issue_button = ttk.Button(
|
||||
action_buttons,
|
||||
@@ -276,13 +287,19 @@ class AssetTab(ttk.Frame):
|
||||
self.variables["inventory_number"].set(self.asset.inventory_number)
|
||||
self.variables["serial_number"].set(self.asset.serial_number)
|
||||
self.variables["deposit_amount_default"].set(self.asset.deposit_amount_default)
|
||||
self.variables["location"].set(self.asset.location)
|
||||
self.variables["handed_over_at"].set(self.asset.handed_over_at)
|
||||
self.variables["condition"].set(self.asset.condition)
|
||||
self.variables["estimated_value"].set(self.asset.estimated_value)
|
||||
self.variables["status"].set(ASSET_STATUS_LABELS.get(self.asset.status, self.asset.status.upper()))
|
||||
if self.notes_text is not None:
|
||||
self.notes_text.delete("1.0", "end")
|
||||
self.notes_text.insert("1.0", self.asset.notes)
|
||||
self.holder_var.set(holder)
|
||||
self.owner_var.set(self._owner_label())
|
||||
issued = bool(self.asset.current_holder_member_id)
|
||||
self.holder_label.configure(cursor="hand2" if issued else "")
|
||||
self.owner_label.configure(cursor="hand2" if self.asset.owner_type == "member" else "")
|
||||
self.issue_button.configure(state="normal" if self.asset.status == "available" else "disabled")
|
||||
self.return_button.configure(state="normal" if issued else "disabled")
|
||||
self.status_box.configure(state="disabled" if issued else "readonly")
|
||||
@@ -300,6 +317,21 @@ class AssetTab(ttk.Frame):
|
||||
return self.asset.current_holder_member_id
|
||||
return f"{member.member_number or member.member_id} · {member.display_name}"
|
||||
|
||||
def _owner_label(self) -> str:
|
||||
if self.asset.owner_type == "club":
|
||||
return "Verein"
|
||||
if self.asset.owner_type == "external":
|
||||
return self.asset.owner_name or "Extern"
|
||||
try:
|
||||
member = self.repository.get_member(self.asset.owner_member_id)
|
||||
except RepositoryError:
|
||||
return self.asset.owner_member_id or "—"
|
||||
return f"{member.member_number or member.member_id} · {member.display_name}"
|
||||
|
||||
def _open_owner_member(self) -> None:
|
||||
if self.asset.owner_type == "member" and self.asset.owner_member_id:
|
||||
self.on_open_member(self.asset.owner_member_id)
|
||||
|
||||
def _refresh_events(self) -> None:
|
||||
self.timeline.delete(*self.timeline.get_children())
|
||||
try:
|
||||
@@ -352,6 +384,10 @@ class AssetTab(ttk.Frame):
|
||||
self.asset.inventory_number = self.variables["inventory_number"].get().strip()
|
||||
self.asset.serial_number = self.variables["serial_number"].get().strip()
|
||||
self.asset.notes = self.notes_text.get("1.0", "end-1c").strip() if self.notes_text else ""
|
||||
self.asset.location = self.variables["location"].get().strip()
|
||||
self.asset.handed_over_at = self.variables["handed_over_at"].get().strip()
|
||||
self.asset.condition = self.variables["condition"].get().strip()
|
||||
self.asset.estimated_value = self.variables["estimated_value"].get().strip()
|
||||
if not self.asset.current_holder_member_id:
|
||||
self.asset.deposit_amount_default = self.variables["deposit_amount_default"].get().strip()
|
||||
self.asset.status = _asset_status_key(self.variables["status"].get())
|
||||
|
||||
@@ -88,8 +88,10 @@ class ClaimTab(ttk.Frame):
|
||||
footer.columnconfigure(0, weight=1)
|
||||
self.hold_button = ttk.Button(footer, text="Mahnsperre setzen", command=self._toggle_hold)
|
||||
self.hold_button.grid(row=0, column=0, sticky="w")
|
||||
self.edit_button = ttk.Button(footer, text="Forderung bearbeiten", command=self._edit_claim)
|
||||
self.edit_button.grid(row=0, column=1, sticky="e", padx=(0, 8))
|
||||
self.cancel_button = ttk.Button(footer, text="Forderung stornieren", command=self._cancel_claim)
|
||||
self.cancel_button.grid(row=0, column=1, sticky="e")
|
||||
self.cancel_button.grid(row=0, column=2, sticky="e")
|
||||
|
||||
def _build_ledger(self) -> None:
|
||||
ledger = ttk.Frame(self, padding=12)
|
||||
@@ -185,6 +187,7 @@ class ClaimTab(ttk.Frame):
|
||||
self.summary_vars["balance"].set(f"{money_text(balance)} EUR")
|
||||
self.summary_vars["status"].set(CLAIM_STATUS_LABELS.get(status, status.upper()))
|
||||
self.cancel_button.configure(state="disabled" if status == "cancelled" else "normal")
|
||||
self.edit_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_ledger()
|
||||
|
||||
@@ -478,6 +481,16 @@ class ClaimTab(ttk.Frame):
|
||||
return
|
||||
self._changed()
|
||||
|
||||
def _edit_claim(self) -> None:
|
||||
ClaimEditDialog(
|
||||
self,
|
||||
self.repository,
|
||||
self.member_id,
|
||||
self.claim_id,
|
||||
self.claim,
|
||||
self._changed,
|
||||
)
|
||||
|
||||
def _changed(self) -> None:
|
||||
self.refresh()
|
||||
self.on_changed()
|
||||
@@ -502,6 +515,68 @@ class _Dialog(tk.Toplevel):
|
||||
ttk.Button(buttons, text="Speichern", style="Accent.TButton", command=command).pack(side="left")
|
||||
|
||||
|
||||
class ClaimEditDialog(_Dialog):
|
||||
def __init__(self, master, repository, member_id, claim_id, claim, on_saved):
|
||||
super().__init__(master, "Forderung bearbeiten", on_saved)
|
||||
self.repository = repository
|
||||
self.member_id = member_id
|
||||
self.claim_id = claim_id
|
||||
items = claim_items(claim)
|
||||
base_item = next(
|
||||
(item for item in items if str(item.get("type", "")) == "base"),
|
||||
items[0] if items else {},
|
||||
)
|
||||
self.variables = {
|
||||
"title": tk.StringVar(value=str(claim.get("title", ""))),
|
||||
"due_date": tk.StringVar(
|
||||
value=format_date_for_display(str(claim.get("due_date", "")))
|
||||
),
|
||||
"base_amount": tk.StringVar(
|
||||
value=str(base_item.get("amount", claim.get("amount", "")))
|
||||
),
|
||||
"description": tk.StringVar(value=str(base_item.get("description", ""))),
|
||||
}
|
||||
fields = (
|
||||
("Titel", "title"),
|
||||
(f"Fällig am ({date_input_hint()})", "due_date"),
|
||||
("Grundbetrag", "base_amount"),
|
||||
("Beschreibung der Grundposition", "description"),
|
||||
)
|
||||
for row, (label, key) in enumerate(fields):
|
||||
ttk.Label(self.frame, text=label).grid(
|
||||
row=row, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Entry(self.frame, textvariable=self.variables[key], width=52).grid(
|
||||
row=row, column=1, sticky="ew", pady=5
|
||||
)
|
||||
ttk.Label(
|
||||
self.frame,
|
||||
text=(
|
||||
"Der Grundbetrag ändert nur die Grundposition. Weitere Positionen, "
|
||||
"zum Beispiel Mahngebühren, bleiben erhalten."
|
||||
),
|
||||
style="Mono.TLabel",
|
||||
wraplength=520,
|
||||
).grid(row=len(fields), column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
self._buttons(len(fields) + 1, self._save)
|
||||
|
||||
def _save(self) -> None:
|
||||
try:
|
||||
self.repository.update_claim(
|
||||
self.member_id,
|
||||
self.claim_id,
|
||||
title=self.variables["title"].get(),
|
||||
due_date=self.variables["due_date"].get(),
|
||||
base_amount=self.variables["base_amount"].get(),
|
||||
description=self.variables["description"].get(),
|
||||
)
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Forderung konnte nicht gespeichert werden", str(exc), parent=self)
|
||||
return
|
||||
self.destroy()
|
||||
self.on_saved()
|
||||
|
||||
|
||||
class ItemDialog(_Dialog):
|
||||
def __init__(self, master, repository, member_id, claim_id, on_saved):
|
||||
super().__init__(master, "Forderungsposition hinzufügen", on_saved)
|
||||
@@ -634,7 +709,11 @@ class AllocatePaymentDialog(_Dialog):
|
||||
self.repository, self.member_id, self.claim_id = repository, member_id, claim_id
|
||||
data = repository.get_contributions(member_id)
|
||||
self.payment_by_label = {}
|
||||
for payment in data.payments:
|
||||
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", ""))
|
||||
available = decimal_value(payment.get("amount", "0")) - payment_allocated_total(data, payment_id)
|
||||
if available <= 0:
|
||||
@@ -655,10 +734,23 @@ class AllocatePaymentDialog(_Dialog):
|
||||
width=60,
|
||||
)
|
||||
combo.grid(row=0, column=1, pady=5)
|
||||
ttk.Label(self.frame, text="Betrag").grid(row=1, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Entry(self.frame, textvariable=self.amount_var).grid(row=1, column=1, sticky="ew", pady=5)
|
||||
if not self.payment_by_label:
|
||||
ttk.Label(
|
||||
self.frame,
|
||||
text="Für dieses Mitglied gibt es keine Zahlung mit freiem Restbetrag.",
|
||||
style="Mono.TLabel",
|
||||
).grid(row=1, column=0, columnspan=2, sticky="w", pady=(3, 5))
|
||||
amount_row = 2
|
||||
else:
|
||||
amount_row = 1
|
||||
ttk.Label(self.frame, text="Betrag").grid(
|
||||
row=amount_row, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Entry(self.frame, textvariable=self.amount_var).grid(
|
||||
row=amount_row, column=1, sticky="ew", pady=5
|
||||
)
|
||||
combo.bind("<<ComboboxSelected>>", lambda _event: self._select(balance))
|
||||
self._buttons(2, self._save)
|
||||
self._buttons(amount_row + 1, self._save)
|
||||
|
||||
def _select(self, balance):
|
||||
_payment_id, available = self.payment_by_label[self.payment_var.get()]
|
||||
|
||||
+139
-19
@@ -124,13 +124,31 @@ class NewAssetDialog(tk.Toplevel):
|
||||
super().__init__(master)
|
||||
self.repository = repository
|
||||
self.on_created = on_created
|
||||
self.members = repository.list_members()
|
||||
self.member_labels = {
|
||||
f"{member.member_number or member.member_id} · {member.display_name}": member.member_id
|
||||
for member in self.members
|
||||
}
|
||||
self.title("Neues Asset")
|
||||
self.transient(master.winfo_toplevel())
|
||||
self.resizable(False, False)
|
||||
self.variables = {
|
||||
name: tk.StringVar()
|
||||
for name in ("label", "category", "inventory_number", "serial_number", "deposit_amount_default")
|
||||
for name in (
|
||||
"label",
|
||||
"category",
|
||||
"inventory_number",
|
||||
"serial_number",
|
||||
"deposit_amount_default",
|
||||
"location",
|
||||
"handed_over_at",
|
||||
"condition",
|
||||
"estimated_value",
|
||||
"owner_name",
|
||||
)
|
||||
}
|
||||
self.owner_type_var = tk.StringVar(value="VEREIN")
|
||||
self.owner_member_var = tk.StringVar()
|
||||
self._build_ui()
|
||||
self.bind("<Escape>", lambda _event: self.destroy())
|
||||
self.bind("<Return>", lambda _event: self._create())
|
||||
@@ -145,6 +163,10 @@ class NewAssetDialog(tk.Toplevel):
|
||||
("Inventarnummer", "inventory_number"),
|
||||
("Seriennummer", "serial_number"),
|
||||
("Kaution (EUR)", "deposit_amount_default"),
|
||||
("Standort", "location"),
|
||||
("Übergabedatum", "handed_over_at"),
|
||||
("Zustand bei Übergabe", "condition"),
|
||||
("Wert / Kaufpreis", "estimated_value"),
|
||||
]
|
||||
self.entries: dict[str, ttk.Entry] = {}
|
||||
for row, (label, key) in enumerate(fields):
|
||||
@@ -152,17 +174,42 @@ class NewAssetDialog(tk.Toplevel):
|
||||
entry = ttk.Entry(frame, textvariable=self.variables[key], width=38)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=5)
|
||||
self.entries[key] = entry
|
||||
owner_row = len(fields)
|
||||
ttk.Label(frame, text="Eigentümer").grid(row=owner_row, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.owner_type_var,
|
||||
values=("VEREIN", "MITGLIED", "EXTERN"),
|
||||
state="readonly",
|
||||
width=35,
|
||||
).grid(row=owner_row, column=1, sticky="ew", pady=5)
|
||||
ttk.Label(frame, text="Eigentümer-Mitglied").grid(
|
||||
row=owner_row + 1, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.owner_member_var,
|
||||
values=list(self.member_labels),
|
||||
state="readonly",
|
||||
width=35,
|
||||
).grid(row=owner_row + 1, column=1, sticky="ew", pady=5)
|
||||
ttk.Label(frame, text="Externer Eigentümer").grid(
|
||||
row=owner_row + 2, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Entry(frame, textvariable=self.variables["owner_name"], width=38).grid(
|
||||
row=owner_row + 2, column=1, sticky="ew", pady=5
|
||||
)
|
||||
ttk.Label(frame, text="Interne Notiz").grid(
|
||||
row=len(fields),
|
||||
row=owner_row + 3,
|
||||
column=0,
|
||||
sticky="nw",
|
||||
pady=5,
|
||||
padx=(0, 12),
|
||||
)
|
||||
self.notes_text = tk.Text(frame, width=38, height=5, wrap="word")
|
||||
self.notes_text.grid(row=len(fields), column=1, sticky="ew", pady=5)
|
||||
self.notes_text.grid(row=owner_row + 3, column=1, sticky="ew", pady=5)
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.grid(row=len(fields) + 1, column=0, columnspan=2, sticky="e", pady=(16, 0))
|
||||
buttons.grid(row=owner_row + 4, column=0, columnspan=2, sticky="e", pady=(16, 0))
|
||||
ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(
|
||||
buttons,
|
||||
@@ -176,9 +223,15 @@ class NewAssetDialog(tk.Toplevel):
|
||||
_activate_modal_window(self, self.entries.get("label"))
|
||||
|
||||
def _create(self) -> None:
|
||||
owner_type = {"VEREIN": "club", "MITGLIED": "member", "EXTERN": "external"}[self.owner_type_var.get()]
|
||||
try:
|
||||
asset = self.repository.create_asset(
|
||||
**{key: variable.get() for key, variable in self.variables.items()},
|
||||
**{key: variable.get() for key, variable in self.variables.items() if key != "owner_name"},
|
||||
owner_type=owner_type,
|
||||
owner_member_id=(
|
||||
self.member_labels.get(self.owner_member_var.get(), "") if owner_type == "member" else ""
|
||||
),
|
||||
owner_name=self.variables["owner_name"].get() if owner_type == "external" else "",
|
||||
notes=self.notes_text.get("1.0", "end-1c"),
|
||||
)
|
||||
except RepositoryError as exc:
|
||||
@@ -201,6 +254,19 @@ class EditAssetDialog(tk.Toplevel):
|
||||
self.asset_id = asset_id
|
||||
self.on_saved = on_saved
|
||||
self.asset = repository.get_asset(asset_id)
|
||||
self.members = repository.list_members()
|
||||
self.member_labels = {
|
||||
f"{member.member_number or member.member_id} · {member.display_name}": member.member_id
|
||||
for member in self.members
|
||||
}
|
||||
owner_member_label = next(
|
||||
(
|
||||
label
|
||||
for label, member_id in self.member_labels.items()
|
||||
if member_id == self.asset.owner_member_id
|
||||
),
|
||||
"",
|
||||
)
|
||||
self.title("Asset bearbeiten")
|
||||
self.transient(master.winfo_toplevel())
|
||||
self.resizable(False, False)
|
||||
@@ -211,7 +277,18 @@ class EditAssetDialog(tk.Toplevel):
|
||||
"serial_number": tk.StringVar(value=self.asset.serial_number),
|
||||
"deposit_amount_default": tk.StringVar(value=self.asset.deposit_amount_default),
|
||||
"status": tk.StringVar(value=self.asset.status),
|
||||
"location": tk.StringVar(value=self.asset.location),
|
||||
"handed_over_at": tk.StringVar(value=self.asset.handed_over_at),
|
||||
"condition": tk.StringVar(value=self.asset.condition),
|
||||
"estimated_value": tk.StringVar(value=self.asset.estimated_value),
|
||||
"owner_name": tk.StringVar(value=self.asset.owner_name),
|
||||
}
|
||||
self.owner_type_var = tk.StringVar(
|
||||
value={"club": "VEREIN", "member": "MITGLIED", "external": "EXTERN"}.get(
|
||||
self.asset.owner_type, "VEREIN"
|
||||
)
|
||||
)
|
||||
self.owner_member_var = tk.StringVar(value=owner_member_label)
|
||||
self._build_ui()
|
||||
self.bind("<Escape>", lambda _event: self.destroy())
|
||||
self.bind("<Return>", lambda _event: self._save())
|
||||
@@ -227,6 +304,10 @@ class EditAssetDialog(tk.Toplevel):
|
||||
("Inventarnummer", "inventory_number"),
|
||||
("Seriennummer", "serial_number"),
|
||||
("Kaution (EUR)", "deposit_amount_default"),
|
||||
("Standort", "location"),
|
||||
("Übergabedatum", "handed_over_at"),
|
||||
("Zustand bei Übergabe", "condition"),
|
||||
("Wert / Kaufpreis", "estimated_value"),
|
||||
]
|
||||
self.entries: dict[str, ttk.Entry] = {}
|
||||
for row, (label, key) in enumerate(fields):
|
||||
@@ -235,12 +316,40 @@ class EditAssetDialog(tk.Toplevel):
|
||||
entry = ttk.Entry(frame, textvariable=self.variables[key], width=38, state=state)
|
||||
entry.grid(row=row, column=1, sticky="ew", pady=5)
|
||||
self.entries[key] = entry
|
||||
ttk.Label(frame, text="Status").grid(row=len(fields), column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
status_values = [value for key, value in (
|
||||
("available", "VERFUEGBAR"),
|
||||
("lost", "VERLOREN"),
|
||||
("retired", "AUSGEMUSTERT"),
|
||||
)]
|
||||
owner_row = len(fields)
|
||||
ttk.Label(frame, text="Eigentümer").grid(row=owner_row, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.owner_type_var,
|
||||
values=("VEREIN", "MITGLIED", "EXTERN"),
|
||||
state="readonly",
|
||||
width=35,
|
||||
).grid(row=owner_row, column=1, sticky="ew", pady=5)
|
||||
ttk.Label(frame, text="Eigentümer-Mitglied").grid(
|
||||
row=owner_row + 1, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.owner_member_var,
|
||||
values=list(self.member_labels),
|
||||
state="readonly",
|
||||
width=35,
|
||||
).grid(row=owner_row + 1, column=1, sticky="ew", pady=5)
|
||||
ttk.Label(frame, text="Externer Eigentümer").grid(
|
||||
row=owner_row + 2, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Entry(frame, textvariable=self.variables["owner_name"], width=38).grid(
|
||||
row=owner_row + 2, column=1, sticky="ew", pady=5
|
||||
)
|
||||
ttk.Label(frame, text="Status").grid(row=owner_row + 3, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
status_values = [
|
||||
value
|
||||
for key, value in (
|
||||
("available", "VERFUEGBAR"),
|
||||
("lost", "VERLOREN"),
|
||||
("retired", "AUSGEMUSTERT"),
|
||||
)
|
||||
]
|
||||
self.status_map = {
|
||||
"VERFUEGBAR": "available",
|
||||
"VERLOREN": "lost",
|
||||
@@ -260,8 +369,8 @@ class EditAssetDialog(tk.Toplevel):
|
||||
state="readonly" if not issued else "disabled",
|
||||
width=35,
|
||||
)
|
||||
self.status_box.grid(row=len(fields), column=1, sticky="ew", pady=5)
|
||||
note_row = len(fields) + 1
|
||||
self.status_box.grid(row=owner_row + 3, column=1, sticky="ew", pady=5)
|
||||
note_row = owner_row + 4
|
||||
ttk.Label(frame, text="Interne Notiz").grid(row=note_row, column=0, sticky="nw", pady=5, padx=(0, 12))
|
||||
self.notes_text = tk.Text(frame, width=38, height=5, wrap="word")
|
||||
self.notes_text.grid(row=note_row, column=1, sticky="ew", pady=5)
|
||||
@@ -290,6 +399,21 @@ class EditAssetDialog(tk.Toplevel):
|
||||
self.asset.inventory_number = self.variables["inventory_number"].get()
|
||||
self.asset.serial_number = self.variables["serial_number"].get()
|
||||
self.asset.notes = self.notes_text.get("1.0", "end-1c")
|
||||
self.asset.location = self.variables["location"].get()
|
||||
self.asset.handed_over_at = self.variables["handed_over_at"].get()
|
||||
self.asset.condition = self.variables["condition"].get()
|
||||
self.asset.estimated_value = self.variables["estimated_value"].get()
|
||||
self.asset.owner_type = {"VEREIN": "club", "MITGLIED": "member", "EXTERN": "external"}[
|
||||
self.owner_type_var.get()
|
||||
]
|
||||
self.asset.owner_member_id = (
|
||||
self.member_labels.get(self.owner_member_var.get(), "")
|
||||
if self.asset.owner_type == "member"
|
||||
else ""
|
||||
)
|
||||
self.asset.owner_name = (
|
||||
self.variables["owner_name"].get() if self.asset.owner_type == "external" else ""
|
||||
)
|
||||
if not self.asset.current_holder_member_id:
|
||||
self.asset.deposit_amount_default = self.variables["deposit_amount_default"].get()
|
||||
self.asset.status = self.status_map.get(self.status_var.get(), self.asset.status)
|
||||
@@ -394,9 +518,7 @@ class IssueAssetDialog(tk.Toplevel):
|
||||
self.member_tree.delete(*self.member_tree.get_children())
|
||||
query = self.search_var.get().strip().casefold()
|
||||
filtered = [
|
||||
member
|
||||
for member in self.members
|
||||
if not query or query in self._member_search_text(member)
|
||||
member for member in self.members if not query or query in self._member_search_text(member)
|
||||
]
|
||||
for member in filtered:
|
||||
self.member_tree.insert(
|
||||
@@ -599,9 +721,7 @@ class IntegrityWarningDialog(tk.Toplevel):
|
||||
text="\n".join(f"• {item}" for item in self.warnings),
|
||||
style="Warning.TLabel",
|
||||
justify="left",
|
||||
).grid(
|
||||
row=1, column=0, sticky="w", pady=(12, 0)
|
||||
)
|
||||
).grid(row=1, column=0, sticky="w", pady=(12, 0))
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.grid(row=2, column=0, sticky="e", pady=(18, 0))
|
||||
ttk.Button(buttons, text="Nein", command=self.destroy).pack(side="left", padx=(0, 8))
|
||||
|
||||
+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)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from ccma.domain.contributions import (
|
||||
claim_balance,
|
||||
claim_status,
|
||||
decimal_value,
|
||||
money_text,
|
||||
)
|
||||
from ccma.domain.dates import date_input_hint, format_date_for_display
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
|
||||
|
||||
class PaymentEditDialog(tk.Toplevel):
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.Misc,
|
||||
repository: MemberRepository,
|
||||
member_id: str,
|
||||
payment_id: str,
|
||||
on_saved: Callable[[], None],
|
||||
):
|
||||
super().__init__(master)
|
||||
self.repository = repository
|
||||
self.member_id = member_id
|
||||
self.payment_id = payment_id
|
||||
self.on_saved = on_saved
|
||||
self.data = repository.get_contributions(member_id)
|
||||
self.payment = next(
|
||||
(
|
||||
payment
|
||||
for payment in self.data.payments
|
||||
if str(payment.get("payment_id", "")) == payment_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if self.payment is None:
|
||||
raise RepositoryError("Zahlung nicht gefunden.")
|
||||
self.allocations = self._current_allocations()
|
||||
self.claims_by_id = {
|
||||
str(claim.get("claim_id", "")): claim
|
||||
for claim in self.data.claims
|
||||
if str(claim.get("claim_id", ""))
|
||||
and (
|
||||
claim_status(self.data, claim) != "cancelled"
|
||||
or str(claim.get("claim_id", "")) in self.allocations
|
||||
)
|
||||
}
|
||||
self.capacities = self._claim_capacities()
|
||||
|
||||
self.title("Zahlung bearbeiten")
|
||||
self.transient(master.winfo_toplevel())
|
||||
self.geometry("880x560")
|
||||
self.minsize(720, 460)
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
self.bind("<Escape>", lambda _event: self.destroy())
|
||||
self._build_ui()
|
||||
self._refresh_claims()
|
||||
self.after_idle(self._activate)
|
||||
|
||||
def _current_allocations(self) -> dict[str, str]:
|
||||
totals: dict[str, Decimal] = {}
|
||||
for allocation in self.data.allocations:
|
||||
if str(allocation.get("payment_id", "")) != self.payment_id:
|
||||
continue
|
||||
claim_id = str(allocation.get("claim_id", ""))
|
||||
totals[claim_id] = totals.get(claim_id, Decimal("0")) + decimal_value(
|
||||
allocation.get("amount", "0")
|
||||
)
|
||||
return {claim_id: money_text(amount) for claim_id, amount in totals.items() if claim_id}
|
||||
|
||||
def _claim_capacities(self) -> dict[str, Decimal]:
|
||||
capacities = {}
|
||||
for claim_id, claim in self.claims_by_id.items():
|
||||
current = decimal_value(self.allocations.get(claim_id, "0"))
|
||||
capacities[claim_id] = max(claim_balance(self.data, claim) + current, Decimal("0"))
|
||||
return capacities
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(1, weight=1)
|
||||
form = ttk.Frame(self, padding=16)
|
||||
form.grid(row=0, column=0, sticky="ew")
|
||||
form.columnconfigure(1, weight=1)
|
||||
self.variables = {
|
||||
"date": tk.StringVar(
|
||||
value=format_date_for_display(str(self.payment.get("date", "")))
|
||||
),
|
||||
"amount": tk.StringVar(value=str(self.payment.get("amount", ""))),
|
||||
"gnucash": tk.StringVar(value=str(self.payment.get("gnucash_transaction_id", ""))),
|
||||
"reference": tk.StringVar(value=str(self.payment.get("reference", ""))),
|
||||
}
|
||||
fields = (
|
||||
(f"Zahlungsdatum ({date_input_hint()})", "date"),
|
||||
("Zahlungsbetrag", "amount"),
|
||||
("GnuCash-ID", "gnucash"),
|
||||
("Referenz", "reference"),
|
||||
)
|
||||
for row, (label, key) in enumerate(fields):
|
||||
ttk.Label(form, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=4)
|
||||
ttk.Entry(form, textvariable=self.variables[key], width=70).grid(
|
||||
row=row, column=1, sticky="ew", pady=4
|
||||
)
|
||||
self.variables["amount"].trace_add("write", lambda *_args: self._refresh_totals())
|
||||
|
||||
allocation_frame = ttk.LabelFrame(self, text="Aufteilung auf Forderungen", padding=12)
|
||||
allocation_frame.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 12))
|
||||
allocation_frame.columnconfigure(0, weight=1)
|
||||
allocation_frame.rowconfigure(0, weight=1)
|
||||
self.claims = ttk.Treeview(
|
||||
allocation_frame,
|
||||
columns=("title", "due", "available", "allocated"),
|
||||
show="headings",
|
||||
selectmode="browse",
|
||||
)
|
||||
for key, title, width in (
|
||||
("title", "Forderung", 300),
|
||||
("due", "Fällig", 110),
|
||||
("available", "Maximal zuordenbar", 150),
|
||||
("allocated", "Aktuell zugeordnet", 150),
|
||||
):
|
||||
self.claims.heading(key, text=title)
|
||||
self.claims.column(key, width=width, anchor="w")
|
||||
self.claims.grid(row=0, column=0, sticky="nsew")
|
||||
scrollbar = ttk.Scrollbar(allocation_frame, orient="vertical", command=self.claims.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.claims.configure(yscrollcommand=scrollbar.set)
|
||||
self.claims.bind("<<TreeviewSelect>>", self._select_claim)
|
||||
|
||||
allocation_actions = ttk.Frame(allocation_frame)
|
||||
allocation_actions.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
|
||||
ttk.Label(allocation_actions, text="Betrag für ausgewählte Forderung").pack(side="left")
|
||||
self.allocation_var = tk.StringVar()
|
||||
ttk.Entry(allocation_actions, textvariable=self.allocation_var, width=14).pack(
|
||||
side="left", padx=(8, 8)
|
||||
)
|
||||
ttk.Button(allocation_actions, text="Zuordnung setzen", command=self._set_allocation).pack(
|
||||
side="left"
|
||||
)
|
||||
ttk.Button(allocation_actions, text="Zuordnung lösen", command=self._remove_allocation).pack(
|
||||
side="left", padx=(8, 0)
|
||||
)
|
||||
self.total_var = tk.StringVar()
|
||||
ttk.Label(allocation_actions, textvariable=self.total_var, style="Mono.TLabel").pack(
|
||||
side="right"
|
||||
)
|
||||
|
||||
buttons = ttk.Frame(self, padding=(16, 0, 16, 16))
|
||||
buttons.grid(row=2, column=0, sticky="e")
|
||||
ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(
|
||||
buttons,
|
||||
text="Zahlung speichern",
|
||||
style="Accent.TButton",
|
||||
command=self._save,
|
||||
).pack(side="left")
|
||||
|
||||
def _activate(self) -> None:
|
||||
try:
|
||||
self.deiconify()
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
self.grab_set()
|
||||
except tk.TclError:
|
||||
return
|
||||
|
||||
def _refresh_claims(self) -> None:
|
||||
selected = self.claims.selection()
|
||||
self.claims.delete(*self.claims.get_children())
|
||||
ordered = sorted(
|
||||
self.claims_by_id.items(),
|
||||
key=lambda item: (
|
||||
str(item[1].get("due_date", "")),
|
||||
str(item[1].get("title", "")).casefold(),
|
||||
),
|
||||
)
|
||||
for claim_id, claim in ordered:
|
||||
self.claims.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=claim_id,
|
||||
values=(
|
||||
claim.get("title", "Forderung"),
|
||||
format_date_for_display(str(claim.get("due_date", ""))),
|
||||
f"{money_text(self.capacities[claim_id])} EUR",
|
||||
f"{self.allocations.get(claim_id, '0.00')} EUR",
|
||||
),
|
||||
)
|
||||
if selected and self.claims.exists(selected[0]):
|
||||
self.claims.selection_set(selected[0])
|
||||
self._refresh_totals()
|
||||
|
||||
def _select_claim(self, _event=None) -> None:
|
||||
selected = self.claims.selection()
|
||||
if selected:
|
||||
self.allocation_var.set(self.allocations.get(selected[0], "0.00"))
|
||||
|
||||
def _set_allocation(self) -> None:
|
||||
selected = self.claims.selection()
|
||||
if not selected:
|
||||
messagebox.showerror("Forderung auswählen", "Bitte eine Forderung auswählen.", parent=self)
|
||||
return
|
||||
try:
|
||||
amount = decimal_value(self.allocation_var.get(), "Zuordnung")
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("Ungültige Zuordnung", str(exc), parent=self)
|
||||
return
|
||||
capacity = self.capacities[selected[0]]
|
||||
if amount < 0 or amount > capacity:
|
||||
messagebox.showerror(
|
||||
"Ungültige Zuordnung",
|
||||
f"Für diese Forderung können höchstens {money_text(capacity)} EUR zugeordnet werden.",
|
||||
parent=self,
|
||||
)
|
||||
return
|
||||
if amount:
|
||||
self.allocations[selected[0]] = money_text(amount)
|
||||
else:
|
||||
self.allocations.pop(selected[0], None)
|
||||
self._refresh_claims()
|
||||
|
||||
def _remove_allocation(self) -> None:
|
||||
selected = self.claims.selection()
|
||||
if not selected:
|
||||
messagebox.showerror("Forderung auswählen", "Bitte eine Forderung auswählen.", parent=self)
|
||||
return
|
||||
self.allocations.pop(selected[0], None)
|
||||
self.allocation_var.set("0.00")
|
||||
self._refresh_claims()
|
||||
|
||||
def _refresh_totals(self) -> None:
|
||||
allocated = sum(
|
||||
(decimal_value(value) for value in self.allocations.values()), Decimal("0")
|
||||
)
|
||||
try:
|
||||
payment_amount = decimal_value(self.variables["amount"].get())
|
||||
free = payment_amount - allocated
|
||||
self.total_var.set(
|
||||
f"Zugeordnet: {money_text(allocated)} EUR · Frei: {money_text(free)} EUR"
|
||||
)
|
||||
except (ValueError, InvalidOperation):
|
||||
self.total_var.set(f"Zugeordnet: {money_text(allocated)} EUR · Betrag ungültig")
|
||||
|
||||
def _save(self) -> None:
|
||||
try:
|
||||
self.repository.update_payment(
|
||||
self.member_id,
|
||||
self.payment_id,
|
||||
payment_date=self.variables["date"].get(),
|
||||
amount=self.variables["amount"].get(),
|
||||
allocations=self.allocations,
|
||||
gnucash_transaction_id=self.variables["gnucash"].get(),
|
||||
reference=self.variables["reference"].get(),
|
||||
)
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Zahlung konnte nicht gespeichert werden", str(exc), parent=self)
|
||||
return
|
||||
self.destroy()
|
||||
self.on_saved()
|
||||
@@ -31,7 +31,9 @@ ASSET_TABLE_COLUMNS = (
|
||||
("category", "Kategorie", 140),
|
||||
("inventory_number", "Inventarnummer", 140),
|
||||
("status", "Status", 140),
|
||||
("holder", "Mitglied", 240),
|
||||
("owner", "Eigentümer", 240),
|
||||
("holder", "Gewahrsam", 240),
|
||||
("location", "Standort", 180),
|
||||
)
|
||||
|
||||
|
||||
@@ -77,7 +79,7 @@ def _filter_label_frame(parent: tk.Misc) -> ttk.LabelFrame:
|
||||
return titled_frame(parent, "// FILTER")
|
||||
|
||||
|
||||
def _asset_table_value(asset: Asset, column: str, holder_label: str) -> str:
|
||||
def _asset_table_value(asset: Asset, column: str, holder_label: str, owner_label: str = "") -> str:
|
||||
if column == "label":
|
||||
return asset.label
|
||||
if column == "category":
|
||||
@@ -88,6 +90,10 @@ def _asset_table_value(asset: Asset, column: str, holder_label: str) -> str:
|
||||
return ASSET_STATUS_LABELS.get(asset.status, asset.status)
|
||||
if column == "holder":
|
||||
return holder_label
|
||||
if column == "owner":
|
||||
return owner_label
|
||||
if column == "location":
|
||||
return asset.location
|
||||
return ""
|
||||
|
||||
|
||||
@@ -429,6 +435,7 @@ class AssetsTab(ttk.Frame):
|
||||
asset,
|
||||
self.sort_column,
|
||||
self.resolve_holder_label(asset.current_holder_member_id),
|
||||
self._owner_label(asset),
|
||||
).casefold(),
|
||||
reverse=self.sort_descending,
|
||||
)
|
||||
@@ -447,11 +454,20 @@ class AssetsTab(ttk.Frame):
|
||||
asset.category,
|
||||
asset.inventory_number,
|
||||
ASSET_STATUS_LABELS.get(asset.status, asset.status),
|
||||
self._owner_label(asset),
|
||||
self.resolve_holder_label(asset.current_holder_member_id),
|
||||
asset.location,
|
||||
),
|
||||
)
|
||||
self._update_actions()
|
||||
|
||||
def _owner_label(self, asset: Asset) -> str:
|
||||
if asset.owner_type == "club":
|
||||
return "Verein"
|
||||
if asset.owner_type == "member":
|
||||
return self.resolve_holder_label(asset.owner_member_id)
|
||||
return asset.owner_name or "Extern"
|
||||
|
||||
def _toggle_sort(self, column: str) -> None:
|
||||
if self.sort_column == column:
|
||||
self.sort_descending = not self.sort_descending
|
||||
|
||||
Reference in New Issue
Block a user