mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
Replace the GnuCash import checkbox column with native multi-select
Selecting bookings to import now uses the Treeview's own multi-selection (click, Ctrl+click, Shift+click for ranges) instead of a dedicated checkbox column that had to be clicked precisely -- more standard and much faster for marking many rows at once. Bookings matching an existing payment's date+amount are no longer blocked from selection; they're still flagged (red row, "Bereits vorhanden"). If any selected booking is such a duplicate, importing now asks whether to skip those or instead adopt the booking's description onto the already-recorded payment. That relabeling is handled by a new repository.update_payment_reference, which only touches the reference and gnucash_transaction_id fields, leaving date/amount/allocations untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e25accd35b
commit
195ae0e228
@@ -1426,6 +1426,44 @@ class MemberRepository:
|
|||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
def update_payment_reference(
|
||||||
|
self,
|
||||||
|
member_id: str,
|
||||||
|
payment_id: str,
|
||||||
|
*,
|
||||||
|
reference: str,
|
||||||
|
gnucash_transaction_id: str = "",
|
||||||
|
actor_name: str = "Vorstand",
|
||||||
|
) -> dict:
|
||||||
|
"""Relabel an existing payment without touching its date, amount, or
|
||||||
|
allocations -- used e.g. when a GnuCash import recognizes a booking as
|
||||||
|
matching an already-recorded payment and the board wants to adopt the
|
||||||
|
(better) description from the statement instead of re-importing it."""
|
||||||
|
data = self.get_contributions(member_id)
|
||||||
|
payment = next(
|
||||||
|
(item for item in data.payments if str(item.get("payment_id", "")) == payment_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if payment is None:
|
||||||
|
raise RepositoryError("Zahlung nicht gefunden.")
|
||||||
|
gnucash_id = gnucash_transaction_id.strip()
|
||||||
|
if gnucash_id and gnucash_id != str(payment.get("gnucash_transaction_id", "")):
|
||||||
|
self._assert_gnucash_id_available(gnucash_id, exclude_payment_id=payment_id)
|
||||||
|
payment["reference"] = reference.strip()
|
||||||
|
if gnucash_id:
|
||||||
|
payment["gnucash_transaction_id"] = gnucash_id
|
||||||
|
payment["updated_at"] = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||||
|
self.save_contributions(member_id, data)
|
||||||
|
self.append_event(
|
||||||
|
member_id,
|
||||||
|
event_type="payment_changed",
|
||||||
|
summary=f"Zahlung geändert: Referenz aktualisiert ({payment['reference']})",
|
||||||
|
actor_type="user",
|
||||||
|
actor_name=actor_name,
|
||||||
|
references={"payment_id": payment_id},
|
||||||
|
)
|
||||||
|
return payment
|
||||||
|
|
||||||
def delete_payment(self, member_id: str, payment_id: str) -> None:
|
def delete_payment(self, member_id: str, payment_id: str) -> None:
|
||||||
data = self.get_contributions(member_id)
|
data = self.get_contributions(member_id)
|
||||||
payment = next(
|
payment = next(
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ from ccma.ui.monitors import centered_geometry, preferred_monitor
|
|||||||
|
|
||||||
class GnuCashImportDialog(tk.Toplevel):
|
class GnuCashImportDialog(tk.Toplevel):
|
||||||
"""Lets the board point at a GnuCash file, pick one account, narrow its bookings
|
"""Lets the board point at a GnuCash file, pick one account, narrow its bookings
|
||||||
down with filters, and tick which ones to import as payments for this member.
|
down with filters, and select (multi-select: click, Ctrl+click, Shift+click for
|
||||||
|
ranges) which ones to import as payments for this member.
|
||||||
|
|
||||||
Only bookings with a positive amount on the selected account are offered (money
|
Only bookings with a positive amount on the selected account are offered (money
|
||||||
coming in), and any booking whose date + amount already match an existing payment
|
coming in). A booking whose date + amount already match an existing payment for
|
||||||
for this member is flagged and cannot be checked -- avoiding accidental double
|
this member is flagged, but can still be selected -- at import time, the board is
|
||||||
imports if the same statement is imported twice."""
|
asked whether to skip those or instead adopt the booking's description onto the
|
||||||
|
already-recorded payment, rather than silently blocking them."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -51,8 +53,7 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
self.accounts: list[GnuCashAccount] = []
|
self.accounts: list[GnuCashAccount] = []
|
||||||
self.account_by_label: dict[str, GnuCashAccount] = {}
|
self.account_by_label: dict[str, GnuCashAccount] = {}
|
||||||
self.transactions: list[GnuCashTransaction] = []
|
self.transactions: list[GnuCashTransaction] = []
|
||||||
self.selected_guids: set[str] = set()
|
self.existing_payment_by_key: dict[tuple[str, str], str] = {}
|
||||||
self.existing_payment_keys: set[tuple[str, str]] = set()
|
|
||||||
|
|
||||||
self.title("Zahlungen aus GnuCash importieren")
|
self.title("Zahlungen aus GnuCash importieren")
|
||||||
self.transient(master.winfo_toplevel())
|
self.transient(master.winfo_toplevel())
|
||||||
@@ -123,12 +124,11 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
tree_frame.rowconfigure(0, weight=1)
|
tree_frame.rowconfigure(0, weight=1)
|
||||||
self.tree = ttk.Treeview(
|
self.tree = ttk.Treeview(
|
||||||
tree_frame,
|
tree_frame,
|
||||||
columns=("selected", "date", "description", "memo", "amount", "status"),
|
columns=("date", "description", "memo", "amount", "status"),
|
||||||
show="headings",
|
show="headings",
|
||||||
selectmode="browse",
|
selectmode="extended",
|
||||||
)
|
)
|
||||||
for key, title, width in (
|
for key, title, width in (
|
||||||
("selected", "Import", 60),
|
|
||||||
("date", "Datum", 100),
|
("date", "Datum", 100),
|
||||||
("description", "Beschreibung", 280),
|
("description", "Beschreibung", 280),
|
||||||
("memo", "Memo", 180),
|
("memo", "Memo", 180),
|
||||||
@@ -137,20 +137,21 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
):
|
):
|
||||||
self.tree.heading(key, text=title)
|
self.tree.heading(key, text=title)
|
||||||
self.tree.column(key, width=width, anchor="w", stretch=key in {"description", "memo"})
|
self.tree.column(key, width=width, anchor="w", stretch=key in {"description", "memo"})
|
||||||
self.tree.column("selected", anchor="center")
|
|
||||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
self.tree.grid(row=0, column=0, sticky="nsew")
|
||||||
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
|
scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
|
||||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
self.tree.tag_configure("duplicate", background="#7a2323", foreground="#ffffff")
|
self.tree.tag_configure("duplicate", background="#7a2323", foreground="#ffffff")
|
||||||
self.tree.bind("<Button-1>", self._on_tree_click)
|
self.tree.bind("<<TreeviewSelect>>", lambda _event: self._update_summary())
|
||||||
|
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
self,
|
self,
|
||||||
text=(
|
text=(
|
||||||
"Es werden nur Buchungen mit positivem Betrag auf dem gewählten Konto angezeigt "
|
"Es werden nur Buchungen mit positivem Betrag auf dem gewählten Konto angezeigt "
|
||||||
"(eingehende Zahlungen). Rot markierte Buchungen haben Datum und Betrag einer bereits "
|
"(eingehende Zahlungen). Mehrfachauswahl per Klick, Strg+Klick oder Umschalt+Klick. "
|
||||||
"vorhandenen Zahlung dieses Mitglieds und können nicht erneut importiert werden."
|
"Rot markierte Buchungen haben Datum und Betrag einer bereits vorhandenen Zahlung "
|
||||||
|
"dieses Mitglieds; beim Import kann gewählt werden, ob diese übersprungen werden "
|
||||||
|
"oder ob die Beschreibung der vorhandenen Zahlung ersetzt wird."
|
||||||
),
|
),
|
||||||
style="Mono.TLabel",
|
style="Mono.TLabel",
|
||||||
wraplength=900,
|
wraplength=900,
|
||||||
@@ -174,10 +175,10 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
|
|
||||||
def _load_existing_payment_keys(self) -> None:
|
def _load_existing_payment_keys(self) -> None:
|
||||||
data = self.repository.get_contributions(self.member_id)
|
data = self.repository.get_contributions(self.member_id)
|
||||||
self.existing_payment_keys = {
|
self.existing_payment_by_key = {}
|
||||||
(str(payment.get("date", "")), money_text(payment.get("amount", "0")))
|
for payment in data.payments:
|
||||||
for payment in data.payments
|
key = (str(payment.get("date", "")), money_text(payment.get("amount", "0")))
|
||||||
}
|
self.existing_payment_by_key.setdefault(key, str(payment.get("payment_id", "")))
|
||||||
|
|
||||||
def _browse_file(self) -> None:
|
def _browse_file(self) -> None:
|
||||||
current_text = self.file_var.get().strip()
|
current_text = self.file_var.get().strip()
|
||||||
@@ -213,7 +214,6 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
self.account_combo.configure(values=list(self.account_by_label))
|
self.account_combo.configure(values=list(self.account_by_label))
|
||||||
if self.account_by_label and self.account_var.get() not in self.account_by_label:
|
if self.account_by_label and self.account_var.get() not in self.account_by_label:
|
||||||
self.account_var.set(self._preferred_account_label() or next(iter(self.account_by_label)))
|
self.account_var.set(self._preferred_account_label() or next(iter(self.account_by_label)))
|
||||||
self.selected_guids.clear()
|
|
||||||
if self.account_var.get():
|
if self.account_var.get():
|
||||||
self._account_selected()
|
self._account_selected()
|
||||||
else:
|
else:
|
||||||
@@ -257,7 +257,6 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
self._render_transactions()
|
self._render_transactions()
|
||||||
return
|
return
|
||||||
self.transactions = [item for item in all_transactions if item.amount > 0]
|
self.transactions = [item for item in all_transactions if item.amount > 0]
|
||||||
self.selected_guids.clear()
|
|
||||||
self._render_transactions()
|
self._render_transactions()
|
||||||
file_key = self._current_file_key()
|
file_key = self._current_file_key()
|
||||||
if file_key is not None and self.config.gnucash_last_accounts.get(file_key) != account.guid:
|
if file_key is not None and self.config.gnucash_last_accounts.get(file_key) != account.guid:
|
||||||
@@ -286,22 +285,22 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
result.append(item)
|
result.append(item)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _duplicate_payment_id(self, item: GnuCashTransaction) -> str | None:
|
||||||
|
return self.existing_payment_by_key.get((item.date.isoformat(), money_text(item.amount)))
|
||||||
|
|
||||||
def _is_duplicate(self, item: GnuCashTransaction) -> bool:
|
def _is_duplicate(self, item: GnuCashTransaction) -> bool:
|
||||||
return (item.date.isoformat(), money_text(item.amount)) in self.existing_payment_keys
|
return self._duplicate_payment_id(item) is not None
|
||||||
|
|
||||||
def _render_transactions(self) -> None:
|
def _render_transactions(self) -> None:
|
||||||
|
previously_selected = set(self.tree.selection())
|
||||||
self.tree.delete(*self.tree.get_children())
|
self.tree.delete(*self.tree.get_children())
|
||||||
for item in self._filtered_transactions():
|
for item in self._filtered_transactions():
|
||||||
duplicate = self._is_duplicate(item)
|
duplicate = self._is_duplicate(item)
|
||||||
if duplicate:
|
|
||||||
self.selected_guids.discard(item.guid)
|
|
||||||
checked = item.guid in self.selected_guids
|
|
||||||
self.tree.insert(
|
self.tree.insert(
|
||||||
"",
|
"",
|
||||||
"end",
|
"end",
|
||||||
iid=item.guid,
|
iid=item.guid,
|
||||||
values=(
|
values=(
|
||||||
"☑" if checked else "☐",
|
|
||||||
format_date_for_display(item.date.isoformat()),
|
format_date_for_display(item.date.isoformat()),
|
||||||
item.description,
|
item.description,
|
||||||
item.memo,
|
item.memo,
|
||||||
@@ -310,25 +309,15 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
),
|
),
|
||||||
tags=("duplicate",) if duplicate else (),
|
tags=("duplicate",) if duplicate else (),
|
||||||
)
|
)
|
||||||
|
still_present = [guid for guid in previously_selected if self.tree.exists(guid)]
|
||||||
|
if still_present:
|
||||||
|
self.tree.selection_set(still_present)
|
||||||
self._update_summary()
|
self._update_summary()
|
||||||
|
|
||||||
def _on_tree_click(self, event: tk.Event) -> None:
|
|
||||||
row_id = self.tree.identify_row(event.y)
|
|
||||||
column = self.tree.identify_column(event.x)
|
|
||||||
if not row_id or column != "#1":
|
|
||||||
return
|
|
||||||
item = next((entry for entry in self.transactions if entry.guid == row_id), None)
|
|
||||||
if item is None or self._is_duplicate(item):
|
|
||||||
return
|
|
||||||
if row_id in self.selected_guids:
|
|
||||||
self.selected_guids.discard(row_id)
|
|
||||||
else:
|
|
||||||
self.selected_guids.add(row_id)
|
|
||||||
self._render_transactions()
|
|
||||||
|
|
||||||
def _update_summary(self) -> None:
|
def _update_summary(self) -> None:
|
||||||
filtered = self._filtered_transactions()
|
filtered = self._filtered_transactions()
|
||||||
selected_items = [item for item in filtered if item.guid in self.selected_guids]
|
selected_guids = set(self.tree.selection())
|
||||||
|
selected_items = [item for item in filtered if item.guid in selected_guids]
|
||||||
total = sum((item.amount for item in selected_items), Decimal("0"))
|
total = sum((item.amount for item in selected_items), Decimal("0"))
|
||||||
self.summary_var.set(
|
self.summary_var.set(
|
||||||
f"{len(filtered)} Buchungen gefunden · {len(selected_items)} ausgewählt · "
|
f"{len(filtered)} Buchungen gefunden · {len(selected_items)} ausgewählt · "
|
||||||
@@ -336,17 +325,38 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _import_selected(self) -> None:
|
def _import_selected(self) -> None:
|
||||||
selected_items = [
|
selected_guids = set(self.tree.selection())
|
||||||
item
|
selected_items = [item for item in self.transactions if item.guid in selected_guids]
|
||||||
for item in self.transactions
|
|
||||||
if item.guid in self.selected_guids and not self._is_duplicate(item)
|
|
||||||
]
|
|
||||||
if not selected_items:
|
if not selected_items:
|
||||||
messagebox.showinfo("Import", "Bitte mindestens eine Buchung auswählen.", parent=self)
|
messagebox.showinfo("Import", "Bitte mindestens eine Buchung auswählen.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
new_items = [item for item in selected_items if not self._is_duplicate(item)]
|
||||||
|
duplicate_items = [item for item in selected_items if self._is_duplicate(item)]
|
||||||
|
|
||||||
|
replace_description = False
|
||||||
|
if duplicate_items:
|
||||||
|
response = messagebox.askyesnocancel(
|
||||||
|
"Bereits vorhandene Buchungen ausgewählt",
|
||||||
|
(
|
||||||
|
f"{len(duplicate_items)} der ausgewählten Buchungen stimmen in Datum und Betrag "
|
||||||
|
"mit bereits vorhandenen Zahlungen dieses Mitglieds überein.\n\n"
|
||||||
|
"Ja: Diese werden beim Import übersprungen.\n"
|
||||||
|
"Nein: Bei diesen wird stattdessen die Beschreibung der vorhandenen Zahlung "
|
||||||
|
"durch die Beschreibung aus GnuCash ersetzt.\n"
|
||||||
|
"Abbrechen: Es wird nichts importiert."
|
||||||
|
),
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
if response is None:
|
||||||
|
return
|
||||||
|
replace_description = not response
|
||||||
|
|
||||||
imported = 0
|
imported = 0
|
||||||
|
updated = 0
|
||||||
|
processed_guids: set[str] = set()
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
for item in selected_items:
|
for item in new_items:
|
||||||
try:
|
try:
|
||||||
self.repository.create_payment(
|
self.repository.create_payment(
|
||||||
self.member_id,
|
self.member_id,
|
||||||
@@ -356,21 +366,54 @@ class GnuCashImportDialog(tk.Toplevel):
|
|||||||
gnucash_transaction_id=item.guid,
|
gnucash_transaction_id=item.guid,
|
||||||
)
|
)
|
||||||
imported += 1
|
imported += 1
|
||||||
self.selected_guids.discard(item.guid)
|
processed_guids.add(item.guid)
|
||||||
except RepositoryError as exc:
|
except RepositoryError as exc:
|
||||||
errors.append(
|
errors.append(
|
||||||
f"{format_date_for_display(item.date.isoformat())} · {money_text(item.amount)} EUR: {exc}"
|
f"{format_date_for_display(item.date.isoformat())} · {money_text(item.amount)} EUR: {exc}"
|
||||||
)
|
)
|
||||||
if imported:
|
if replace_description:
|
||||||
|
for item in duplicate_items:
|
||||||
|
payment_id = self._duplicate_payment_id(item)
|
||||||
|
if not payment_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
self.repository.update_payment_reference(
|
||||||
|
self.member_id,
|
||||||
|
payment_id,
|
||||||
|
reference=item.description,
|
||||||
|
gnucash_transaction_id=item.guid,
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
processed_guids.add(item.guid)
|
||||||
|
except RepositoryError as exc:
|
||||||
|
errors.append(
|
||||||
|
f"{format_date_for_display(item.date.isoformat())} · "
|
||||||
|
f"{money_text(item.amount)} EUR: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if imported or updated:
|
||||||
self._load_existing_payment_keys()
|
self._load_existing_payment_keys()
|
||||||
self._render_transactions()
|
self._render_transactions()
|
||||||
|
still_selected = set(self.tree.selection()) - processed_guids
|
||||||
|
self.tree.selection_set(list(still_selected))
|
||||||
|
|
||||||
|
summary_parts = []
|
||||||
|
if imported:
|
||||||
|
summary_parts.append(f"{imported} Zahlung(en) importiert")
|
||||||
|
if updated:
|
||||||
|
summary_parts.append(f"{updated} Beschreibung(en) aktualisiert")
|
||||||
|
skipped = len(duplicate_items) if not replace_description else 0
|
||||||
|
if skipped:
|
||||||
|
summary_parts.append(f"{skipped} übersprungen (bereits vorhanden)")
|
||||||
|
summary = ", ".join(summary_parts) or "Keine Buchung verarbeitet."
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
messagebox.showwarning(
|
messagebox.showwarning(
|
||||||
"Import teilweise fehlgeschlagen",
|
"Import teilweise fehlgeschlagen",
|
||||||
f"{imported} Zahlung(en) importiert.\n\nNicht importiert:\n" + "\n".join(errors),
|
f"{summary}.\n\nFehler:\n" + "\n".join(errors),
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo("Import abgeschlossen", f"{imported} Zahlung(en) importiert.", parent=self)
|
messagebox.showinfo("Import abgeschlossen", f"{summary}.", parent=self)
|
||||||
if imported:
|
if imported or updated:
|
||||||
self.on_imported()
|
self.on_imported()
|
||||||
|
|||||||
@@ -251,6 +251,71 @@ def test_payment_can_be_deleted_with_its_allocations(tmp_path) -> None:
|
|||||||
assert repository.get_events(member.member_id)[-1].event_type == "payment_deleted"
|
assert repository.get_events(member.member_id)[-1].event_type == "payment_deleted"
|
||||||
|
|
||||||
|
|
||||||
|
def test_payment_reference_can_be_replaced_without_touching_amount_or_allocations(tmp_path) -> None:
|
||||||
|
repository, member = _repository_with_claim(tmp_path)
|
||||||
|
payment = repository.record_payment(
|
||||||
|
member.member_id,
|
||||||
|
"claim-1",
|
||||||
|
payment_date="2026-06-21",
|
||||||
|
amount="10.00",
|
||||||
|
allocation_amount="10.00",
|
||||||
|
reference="Alte Referenz",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated = repository.update_payment_reference(
|
||||||
|
member.member_id,
|
||||||
|
payment["payment_id"],
|
||||||
|
reference="Neue Referenz aus GnuCash",
|
||||||
|
gnucash_transaction_id="TX-99",
|
||||||
|
)
|
||||||
|
|
||||||
|
data = repository.get_contributions(member.member_id)
|
||||||
|
assert updated["reference"] == "Neue Referenz aus GnuCash"
|
||||||
|
assert updated["gnucash_transaction_id"] == "TX-99"
|
||||||
|
assert data.payments[0]["amount"] == "10.00"
|
||||||
|
assert payment_allocated_total(data, payment["payment_id"]) == Decimal("10.00")
|
||||||
|
assert repository.get_events(member.member_id)[-1].event_type == "payment_changed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_payment_reference_update_rejects_gnucash_id_already_used_elsewhere(tmp_path) -> None:
|
||||||
|
repository, member = _repository_with_claim(tmp_path)
|
||||||
|
repository.record_payment(
|
||||||
|
member.member_id,
|
||||||
|
"claim-1",
|
||||||
|
payment_date="2026-06-21",
|
||||||
|
amount="10.00",
|
||||||
|
allocation_amount="10.00",
|
||||||
|
gnucash_transaction_id="TX-1",
|
||||||
|
)
|
||||||
|
data = repository.get_contributions(member.member_id)
|
||||||
|
data.claims.append(
|
||||||
|
{
|
||||||
|
"claim_id": "claim-2",
|
||||||
|
"claim_key": "second-claim",
|
||||||
|
"title": "Zweite Forderung",
|
||||||
|
"amount": "20.00",
|
||||||
|
"due_date": "2026-12-31",
|
||||||
|
"status": "open",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
repository.save_contributions(member.member_id, data)
|
||||||
|
second_payment = repository.record_payment(
|
||||||
|
member.member_id,
|
||||||
|
"claim-2",
|
||||||
|
payment_date="2026-06-22",
|
||||||
|
amount="20.00",
|
||||||
|
allocation_amount="20.00",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RepositoryError, match="GnuCash-ID bereits verwendet"):
|
||||||
|
repository.update_payment_reference(
|
||||||
|
member.member_id,
|
||||||
|
second_payment["payment_id"],
|
||||||
|
reference="Duplikatversuch",
|
||||||
|
gnucash_transaction_id="TX-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_credit_claim_settlement_is_displayed_as_positive_amount() -> None:
|
def test_credit_claim_settlement_is_displayed_as_positive_amount() -> None:
|
||||||
claim = {"claim_id": "claim-1", "title": "Kautionsrückzahlung", "amount": "-25.00"}
|
claim = {"claim_id": "claim-1", "title": "Kautionsrückzahlung", "amount": "-25.00"}
|
||||||
data = ContributionData(
|
data = ContributionData(
|
||||||
|
|||||||
Reference in New Issue
Block a user