mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
Merge pull request 'Feature/sepa export selection' (#21) from feature/sepa-export-selection into dev
Reviewed-on: https://git.hiabuto.net/C3MA/CCMA/pulls/21 Reviewed-by: Matcha <20+matcha@noreply.git.hiabuto.net>
This commit is contained in:
@@ -35,7 +35,8 @@
|
||||
"Mahnungsentwürfe können nachträglich bearbeitet werden; auch eine bereits als versandt markierte Mahnung lässt sich dafür sicher zurücksetzen (die gebuchte Mahngebühr wird wieder entfernt), sofern noch keine höhere Mahnstufe existiert oder Zahlungen bereits darauf verbucht wurden.",
|
||||
"Eine überfällige Forderung, für die bereits eine Mahnung versandt wurde und deren Zahlungsfrist noch läuft, wird vom Hausmeister nicht mehr fälschlich als unbearbeiteter Fehler gemeldet, sondern als niedrig priorisierter Hinweis, dass die laufende Frist noch nicht abgelaufen ist.",
|
||||
"Geburtstags- und Mitgliedsjubiläums-Hinweise des Hausmeisters werden für Mitglieder mit beendeter Mitgliedschaft nicht mehr angezeigt.",
|
||||
"Eine überfällige Lastschrift, für die bereits eine Rücklastschrift-Mahnung versandt wurde und deren Zahlungsfrist noch läuft, wird vom Hausmeister nicht mehr weiterhin als überfällig gemeldet, sondern als niedrig priorisierter Hinweis, dass die Klärung noch läuft."
|
||||
"Eine überfällige Lastschrift, für die bereits eine Rücklastschrift-Mahnung versandt wurde und deren Zahlungsfrist noch läuft, wird vom Hausmeister nicht mehr weiterhin als überfällig gemeldet, sondern als niedrig priorisierter Hinweis, dass die Klärung noch läuft.",
|
||||
"Im SEPA-Export lassen sich einzelne Lastschriften per Mehrfachauswahl von der CSV-/XML-/Info-Mail-Erstellung ausschließen (z. B. während eine Rücklastschrift geklärt wird), statt immer alle offenen Lastschriften einziehen zu müssen; standardmäßig sind alle ausgewählt."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+60
-12
@@ -40,7 +40,7 @@ class SepaExportDialog(tk.Toplevel):
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(2, weight=1)
|
||||
self.rowconfigure(3, weight=1)
|
||||
filters = ttk.Frame(self, padding=12)
|
||||
filters.grid(row=0, column=0, sticky="ew")
|
||||
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.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, 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 (
|
||||
("number", "Mitglied", 100, "w"), ("name", "Kontoinhaber", 170, "w"),
|
||||
@@ -69,9 +82,10 @@ class SepaExportDialog(tk.Toplevel):
|
||||
):
|
||||
self.table.heading(key, text=label)
|
||||
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.grid(row=3, column=0, sticky="ew")
|
||||
footer.grid(row=4, column=0, sticky="ew")
|
||||
footer.columnconfigure(0, weight=1)
|
||||
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(
|
||||
@@ -107,6 +121,14 @@ class SepaExportDialog(tk.Toplevel):
|
||||
return due_from, due, collection
|
||||
|
||||
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:
|
||||
due_from, due, _collection = self._dates()
|
||||
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)
|
||||
return
|
||||
self.table.delete(*self.table.get_children())
|
||||
for debit in self.debits:
|
||||
for index, debit in enumerate(self.debits):
|
||||
self.table.insert(
|
||||
"", "end",
|
||||
"", "end", iid=str(index),
|
||||
values=(debit.member_number, debit.account_holder, debit.iban,
|
||||
debit.mandate_reference, debit.purpose, f"{debit.amount:.2f} EUR"),
|
||||
)
|
||||
total = sum((item.amount for item in self.debits), Decimal("0"))
|
||||
self.summary_var.set(f"{len(self.debits)} Lastschrift(en) · Summe {total:.2f} EUR")
|
||||
self.table.selection_set(
|
||||
[
|
||||
str(index)
|
||||
for index, debit in enumerate(self.debits)
|
||||
if debit.member_id not in previously_deselected
|
||||
]
|
||||
)
|
||||
self._update_summary()
|
||||
self.warning_var.set(
|
||||
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:
|
||||
self.refresh()
|
||||
if not self.debits:
|
||||
@@ -135,6 +178,11 @@ class SepaExportDialog(tk.Toplevel):
|
||||
"Keine Lastschriften", "Für den Stichtag ist nichts einzuziehen.", parent=self
|
||||
)
|
||||
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(
|
||||
"Unvollständige Mandate",
|
||||
"\n".join(self.warnings[:10]) + "\n\nMit den übrigen Lastschriften fortfahren?",
|
||||
@@ -157,7 +205,7 @@ class SepaExportDialog(tk.Toplevel):
|
||||
return
|
||||
target = self._target(".csv", "CSV-Datei", dates[2])
|
||||
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)
|
||||
|
||||
def _save_xml(self) -> None:
|
||||
@@ -169,7 +217,7 @@ class SepaExportDialog(tk.Toplevel):
|
||||
return
|
||||
try:
|
||||
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 {},
|
||||
)
|
||||
except RepositoryError as exc:
|
||||
@@ -180,7 +228,7 @@ class SepaExportDialog(tk.Toplevel):
|
||||
def _open_mail_export(self) -> None:
|
||||
dates = self._prepare()
|
||||
if dates:
|
||||
SepaMailExportDialog(self, self.repository, self.debits, dates[2])
|
||||
SepaMailExportDialog(self, self.repository, self._selected_debits(), dates[2])
|
||||
|
||||
|
||||
class SepaMailExportDialog(tk.Toplevel):
|
||||
|
||||
Reference in New Issue
Block a user