Import payments per member from a GnuCash file

Adds a "Zahlungen importieren" button to the member Zahlungen tab that opens
a dedicated window: point it at a GnuCash file (plain or gzip-compressed
XML, defaults to the file configured in Optionen), pick one of its accounts,
and narrow the account's bookings down with a description-contains filter
and an optional date range.

Each matching booking gets a checkbox to mark it for import as a payment.
Before anything is ticked, bookings are cross-checked against this member's
existing payments by date + amount; a match is highlighted and its checkbox
is refused, so re-importing the same statement can't create a duplicate
payment. Only bookings with a positive amount on the selected account are
offered, since those are the ones that make sense as an incoming payment.

Parsing lives in ccma.services.gnucash_import, independent of the UI, and
is covered by tests against a synthetic GnuCash XML fixture (plain and
gzip-compressed) -- no gnucash/piecash dependency needed since the native
file format is just XML.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-14 23:02:37 +02:00
co-authored by Claude Sonnet 5
parent e7a18b5cde
commit 745e634a8b
5 changed files with 688 additions and 0 deletions
+13
View File
@@ -7,6 +7,7 @@ from decimal import Decimal
from pathlib import Path
from tkinter import messagebox, ttk
from ccma.config import AppConfig
from ccma.domain.contributions import (
CLAIM_STATUS_LABELS,
DONATION_STATUS_LABELS,
@@ -30,6 +31,7 @@ from ccma.ui.donation_dialog import (
DonationPaymentDialog,
)
from ccma.ui.file_open import open_path
from ccma.ui.gnucash_import_dialog import GnuCashImportDialog
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
@@ -62,6 +64,7 @@ class MemberTab(ttk.Frame):
master: tk.Misc,
repository: MemberRepository,
member_id: str,
config: AppConfig,
on_close: Callable[[], None],
on_changed: Callable[[], None],
on_open_claim: Callable[[str, str], None],
@@ -72,6 +75,7 @@ class MemberTab(ttk.Frame):
super().__init__(master, padding=12)
self.repository = repository
self.member_id = member_id
self.config = config
self.on_close = on_close
self.on_changed = on_changed
self.on_open_claim = on_open_claim
@@ -325,6 +329,10 @@ class MemberTab(ttk.Frame):
ttk.Button(payment_actions, text="Zahlung löschen", command=self._delete_selected_payment).pack(
side="left"
)
ttk.Separator(payment_actions, orient="vertical").pack(side="left", fill="y", padx=10)
ttk.Button(
payment_actions, text="Zahlungen importieren", command=self._import_payments
).pack(side="left")
donations_tab.columnconfigure(0, weight=1)
donations_tab.rowconfigure(1, weight=1)
@@ -733,6 +741,11 @@ class MemberTab(ttk.Frame):
def _create_payment(self) -> None:
PaymentCreateDialog(self, self.repository, self.member_id, self._payment_changed)
def _import_payments(self) -> None:
GnuCashImportDialog(
self, self.repository, self.member_id, self.config, self._payment_changed
)
def _payment_changed(self) -> None:
self.refresh()
self.on_changed()