mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
Fix reminder dialog sizing, item editing, and old-repo fee defaults
The dialog's base class sets resizable(False, False) before this subclass's content (including the preset-populated items table) exists, so its initial size stayed locked to a too-small guess and cut off the bottom. It now explicitly sizes to its actual content after everything, including the selected preset's items, has been built. The items table only supported add/remove -- there was no way to change an already-added row's amount (e.g. after picking the Rücklastschrift preset, its prefilled fee couldn't be adjusted). Selecting a row now loads it into the description/amount fields, and a new "Aktualisieren" button applies edits back to that row. The Rücklastschrift preset label was missing the "Stufe N:" prefix the other presets have, inconsistent for no reason. Also fixed a real gap: repositories created before standard_fee_items existed had no such key in repository.json at all, so Optionen showed an empty Standardpositionen table instead of the built-in defaults. get_reminder_policy() now backfills the defaults when the key is missing entirely, while still respecting a list the board intentionally emptied and saved. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a1719cad5e
commit
d4dfd0066a
@@ -2441,12 +2441,19 @@ class MemberRepository:
|
||||
),
|
||||
key=lambda entry: entry["level"],
|
||||
)
|
||||
# Repositories created before this setting existed have no "standard_fee_items"
|
||||
# key at all -- fall back to the built-in defaults rather than an empty list, but
|
||||
# once the board has actually saved (and possibly emptied) the list, respect that.
|
||||
if "standard_fee_items" in policy:
|
||||
standard_items_raw = policy.get("standard_fee_items") or []
|
||||
else:
|
||||
standard_items_raw = DEFAULT_CONFIGURATION["reminder_policy"]["standard_fee_items"]
|
||||
standard_fee_items = [
|
||||
{
|
||||
"description": str(entry.get("description", "")),
|
||||
"default_amount": money_text(entry.get("default_amount", "0")),
|
||||
}
|
||||
for entry in policy.get("standard_fee_items") or []
|
||||
for entry in standard_items_raw
|
||||
]
|
||||
return {
|
||||
"grace_days_after_due": int(policy.get("grace_days_after_due", 7)),
|
||||
|
||||
@@ -939,6 +939,14 @@ class ReminderDialog(_Dialog):
|
||||
self._build_items_table(row=6)
|
||||
self._apply_preset()
|
||||
self._buttons(7, self._save)
|
||||
# Size to fit the fully built content (incl. the preset-populated items table)
|
||||
# instead of whatever size Tk guessed before everything was in place -- the
|
||||
# base dialog sets resizable(False, False) before this subclass's content
|
||||
# exists, so an explicit geometry() is needed or the window stays clipped.
|
||||
self.update_idletasks()
|
||||
width, height = self.winfo_reqwidth(), self.winfo_reqheight()
|
||||
self.geometry(f"{width}x{height}")
|
||||
self.minsize(width, height)
|
||||
|
||||
def _build_presets(self) -> dict[str, dict]:
|
||||
presets: dict[str, dict] = {}
|
||||
@@ -951,7 +959,7 @@ class ReminderDialog(_Dialog):
|
||||
"items": [{"description": "Mahngebühr", "amount": money_text(fee)}] if fee > 0 else [],
|
||||
}
|
||||
failed_debit_amount = self._default_amount_for("Rücklastschrift")
|
||||
presets["Rücklastschrift"] = {
|
||||
presets[f"Stufe {self.level}: Rücklastschrift"] = {
|
||||
"name": "Rücklastschrift",
|
||||
"payment_deadline_days": 14,
|
||||
"items": [{"description": "Rücklastschriftgebühr", "amount": failed_debit_amount}],
|
||||
@@ -984,6 +992,7 @@ class ReminderDialog(_Dialog):
|
||||
self.items_tree.column("description", width=220, anchor="w")
|
||||
self.items_tree.column("amount", width=100, anchor="w", stretch=False)
|
||||
self.items_tree.grid(row=0, column=0, sticky="ew")
|
||||
self.items_tree.bind("<<TreeviewSelect>>", lambda _event: self._load_selected_item())
|
||||
|
||||
controls = ttk.Frame(table_frame)
|
||||
controls.grid(row=1, column=0, sticky="ew", pady=(8, 0))
|
||||
@@ -1001,6 +1010,7 @@ class ReminderDialog(_Dialog):
|
||||
ttk.Button(controls, text="Position hinzufügen", command=self._add_item).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(controls, text="Aktualisieren", command=self._update_item).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(controls, text="Position entfernen", command=self._remove_item).pack(side="left")
|
||||
|
||||
def _prefill_item_amount(self) -> None:
|
||||
@@ -1017,6 +1027,14 @@ class ReminderDialog(_Dialog):
|
||||
"", "end", iid=str(index), values=(item["description"], f"{item['amount']} EUR")
|
||||
)
|
||||
|
||||
def _load_selected_item(self) -> None:
|
||||
selected = self.items_tree.selection()
|
||||
if not selected:
|
||||
return
|
||||
item = self.items[int(selected[0])]
|
||||
self.item_description_var.set(item["description"])
|
||||
self.item_amount_var.set(item["amount"])
|
||||
|
||||
def _add_item(self) -> None:
|
||||
description = self.item_description_var.get().strip()
|
||||
if not description:
|
||||
@@ -1032,12 +1050,33 @@ class ReminderDialog(_Dialog):
|
||||
self.item_amount_var.set("")
|
||||
self._refresh_items()
|
||||
|
||||
def _update_item(self) -> None:
|
||||
selected = self.items_tree.selection()
|
||||
if not selected:
|
||||
messagebox.showerror(
|
||||
"Auswahl fehlt", "Bitte die zu ändernde Position auswählen.", parent=self
|
||||
)
|
||||
return
|
||||
description = self.item_description_var.get().strip()
|
||||
if not description:
|
||||
messagebox.showerror("Beschreibung fehlt", "Bitte eine Beschreibung angeben.", parent=self)
|
||||
return
|
||||
try:
|
||||
amount = decimal_value(self.item_amount_var.get(), "Betrag")
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("Ungültiger Betrag", str(exc), parent=self)
|
||||
return
|
||||
self.items[int(selected[0])] = {"description": description, "amount": money_text(amount)}
|
||||
self._refresh_items()
|
||||
|
||||
def _remove_item(self) -> None:
|
||||
selected = self.items_tree.selection()
|
||||
if not selected:
|
||||
messagebox.showerror("Auswahl fehlt", "Bitte eine Position auswählen.", parent=self)
|
||||
return
|
||||
del self.items[int(selected[0])]
|
||||
self.item_description_var.set("")
|
||||
self.item_amount_var.set("")
|
||||
self._refresh_items()
|
||||
|
||||
def _save(self):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -278,3 +279,25 @@ def test_reminder_policy_rejects_invalid_input(tmp_path) -> None:
|
||||
levels=[{"name": "Mahnung", "fee": "-1.00", "payment_deadline_days": 14}],
|
||||
standard_fee_items=[],
|
||||
)
|
||||
|
||||
|
||||
def test_reminder_policy_backfills_standard_fee_items_for_old_repositories(tmp_path) -> None:
|
||||
repository = MemberRepository(tmp_path)
|
||||
repository.initialize()
|
||||
config_path = tmp_path / "repository.json"
|
||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
# Simulate a repository created before "standard_fee_items" existed at all.
|
||||
del raw["reminder_policy"]["standard_fee_items"]
|
||||
config_path.write_text(json.dumps(raw), encoding="utf-8")
|
||||
|
||||
policy = repository.get_reminder_policy()
|
||||
assert policy["standard_fee_items"], "should fall back to built-in defaults"
|
||||
assert any(item["description"] == "Rücklastschriftgebühr" for item in policy["standard_fee_items"])
|
||||
|
||||
# But once the board explicitly saves an empty list, that choice is respected.
|
||||
repository.save_reminder_policy(
|
||||
grace_days_after_due=policy["grace_days_after_due"],
|
||||
levels=policy["levels"],
|
||||
standard_fee_items=[],
|
||||
)
|
||||
assert repository.get_reminder_policy()["standard_fee_items"] == []
|
||||
|
||||
Reference in New Issue
Block a user