mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
Merge branch 'dev' into feature/read-only-store
dev gained the editable mail templates, which live in the store like everything else -- so they follow the same read-only rules: the options dialog skips saving them, save_mail_template() reports the store instead of a raw PermissionError, and a store that cannot keep its own copy simply renders from the shipped default. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ from ccma.ui.labels import display_label, storage_key
|
||||
from ccma.ui.messages import MessageAction, MessageBannerList, TabMessage
|
||||
from ccma.ui.payment_dialog import PaymentCreateDialog, PaymentEditDialog
|
||||
from ccma.ui.scrolling import ScrollableFrame
|
||||
from ccma.ui.welcome_mail_dialog import WelcomeMailDialog
|
||||
|
||||
# Shared column sizing for the ledger-style tables (Forderungen/Zahlungen/Spenden): date-
|
||||
# and amount-like columns get the same fixed width, status columns get a third more
|
||||
@@ -355,6 +356,13 @@ class MemberTab(ttk.Frame):
|
||||
self.claims.grid(row=1, column=0, sticky="nsew")
|
||||
self.claims.bind("<Double-1>", lambda _event: self._open_selected_claim())
|
||||
self.claims.bind("<Return>", lambda _event: self._open_selected_claim())
|
||||
claim_actions = ttk.Frame(contribution_tab)
|
||||
claim_actions.grid(row=2, column=0, sticky="e", pady=(8, 0))
|
||||
ttk.Button(
|
||||
claim_actions,
|
||||
text="Willkommens- und Erstrechnungsmail",
|
||||
command=self._send_welcome_mail,
|
||||
).pack(side="left")
|
||||
|
||||
contribution_override_tab.columnconfigure(0, weight=1)
|
||||
contribution_override_tab.rowconfigure(1, weight=1)
|
||||
@@ -1185,6 +1193,14 @@ class MemberTab(ttk.Frame):
|
||||
except OSError as exc:
|
||||
messagebox.showerror("Datei konnte nicht geöffnet werden", str(exc), parent=self)
|
||||
|
||||
def _send_welcome_mail(self) -> None:
|
||||
WelcomeMailDialog(self, self.repository, self.member_id, self._welcome_mail_sent)
|
||||
|
||||
def _welcome_mail_sent(self) -> None:
|
||||
self._refresh_documents()
|
||||
self._refresh_events()
|
||||
self.on_changed()
|
||||
|
||||
def _create_document(self) -> None:
|
||||
DocumentTemplateDialog(
|
||||
self,
|
||||
|
||||
@@ -7,6 +7,14 @@ from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from ccma.config import AppConfig
|
||||
from ccma.domain.contributions import decimal_value, money_text
|
||||
from ccma.domain.mail_templates import (
|
||||
MAIL_TEMPLATES,
|
||||
MailTemplateError,
|
||||
default_mail_template,
|
||||
placeholder_entries,
|
||||
template_spec,
|
||||
validate_mail_template,
|
||||
)
|
||||
from ccma.domain.models import HOUSEKEEPER_MEMBER_FIELD_LABELS
|
||||
from ccma.services.intervals import (
|
||||
IntervalValidationError,
|
||||
@@ -67,6 +75,15 @@ class OptionsDialog(tk.Toplevel):
|
||||
"website", "iban", "bic", "creditor_id",
|
||||
)
|
||||
}
|
||||
# Mail templates are edited one at a time but saved together with the rest of
|
||||
# the options, so the edits of every tab are kept in memory until "Speichern".
|
||||
self.mail_template_key = MAIL_TEMPLATES[0].key
|
||||
self.mail_template_edits: dict[str, tuple[str, str]] = {}
|
||||
self.mail_template_original: dict[str, tuple[str, str]] = {}
|
||||
self.mail_template_var = tk.StringVar(value=MAIL_TEMPLATES[0].label)
|
||||
self.mail_template_subject_var = tk.StringVar()
|
||||
self.mail_template_hint_var = tk.StringVar()
|
||||
self.mail_template_snippets: dict[str, str] = {}
|
||||
self.title("Optionen")
|
||||
self.transient(master.winfo_toplevel())
|
||||
self.grab_set()
|
||||
@@ -102,6 +119,7 @@ class OptionsDialog(tk.Toplevel):
|
||||
automation = ttk.Frame(notebook, padding=16)
|
||||
reminders = ttk.Frame(notebook, padding=16)
|
||||
email = ttk.Frame(notebook, padding=16)
|
||||
mail_templates = ttk.Frame(notebook, padding=16)
|
||||
changelog = ChangelogView(notebook)
|
||||
notebook.add(paths, text="Pfade")
|
||||
notebook.add(appearance, text="Darstellung")
|
||||
@@ -110,6 +128,7 @@ class OptionsDialog(tk.Toplevel):
|
||||
notebook.add(automation, text="Hausmeister")
|
||||
notebook.add(reminders, text="Mahnungen")
|
||||
notebook.add(email, text="E-Mail-Versand")
|
||||
notebook.add(mail_templates, text="E-Mail-Vorlagen")
|
||||
notebook.add(changelog, text="Changelog")
|
||||
self._build_paths(paths)
|
||||
self._build_appearance(appearance)
|
||||
@@ -118,6 +137,7 @@ class OptionsDialog(tk.Toplevel):
|
||||
self._build_automation(automation)
|
||||
self._build_reminders(reminders)
|
||||
self._build_email(email)
|
||||
self._build_mail_templates(mail_templates)
|
||||
|
||||
buttons = ttk.Frame(root)
|
||||
buttons.grid(row=1, column=0, sticky="e", pady=(12, 0))
|
||||
@@ -912,6 +932,7 @@ class OptionsDialog(tk.Toplevel):
|
||||
standard_fee_items=self.standard_items,
|
||||
)
|
||||
self.repository.save_email_settings(**email_settings)
|
||||
self._save_mail_templates()
|
||||
except (OSError, RepositoryError) as exc:
|
||||
messagebox.showerror("Optionen konnten nicht gespeichert werden", str(exc), parent=self)
|
||||
return
|
||||
@@ -919,14 +940,191 @@ class OptionsDialog(tk.Toplevel):
|
||||
messagebox.showwarning(
|
||||
"Store ist schreibgeschützt",
|
||||
"Die Programmeinstellungen wurden gespeichert. Vereinsdaten, "
|
||||
"Mitgliedsnummern, Mahnungen und E-Mail-Versand konnten nicht gespeichert "
|
||||
"werden, weil der Mitglieder-Store nur lesend eingebunden ist.",
|
||||
"Mitgliedsnummern, Mahnungen, E-Mail-Versand und E-Mail-Vorlagen konnten "
|
||||
"nicht gespeichert werden, weil der Mitglieder-Store nur lesend "
|
||||
"eingebunden ist.",
|
||||
parent=self,
|
||||
)
|
||||
if self.on_saved:
|
||||
self.on_saved(store_changed)
|
||||
self.destroy()
|
||||
|
||||
def _build_mail_templates(self, parent: ttk.Frame) -> None:
|
||||
parent.columnconfigure(0, weight=1)
|
||||
parent.rowconfigure(4, weight=1)
|
||||
ttk.Label(
|
||||
parent,
|
||||
text=(
|
||||
"Die Texte aller vom Programm erzeugten E-Mails liegen als bearbeitbare "
|
||||
"Vorlagen im Mitglieder-Store unter templates/mail/. Platzhalter in doppelten "
|
||||
"geschweiften Klammern werden beim Versand mit den Daten des Mitglieds gefüllt."
|
||||
),
|
||||
style="Muted.TLabel",
|
||||
wraplength=820,
|
||||
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 12))
|
||||
|
||||
chooser = ttk.Frame(parent)
|
||||
chooser.grid(row=1, column=0, columnspan=2, sticky="ew")
|
||||
chooser.columnconfigure(1, weight=1)
|
||||
ttk.Label(chooser, text="Vorlage").grid(row=0, column=0, sticky="w", padx=(0, 12))
|
||||
selector = ttk.Combobox(
|
||||
chooser,
|
||||
textvariable=self.mail_template_var,
|
||||
values=[spec.label for spec in MAIL_TEMPLATES],
|
||||
state="readonly",
|
||||
width=32,
|
||||
)
|
||||
selector.grid(row=0, column=1, sticky="w")
|
||||
selector.bind("<<ComboboxSelected>>", lambda _event: self._select_mail_template())
|
||||
ttk.Button(
|
||||
chooser, text="Standardtext wiederherstellen", command=self._reset_mail_template
|
||||
).grid(row=0, column=2, sticky="e", padx=(12, 0))
|
||||
ttk.Label(
|
||||
parent, textvariable=self.mail_template_hint_var, style="Muted.TLabel", wraplength=820
|
||||
).grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 12))
|
||||
|
||||
subject = ttk.Frame(parent)
|
||||
subject.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(0, 8))
|
||||
subject.columnconfigure(1, weight=1)
|
||||
ttk.Label(subject, text="Betreff").grid(row=0, column=0, sticky="w", padx=(0, 12))
|
||||
ttk.Entry(subject, textvariable=self.mail_template_subject_var).grid(
|
||||
row=0, column=1, sticky="ew"
|
||||
)
|
||||
|
||||
editor = ttk.Frame(parent)
|
||||
editor.grid(row=4, column=0, sticky="nsew")
|
||||
editor.columnconfigure(0, weight=1)
|
||||
editor.rowconfigure(0, weight=1)
|
||||
self.mail_template_body = tk.Text(editor, wrap="word", height=16, width=70, undo=True)
|
||||
self.mail_template_body.grid(row=0, column=0, sticky="nsew")
|
||||
body_scroll = ttk.Scrollbar(editor, orient="vertical", command=self.mail_template_body.yview)
|
||||
body_scroll.grid(row=0, column=1, sticky="ns")
|
||||
self.mail_template_body.configure(yscrollcommand=body_scroll.set)
|
||||
|
||||
placeholders = ttk.Frame(parent)
|
||||
placeholders.grid(row=4, column=1, sticky="nsew", padx=(12, 0))
|
||||
placeholders.columnconfigure(0, weight=1)
|
||||
placeholders.rowconfigure(1, weight=1)
|
||||
ttk.Label(placeholders, text="Platzhalter (Doppelklick fügt ein)").grid(
|
||||
row=0, column=0, columnspan=2, sticky="w", pady=(0, 4)
|
||||
)
|
||||
self.mail_template_placeholders = ttk.Treeview(
|
||||
placeholders, columns=("name", "help"), show="headings", selectmode="browse", height=14
|
||||
)
|
||||
for key, title, width in (("name", "Platzhalter", 210), ("help", "Bedeutung", 230)):
|
||||
self.mail_template_placeholders.heading(key, text=title)
|
||||
self.mail_template_placeholders.column(key, width=width, anchor="w", stretch=key == "help")
|
||||
self.mail_template_placeholders.grid(row=1, column=0, sticky="nsew")
|
||||
placeholder_scroll = ttk.Scrollbar(
|
||||
placeholders, orient="vertical", command=self.mail_template_placeholders.yview
|
||||
)
|
||||
placeholder_scroll.grid(row=1, column=1, sticky="ns")
|
||||
self.mail_template_placeholders.configure(yscrollcommand=placeholder_scroll.set)
|
||||
self.mail_template_placeholders.bind(
|
||||
"<Double-1>", lambda _event: self._insert_mail_placeholder()
|
||||
)
|
||||
self.mail_template_placeholders.bind(
|
||||
"<Return>", lambda _event: self._insert_mail_placeholder()
|
||||
)
|
||||
self._load_mail_template(self.mail_template_key)
|
||||
|
||||
def _load_mail_template(self, key: str) -> None:
|
||||
if key not in self.mail_template_edits:
|
||||
try:
|
||||
template = self.repository.get_mail_template(key)
|
||||
except (OSError, RepositoryError) as exc:
|
||||
messagebox.showerror(
|
||||
"Vorlage konnte nicht geladen werden", str(exc), parent=self
|
||||
)
|
||||
return
|
||||
self.mail_template_edits[key] = (template.subject, template.body)
|
||||
self.mail_template_original[key] = (template.subject, template.body)
|
||||
subject, body = self.mail_template_edits[key]
|
||||
self.mail_template_key = key
|
||||
spec = template_spec(key)
|
||||
self.mail_template_var.set(spec.label)
|
||||
self.mail_template_hint_var.set(f"{spec.description} (Datei: {spec.filename})")
|
||||
self.mail_template_subject_var.set(subject)
|
||||
self.mail_template_body.delete("1.0", "end")
|
||||
self.mail_template_body.insert("1.0", body)
|
||||
self.mail_template_placeholders.delete(*self.mail_template_placeholders.get_children())
|
||||
# The row text is a readable label; what actually gets inserted is the entry's
|
||||
# snippet, which for a repeat block is the whole block and not its description.
|
||||
self.mail_template_snippets = {}
|
||||
for entry in placeholder_entries(key):
|
||||
label = f" {entry.label}" if entry.indented else entry.label
|
||||
row = self.mail_template_placeholders.insert(
|
||||
"", "end", values=(label, entry.description)
|
||||
)
|
||||
self.mail_template_snippets[row] = entry.snippet
|
||||
|
||||
def _capture_mail_template(self) -> None:
|
||||
if not hasattr(self, "mail_template_body"):
|
||||
return
|
||||
self.mail_template_edits[self.mail_template_key] = (
|
||||
self.mail_template_subject_var.get(),
|
||||
self.mail_template_body.get("1.0", "end-1c"),
|
||||
)
|
||||
|
||||
def _select_mail_template(self) -> None:
|
||||
self._capture_mail_template()
|
||||
label = self.mail_template_var.get()
|
||||
key = next((spec.key for spec in MAIL_TEMPLATES if spec.label == label), None)
|
||||
if key:
|
||||
self._load_mail_template(key)
|
||||
|
||||
def _reset_mail_template(self) -> None:
|
||||
key = self.mail_template_key
|
||||
spec = template_spec(key)
|
||||
if not messagebox.askyesno(
|
||||
"Standardtext wiederherstellen",
|
||||
f"Soll die Vorlage „{spec.label}“ auf den mitgelieferten Standardtext "
|
||||
"zurückgesetzt werden? Deine Änderungen an dieser Vorlage gehen dabei verloren.",
|
||||
parent=self,
|
||||
):
|
||||
return
|
||||
try:
|
||||
template = default_mail_template(key)
|
||||
except MailTemplateError as exc:
|
||||
messagebox.showerror("Standardtext nicht verfügbar", str(exc), parent=self)
|
||||
return
|
||||
self.mail_template_edits[key] = (template.subject, template.body)
|
||||
self._load_mail_template(key)
|
||||
|
||||
def _insert_mail_placeholder(self) -> None:
|
||||
selected = self.mail_template_placeholders.selection()
|
||||
if not selected:
|
||||
return
|
||||
snippet = self.mail_template_snippets.get(selected[0])
|
||||
if not snippet:
|
||||
return
|
||||
# A multi-line block only works on lines of its own, so it starts on a fresh
|
||||
# one instead of being appended to whatever the cursor sat behind.
|
||||
if "\n" in snippet.strip() and not self._mail_cursor_at_line_start():
|
||||
snippet = "\n" + snippet
|
||||
self.mail_template_body.insert("insert", snippet)
|
||||
self.mail_template_body.focus_set()
|
||||
|
||||
def _mail_cursor_at_line_start(self) -> bool:
|
||||
return str(self.mail_template_body.index("insert")).split(".")[1] == "0"
|
||||
|
||||
def _save_mail_templates(self) -> None:
|
||||
self._capture_mail_template()
|
||||
pending = {
|
||||
key: value
|
||||
for key, value in self.mail_template_edits.items()
|
||||
if self.mail_template_original.get(key) != value
|
||||
}
|
||||
# Check every edited template before writing the first one: a mistake in one
|
||||
# must not leave the others half-saved.
|
||||
for key, (subject, body) in pending.items():
|
||||
try:
|
||||
validate_mail_template(key, subject, body)
|
||||
except MailTemplateError as exc:
|
||||
raise RepositoryError(f"{template_spec(key).label}: {exc}") from exc
|
||||
for key, (subject, body) in pending.items():
|
||||
self.repository.save_mail_template(key, subject=subject, body=body)
|
||||
|
||||
def _center_on_parent(self) -> None:
|
||||
self.update_idletasks()
|
||||
parent = self.master.winfo_toplevel()
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
from email.parser import BytesParser
|
||||
from email.policy import default as default_policy
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from ccma.domain.contributions import claim_balance, money_text
|
||||
from ccma.domain.dates import format_date_for_display
|
||||
from ccma.services.welcome_mail import (
|
||||
generate_and_send_welcome_mail,
|
||||
open_claims,
|
||||
welcome_mail_bytes,
|
||||
)
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
from ccma.ui.email_delivery_dialog import resolve_delivery_mode
|
||||
|
||||
|
||||
class WelcomeMailDialog(tk.Toplevel):
|
||||
"""Sends the welcome mail that doubles as the first invoice for a new member:
|
||||
membership start, member number and every claim that is still open."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master: tk.Misc,
|
||||
repository: MemberRepository,
|
||||
member_id: str,
|
||||
on_sent: Callable[[], None],
|
||||
):
|
||||
super().__init__(master)
|
||||
self.repository = repository
|
||||
self.member_id = member_id
|
||||
self.on_sent = on_sent
|
||||
self.claims: list[dict] = []
|
||||
organization = repository.get_configuration().get("organization") or {}
|
||||
self.sender_name_var = tk.StringVar(value=str(organization.get("name") or "Verwaltung C3MA"))
|
||||
self.sender_email_var = tk.StringVar(value=str(organization.get("email") or ""))
|
||||
self.summary_var = tk.StringVar()
|
||||
self.title("Willkommens- und Erstrechnungsmail")
|
||||
self.transient(master.winfo_toplevel())
|
||||
self.resizable(True, True)
|
||||
self._build_ui()
|
||||
self._refresh_claims()
|
||||
self.grab_set()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(2, weight=1)
|
||||
ttk.Label(
|
||||
self,
|
||||
text=(
|
||||
"Begrüßt das neue Mitglied und stellt die noch offenen Forderungen "
|
||||
"(Aufnahmegebühr und erster Beitrag) als Erstrechnung zusammen. Text und "
|
||||
"Betreff stammen aus der Vorlage unter Optionen → E-Mail-Vorlagen."
|
||||
),
|
||||
wraplength=640,
|
||||
).grid(row=0, column=0, sticky="w", padx=14, pady=(14, 8))
|
||||
ttk.Label(
|
||||
self,
|
||||
text=(
|
||||
"Standardmäßig sind alle offenen Forderungen ausgewählt – einzelne mit "
|
||||
"Strg+Klick abwählen."
|
||||
),
|
||||
style="Muted.TLabel",
|
||||
wraplength=640,
|
||||
).grid(row=1, column=0, sticky="w", padx=14, pady=(0, 8))
|
||||
table = ttk.Frame(self)
|
||||
table.grid(row=2, column=0, sticky="nsew", padx=14)
|
||||
table.columnconfigure(0, weight=1)
|
||||
table.rowconfigure(0, weight=1)
|
||||
self.table = ttk.Treeview(
|
||||
table,
|
||||
columns=("due", "amount", "title"),
|
||||
show="headings",
|
||||
selectmode="extended",
|
||||
height=6,
|
||||
)
|
||||
for key, title, width in (
|
||||
("due", "Fällig", 90),
|
||||
("amount", "Offen", 90),
|
||||
("title", "Forderung", 320),
|
||||
):
|
||||
self.table.heading(key, text=title)
|
||||
self.table.column(key, width=width, anchor="w", stretch=key == "title")
|
||||
self.table.grid(row=0, column=0, sticky="nsew")
|
||||
scroll = ttk.Scrollbar(table, orient="vertical", command=self.table.yview)
|
||||
scroll.grid(row=0, column=1, sticky="ns")
|
||||
self.table.configure(yscrollcommand=scroll.set)
|
||||
self.table.bind("<<TreeviewSelect>>", lambda _event: self._update_summary())
|
||||
ttk.Label(self, textvariable=self.summary_var, style="Mono.TLabel").grid(
|
||||
row=3, column=0, sticky="w", padx=14, pady=(6, 10)
|
||||
)
|
||||
|
||||
fields = ttk.Frame(self)
|
||||
fields.grid(row=4, column=0, sticky="ew", padx=14)
|
||||
fields.columnconfigure(1, weight=1)
|
||||
ttk.Label(fields, text="Absendername").grid(row=0, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Entry(fields, textvariable=self.sender_name_var, width=52).grid(row=0, column=1, sticky="ew")
|
||||
ttk.Label(fields, text="Absenderadresse").grid(row=1, column=0, sticky="w", pady=5, padx=(0, 12))
|
||||
ttk.Entry(fields, textvariable=self.sender_email_var).grid(row=1, column=1, sticky="ew")
|
||||
ttk.Label(fields, text="Signatur").grid(row=2, column=0, sticky="nw", pady=5, padx=(0, 12))
|
||||
self.signature = tk.Text(fields, width=52, height=4, wrap="word")
|
||||
self.signature.grid(row=2, column=1, sticky="ew", pady=5)
|
||||
self.signature.insert("1.0", "Der Vorstand\nChaos Computer Club Mannheim e.V.")
|
||||
|
||||
buttons = ttk.Frame(self, padding=14)
|
||||
buttons.grid(row=5, column=0, sticky="e")
|
||||
ttk.Button(buttons, text="Abbrechen", command=self.destroy).pack(side="left")
|
||||
ttk.Button(buttons, text="Vorschau", command=self._preview).pack(side="left", padx=(8, 0))
|
||||
ttk.Button(
|
||||
buttons, text="E-Mail erzeugen", style="Accent.TButton", command=self._generate
|
||||
).pack(side="left", padx=(8, 0))
|
||||
|
||||
def _refresh_claims(self) -> None:
|
||||
try:
|
||||
self.claims = open_claims(self.repository, self.member_id)
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
except RepositoryError as exc:
|
||||
messagebox.showerror("Forderungen konnten nicht gelesen werden", str(exc), parent=self)
|
||||
return
|
||||
self.table.delete(*self.table.get_children())
|
||||
for index, claim in enumerate(self.claims):
|
||||
self.table.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=str(index),
|
||||
values=(
|
||||
format_date_for_display(str(claim.get("due_date", ""))),
|
||||
money_text(claim_balance(data, claim)),
|
||||
str(claim.get("description", "") or claim.get("title", "")),
|
||||
),
|
||||
)
|
||||
self.table.selection_set(self.table.get_children())
|
||||
self._update_summary()
|
||||
|
||||
def _selected_claims(self) -> list[dict]:
|
||||
return [self.claims[int(iid)] for iid in self.table.selection()]
|
||||
|
||||
def _update_summary(self) -> None:
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
selected = self._selected_claims()
|
||||
total = sum((claim_balance(data, claim) for claim in selected), Decimal("0"))
|
||||
self.summary_var.set(
|
||||
f"{len(selected)} von {len(self.claims)} Forderungen ausgewählt – "
|
||||
f"Gesamtbetrag {money_text(total)} Euro"
|
||||
)
|
||||
|
||||
def _mail_bytes(self) -> bytes:
|
||||
member = self.repository.get_member(self.member_id)
|
||||
data = self.repository.get_contributions(self.member_id)
|
||||
claims = self._selected_claims()
|
||||
return welcome_mail_bytes(
|
||||
self.repository,
|
||||
member=member,
|
||||
claims=claims,
|
||||
balances={
|
||||
str(claim.get("claim_id", "")): claim_balance(data, claim) for claim in claims
|
||||
},
|
||||
organization=self.repository.get_configuration().get("organization") or {},
|
||||
sender_name=self.sender_name_var.get(),
|
||||
sender_email=self.sender_email_var.get(),
|
||||
signature=self.signature.get("1.0", "end-1c"),
|
||||
)
|
||||
|
||||
def _preview(self) -> None:
|
||||
try:
|
||||
message = BytesParser(policy=default_policy).parsebytes(self._mail_bytes())
|
||||
except (OSError, RepositoryError) as exc:
|
||||
messagebox.showerror("Vorschau nicht möglich", str(exc), parent=self)
|
||||
return
|
||||
window = tk.Toplevel(self)
|
||||
window.title("Vorschau der Willkommensmail")
|
||||
window.transient(self)
|
||||
window.columnconfigure(0, weight=1)
|
||||
window.rowconfigure(1, weight=1)
|
||||
ttk.Label(window, text=f"Betreff: {message['Subject']}", padding=12).grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
preview = tk.Text(window, wrap="word", width=88, height=28)
|
||||
preview.grid(row=1, column=0, sticky="nsew", padx=12, pady=(0, 12))
|
||||
preview.insert("1.0", message.get_content())
|
||||
preview.configure(state="disabled")
|
||||
scroll = ttk.Scrollbar(window, orient="vertical", command=preview.yview)
|
||||
scroll.grid(row=1, column=1, sticky="ns", pady=(0, 12), padx=(0, 12))
|
||||
preview.configure(yscrollcommand=scroll.set)
|
||||
ttk.Button(window, text="Schließen", command=window.destroy).grid(
|
||||
row=2, column=0, columnspan=2, sticky="e", padx=12, pady=(0, 12)
|
||||
)
|
||||
|
||||
def _generate(self) -> None:
|
||||
selected = self._selected_claims()
|
||||
if not selected and not messagebox.askyesno(
|
||||
"Keine Forderung ausgewählt",
|
||||
"Es ist keine Forderung ausgewählt. Soll die Mail ohne Rechnungspositionen "
|
||||
"erzeugt werden?",
|
||||
parent=self,
|
||||
):
|
||||
return
|
||||
delivery_mode = resolve_delivery_mode(self, self.repository)
|
||||
if delivery_mode is None:
|
||||
return
|
||||
output_path = None
|
||||
if delivery_mode == "local":
|
||||
chosen = filedialog.asksaveasfilename(
|
||||
parent=self,
|
||||
title="Willkommensmail als Thunderbird-Entwurf speichern",
|
||||
defaultextension=".eml",
|
||||
initialfile="Willkommen.eml",
|
||||
filetypes=[("E-Mail-Datei", "*.eml"), ("Alle Dateien", "*.*")],
|
||||
)
|
||||
if not chosen:
|
||||
return
|
||||
output_path = chosen
|
||||
try:
|
||||
result = generate_and_send_welcome_mail(
|
||||
self.repository,
|
||||
self.member_id,
|
||||
claim_ids=[str(claim.get("claim_id", "")) for claim in selected],
|
||||
delivery_mode=delivery_mode,
|
||||
output_path=output_path,
|
||||
sender_name=self.sender_name_var.get(),
|
||||
sender_email=self.sender_email_var.get(),
|
||||
signature=self.signature.get("1.0", "end-1c"),
|
||||
)
|
||||
except (OSError, RepositoryError) as exc:
|
||||
messagebox.showerror("E-Mail konnte nicht erzeugt werden", str(exc), parent=self)
|
||||
return
|
||||
outcome = {
|
||||
"local": f"gespeichert:\n{result.export_path}",
|
||||
"send": "direkt versandt.",
|
||||
"drafts": "als Entwurf im Postfach abgelegt.",
|
||||
}[delivery_mode]
|
||||
messagebox.showinfo(
|
||||
"Willkommensmail erzeugt",
|
||||
f"Die E-Mail wurde {outcome}\n\nEine Kopie liegt in der Mitgliederakte.",
|
||||
parent=self,
|
||||
)
|
||||
self.destroy()
|
||||
self.on_sent()
|
||||
Reference in New Issue
Block a user