mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 14:35:19 +02:00
Let individual SEPA debits be excluded from an export, not just all-or-nothing
The SEPA export dialog always exported every pending direct debit in the filtered date range with no way to hold one back -- e.g. a member whose Rücklastschrift is still being clarified had to either be collected again regardless or the whole run skipped. The table now supports multi-select (everything selected by default) and the CSV/XML/mail exports only include the currently selected rows. Deselection survives the refresh() that _prepare() runs right before exporting (tracked by member_id, not row index, so a debit newly appearing after a requery still defaults to selected). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d87a57775e
commit
bfb65b2583
+60
-12
@@ -40,7 +40,7 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(2, weight=1)
|
self.rowconfigure(3, weight=1)
|
||||||
filters = ttk.Frame(self, padding=12)
|
filters = ttk.Frame(self, padding=12)
|
||||||
filters.grid(row=0, column=0, sticky="ew")
|
filters.grid(row=0, column=0, sticky="ew")
|
||||||
ttk.Label(filters, text="Fällig ab").grid(row=0, column=0)
|
ttk.Label(filters, text="Fällig ab").grid(row=0, column=0)
|
||||||
@@ -57,10 +57,23 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
)
|
)
|
||||||
ttk.Button(filters, text="Liste aktualisieren", command=self.refresh).grid(row=0, column=6)
|
ttk.Button(filters, text="Liste aktualisieren", command=self.refresh).grid(row=0, column=6)
|
||||||
ttk.Label(self, textvariable=self.summary_var, style="Mono.TLabel").grid(
|
ttk.Label(self, textvariable=self.summary_var, style="Mono.TLabel").grid(
|
||||||
row=1, column=0, sticky="w", padx=12, pady=(0, 8)
|
row=1, column=0, sticky="w", padx=12, pady=(0, 2)
|
||||||
)
|
)
|
||||||
|
ttk.Label(
|
||||||
|
self,
|
||||||
|
text=(
|
||||||
|
"Standardmäßig sind alle Lastschriften ausgewählt -- einzelne mit Strg+Klick "
|
||||||
|
"abwählen (z. B. während eine Rücklastschrift geklärt wird), um sie erst bei "
|
||||||
|
"einem späteren Einzug zu berücksichtigen."
|
||||||
|
),
|
||||||
|
style="Muted.TLabel",
|
||||||
|
wraplength=960,
|
||||||
|
).grid(row=2, column=0, sticky="w", padx=12, pady=(0, 8))
|
||||||
self.table = ttk.Treeview(
|
self.table = ttk.Treeview(
|
||||||
self, columns=("number", "name", "iban", "mandate", "purpose", "amount"), show="headings"
|
self,
|
||||||
|
columns=("number", "name", "iban", "mandate", "purpose", "amount"),
|
||||||
|
show="headings",
|
||||||
|
selectmode="extended",
|
||||||
)
|
)
|
||||||
for key, label, width, anchor in (
|
for key, label, width, anchor in (
|
||||||
("number", "Mitglied", 100, "w"), ("name", "Kontoinhaber", 170, "w"),
|
("number", "Mitglied", 100, "w"), ("name", "Kontoinhaber", 170, "w"),
|
||||||
@@ -69,9 +82,10 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
):
|
):
|
||||||
self.table.heading(key, text=label)
|
self.table.heading(key, text=label)
|
||||||
self.table.column(key, width=width, anchor=anchor)
|
self.table.column(key, width=width, anchor=anchor)
|
||||||
self.table.grid(row=2, column=0, sticky="nsew", padx=12)
|
self.table.grid(row=3, column=0, sticky="nsew", padx=12)
|
||||||
|
self.table.bind("<<TreeviewSelect>>", lambda _event: self._update_summary())
|
||||||
footer = ttk.Frame(self, padding=12)
|
footer = ttk.Frame(self, padding=12)
|
||||||
footer.grid(row=3, column=0, sticky="ew")
|
footer.grid(row=4, column=0, sticky="ew")
|
||||||
footer.columnconfigure(0, weight=1)
|
footer.columnconfigure(0, weight=1)
|
||||||
ttk.Label(footer, textvariable=self.warning_var).grid(row=0, column=0, sticky="w")
|
ttk.Label(footer, textvariable=self.warning_var).grid(row=0, column=0, sticky="w")
|
||||||
ttk.Button(footer, text="CSV-Liste speichern", command=self._save_csv).grid(
|
ttk.Button(footer, text="CSV-Liste speichern", command=self._save_csv).grid(
|
||||||
@@ -107,6 +121,14 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
return due_from, due, collection
|
return due_from, due, collection
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
|
# Preserve which members were deliberately deselected across a refresh (e.g.
|
||||||
|
# _prepare() re-querying right before an export) by identity, not by row index
|
||||||
|
# -- a debit newly appearing in the requeried list still defaults to selected.
|
||||||
|
previously_deselected = {
|
||||||
|
self.debits[int(iid)].member_id
|
||||||
|
for iid in self.table.get_children()
|
||||||
|
if iid not in self.table.selection()
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
due_from, due, _collection = self._dates()
|
due_from, due, _collection = self._dates()
|
||||||
self.debits, self.warnings = pending_direct_debits(
|
self.debits, self.warnings = pending_direct_debits(
|
||||||
@@ -116,18 +138,39 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
messagebox.showerror("SEPA-Liste konnte nicht erstellt werden", str(exc), parent=self)
|
messagebox.showerror("SEPA-Liste konnte nicht erstellt werden", str(exc), parent=self)
|
||||||
return
|
return
|
||||||
self.table.delete(*self.table.get_children())
|
self.table.delete(*self.table.get_children())
|
||||||
for debit in self.debits:
|
for index, debit in enumerate(self.debits):
|
||||||
self.table.insert(
|
self.table.insert(
|
||||||
"", "end",
|
"", "end", iid=str(index),
|
||||||
values=(debit.member_number, debit.account_holder, debit.iban,
|
values=(debit.member_number, debit.account_holder, debit.iban,
|
||||||
debit.mandate_reference, debit.purpose, f"{debit.amount:.2f} EUR"),
|
debit.mandate_reference, debit.purpose, f"{debit.amount:.2f} EUR"),
|
||||||
)
|
)
|
||||||
total = sum((item.amount for item in self.debits), Decimal("0"))
|
self.table.selection_set(
|
||||||
self.summary_var.set(f"{len(self.debits)} Lastschrift(en) · Summe {total:.2f} EUR")
|
[
|
||||||
|
str(index)
|
||||||
|
for index, debit in enumerate(self.debits)
|
||||||
|
if debit.member_id not in previously_deselected
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self._update_summary()
|
||||||
self.warning_var.set(
|
self.warning_var.set(
|
||||||
f"{len(self.warnings)} unvollständige(s) Mandat(e) übersprungen." if self.warnings else ""
|
f"{len(self.warnings)} unvollständige(s) Mandat(e) übersprungen." if self.warnings else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _selected_debits(self) -> list:
|
||||||
|
return [self.debits[int(iid)] for iid in sorted(self.table.selection(), key=int)]
|
||||||
|
|
||||||
|
def _update_summary(self) -> None:
|
||||||
|
selected = self._selected_debits()
|
||||||
|
total = sum((item.amount for item in self.debits), Decimal("0"))
|
||||||
|
if len(selected) == len(self.debits):
|
||||||
|
self.summary_var.set(f"{len(self.debits)} Lastschrift(en) · Summe {total:.2f} EUR")
|
||||||
|
return
|
||||||
|
selected_total = sum((item.amount for item in selected), Decimal("0"))
|
||||||
|
self.summary_var.set(
|
||||||
|
f"{len(selected)} von {len(self.debits)} Lastschrift(en) ausgewählt · "
|
||||||
|
f"Summe {selected_total:.2f} EUR (gesamt {total:.2f} EUR)"
|
||||||
|
)
|
||||||
|
|
||||||
def _prepare(self) -> tuple[date, date, date] | None:
|
def _prepare(self) -> tuple[date, date, date] | None:
|
||||||
self.refresh()
|
self.refresh()
|
||||||
if not self.debits:
|
if not self.debits:
|
||||||
@@ -135,6 +178,11 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
"Keine Lastschriften", "Für den Stichtag ist nichts einzuziehen.", parent=self
|
"Keine Lastschriften", "Für den Stichtag ist nichts einzuziehen.", parent=self
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
if not self._selected_debits():
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Keine Auswahl", "Bitte mindestens eine Lastschrift auswählen.", parent=self
|
||||||
|
)
|
||||||
|
return None
|
||||||
if self.warnings and not messagebox.askyesno(
|
if self.warnings and not messagebox.askyesno(
|
||||||
"Unvollständige Mandate",
|
"Unvollständige Mandate",
|
||||||
"\n".join(self.warnings[:10]) + "\n\nMit den übrigen Lastschriften fortfahren?",
|
"\n".join(self.warnings[:10]) + "\n\nMit den übrigen Lastschriften fortfahren?",
|
||||||
@@ -157,7 +205,7 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
return
|
return
|
||||||
target = self._target(".csv", "CSV-Datei", dates[2])
|
target = self._target(".csv", "CSV-Datei", dates[2])
|
||||||
if target:
|
if target:
|
||||||
write_csv(target, self.debits, collection_date=dates[2])
|
write_csv(target, self._selected_debits(), collection_date=dates[2])
|
||||||
messagebox.showinfo("Export gespeichert", f"CSV gespeichert:\n{target}", parent=self)
|
messagebox.showinfo("Export gespeichert", f"CSV gespeichert:\n{target}", parent=self)
|
||||||
|
|
||||||
def _save_xml(self) -> None:
|
def _save_xml(self) -> None:
|
||||||
@@ -169,7 +217,7 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
write_pain008(
|
write_pain008(
|
||||||
target, self.debits, collection_date=dates[2],
|
target, self._selected_debits(), collection_date=dates[2],
|
||||||
organization=self.repository.get_configuration().get("organization") or {},
|
organization=self.repository.get_configuration().get("organization") or {},
|
||||||
)
|
)
|
||||||
except RepositoryError as exc:
|
except RepositoryError as exc:
|
||||||
@@ -180,7 +228,7 @@ class SepaExportDialog(tk.Toplevel):
|
|||||||
def _open_mail_export(self) -> None:
|
def _open_mail_export(self) -> None:
|
||||||
dates = self._prepare()
|
dates = self._prepare()
|
||||||
if dates:
|
if dates:
|
||||||
SepaMailExportDialog(self, self.repository, self.debits, dates[2])
|
SepaMailExportDialog(self, self.repository, self._selected_debits(), dates[2])
|
||||||
|
|
||||||
|
|
||||||
class SepaMailExportDialog(tk.Toplevel):
|
class SepaMailExportDialog(tk.Toplevel):
|
||||||
|
|||||||
Reference in New Issue
Block a user