Commit Graph
58 Commits
Author SHA1 Message Date
Marcel PeterkauandClaude Opus 5 aa0c16347b Send new members a welcome mail that doubles as their first invoice
Accepting a member left the board writing the "welcome, you are a member from
the 1st, and this is what it costs" mail by hand. The member file now offers it
directly: it greets the member, names the membership number and the start of the
membership, and lists every claim that is still open -- admission fee and first
contribution -- with a payment hint that adapts to whether a direct debit
mandate is active.

"Mitglied seit" rejects future dates, so a member accepted this month cannot
carry their actual start date yet; {{membership.start_date}} therefore falls
back to the 1st of the month following the acceptance decision. Individual
claims can be deselected before sending, the finished mail can be previewed,
and the result is archived in the member file like every other generated mail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:49:08 +02:00
Marcel PeterkauandClaude Opus 5 d6cd58a788 Render every outgoing e-mail from an editable text template
The wording of the dunning and SEPA pre-notification mails was hard-coded in
Python, so adjusting a single sentence required a new release. Both texts now
live in plain text templates that ship as defaults, are copied into the store's
templates/mail/ directory on first start and can be edited there or under
Optionen -> E-Mail-Vorlagen; an existing file is never overwritten and a deleted
one is restored from the shipped default.

A template carries its subject in the first line and the body after a blank
line. Placeholders use the same {{ ... }} syntax as the document templates and
share their member/organization values, so a name means the same thing in a
letter and in the mail that carries it. Unknown placeholders are rejected while
editing instead of during a send run, a line holding nothing but placeholders
that render empty is dropped, and {{#claims}} ... {{/claims}} repeats per entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:49:08 +02:00
Marcel PeterkauandClaude Sonnet 5 cc4aaef895 Copy directly-sent e-mails to an IMAP Sent folder, with a live folder picker
A raw SMTP send has no server-side "Sent" copy on its own (unlike IMAP
drafts, which are inherently server-side) -- add an opt-in checkbox plus a
configurable target folder so directly sent Mahnungen/SEPA-info-mails still
show up in the account's Gesendet/Sent folder like a normal mail client
would leave them. Applies only to "send" delivery; drafts already live on
the server by definition.

Both the Entwürfe- and Gesendet-folder fields are now editable comboboxes:
a new "Ordnerliste laden" button fetches the real folder list from the IMAP
server (needs working credentials first) via LIST, decoding folder names
from modified UTF-7 (RFC 3501) so names like "Entwürfe" render correctly
instead of as "Entw&APw-rfe". Free text still works -- ensure_imap_folder()
creates the folder on first use if it doesn't exist yet, checked once per
batch rather than before every single message.

mail_delivery.append_to_imap_drafts() became the more general
append_message(client, content, folder=, flags=), reused for both the
\Draft and \Seen cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 04:34:34 +02:00
Marcel PeterkauandClaude Sonnet 5 6b0da82b45 Add configurable e-mail delivery: direct SMTP send or IMAP drafts
Mahnungs- and SEPA-info-mails could previously only be saved as a local .eml
file that still had to be manually imported into Thunderbird. Add a per-store
"E-Mail-Versand" configuration (Optionen -> E-Mail-Versand, stored in
repository.json alongside the rest of the club's settings, since different
stores may use different mailboxes) with four delivery modes:

- "Lokal speichern": today's behaviour, unchanged default for existing stores.
- "Direkt versenden": sends via SMTP.
- "Als Entwurf ablegen": IMAP APPENDs into a configurable drafts folder, so it
  shows up live in whatever mail client is already watching that account.
- "Jedes Mal fragen": prompts once per generation action (not per e-mail --
  a SEPA batch can cover dozens of members) with Senden/Entwürfe/Abbrechen.

New ccma.services.mail_delivery module (smtplib/imaplib, no new dependency)
opens one authenticated connection per batch and reuses it across all
messages instead of reconnecting per recipient. Both "Verbindung testen"
buttons in Options exercise the same connection path used for real delivery.
The archived per-member copy of every generated e-mail is unaffected and
still always written regardless of delivery mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 04:34:34 +02:00
Marcel Peterkau 85a865fa0e Merge branch 'dev' into feature/sepa-export-selection 2026-08-15 02:02:14 +00:00
Marcel Peterkau aa5829acb0 Merge branch 'dev' into feature/failed-direct-debit 2026-08-15 01:58:19 +00:00
Marcel PeterkauandClaude Sonnet 5 bfb65b2583 Let individual SEPA debits be excluded from an export, not just all-or-nothing
The SEPA export dialog always exported every pending direct debit in the
filtered date range with no way to hold one back -- e.g. a member whose
Rücklastschrift is still being clarified had to either be collected again
regardless or the whole run skipped. The table now supports multi-select
(everything selected by default) and the CSV/XML/mail exports only include
the currently selected rows. Deselection survives the refresh() that
_prepare() runs right before exporting (tracked by member_id, not row index,
so a debit newly appearing after a requery still defaults to selected).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:51:32 +02:00
Marcel PeterkauandClaude Sonnet 5 a1fb2f4e30 Refresh the member tab after a single-member housekeeper run
_execute_housekeeper_run() showed the success dialog but never called
refresh()/on_changed(), so retroactively created claims/payments stayed
invisible in the currently open Mitgliedsakte until it was closed and
reopened, and the overview list wasn't notified either. Match the pattern
used by every other mutating action in this file (save, payment, credit,
reminder, ...): refresh the tab's own contribution lists and notify the
overview callback right after the run succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:40:40 +02:00
Marcel PeterkauandClaude Sonnet 5 186af530b0 Allow editing reminders (incl. undoing "sent") and prefill a Rücklastschrift reason
A generated/sent Mahnung email couldn't be corrected before: sent reminders were
locked, so a wording mistake meant living with it. Add edit_reminder_draft() for
draft/generated reminders and revert_reminder_sent() to safely undo a "sent"
reminder (removing the fee items it booked, refusing if a payment already covers
them or a higher Mahnstufe already exists) so it can be edited and resent.

ReminderDialog now supports an edit mode (reminder=... prefills name/detail/
items/deadline/channel, level stays fixed) and claim_tab gained a "Mahnung
bearbeiten" button that reverts-then-edits for sent reminders automatically,
asking for confirmation first since it un-books the fee.

The Rücklastschrift preset now also prefills "Details" with a short explanation
("Leider konnte die Lastschrift ... nicht eingelöst werden.") since that field
goes straight into the reminder email -- the board can append the concrete
reason (mangels Deckung, Konto ungültig, ...) right there instead of the field
starting empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:13:04 +02:00
Marcel PeterkauandClaude Sonnet 5 f3bdffd347 Make ReminderDialog resizable/wider and fix Options dialog bottom clipping
ReminderDialog: override the shared dialog base's resizable(False, False),
widen the initial layout, and pin the items-table edit row's amount field
to the exact same pixel width as the table's fixed "amount" column (via a
pack_propagate(False) frame) while the description field stretches like
the table's own column does -- keeps alignment exact even after resizing.

OptionsDialog: compute the window's initial geometry from its actual
required size after all tabs (incl. "Mahnungen") are built, instead of a
static guess that clipped the Mahnungen tab's tables/buttons at the bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:13:04 +02:00
Marcel PeterkauandClaude Sonnet 5 3940926a22 Line up the reminder items edit row with the table columns
Beschreibung/Betrag now sit on their own row directly under the table,
sized to the same 220:100 ratio as the table's own columns, with the
Hinzufügen/Aktualisieren/Entfernen buttons moved to a row below that
instead of everything being crammed into one row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:13:04 +02:00
Marcel PeterkauandClaude Sonnet 5 d4dfd0066a 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>
2026-08-15 03:13:04 +02:00
Marcel PeterkauandClaude Sonnet 5 a1719cad5e Handle failed direct debits through the same reminder mechanism
A Rücklastschrift is still the member's own failure to ensure cover or
provide valid bank details, so it belongs in the same escalation ladder
as a regular reminder -- it now occupies whatever level the claim is next
due for, alongside a "Rücklastschrift" preset next to the existing
Mahnstufe presets.

Mahnung vorbereiten no longer lets the board pick a level (it's computed
automatically from what's already been sent, since the sequencing was
already server-enforced) and replaces the single fee field with a small
items table (Beschreibung + Betrag), so a reminder can carry several
charges at once -- e.g. Rücklastschriftgebühr plus Bankgebühr plus Porto
-- each landing on the claim as its own line item instead of one lump
sum under a generic label. The description field offers the configured
standard texts but stays free-editable.

Those standard texts and amounts, plus the dunning levels themselves
(name/fee/deadline), are now configurable from Optionen -> Mahnungen
instead of only being editable by hand-editing repository.json, which
was the case for the whole reminder policy until now. Cleaned up
reminder_fee/failed_debit_fee on contribution rules while at it -- both
were unused leftovers superseded by this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 03:13:04 +02:00
Marcel PeterkauandClaude Sonnet 5 ee99577b20 Run the housekeeper for a single member, optionally retroactively
Add a "Hausmeister für dieses Mitglied" button to the member view. It
runs every rule scoped to just that member -- e.g. after editing a
contribution override or payment frequency, to recompute their claims on
demand instead of waiting for or triggering a full run over every
member. A checkbox lets the board force retroactive claim creation for
just that one run, without changing the application-wide default
setting.

Housekeeper.run() gained member_id and retroactive_claims parameters.
Scoping to one member skips the asset checks and leaves every other
member's tasks completely untouched, since their rules never ran this
pass and so fall outside the resolved-scope set that
_resolve_stale_tasks uses to decide what to clear.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 00:59:01 +02:00
Marcel PeterkauandClaude Sonnet 5 2484a1631d Add individually agreed membership fees (contribution overrides)
Members are sometimes given a fee that deviates from the regular
schedule -- e.g. a reduced rate for students -- for a specific period.
Add a per-member "Beitrag" tab where such deviations can be recorded
with a month-granular date range (Ab/Bis), a mandatory reason, and
either a fixed annual amount or a percentage discount off whichever
base rate is in effect at the time.

Data model: ContributionData gets a contribution_overrides list, each
entry validated (month format, Bis >= Ab, non-overlapping ranges per
member, reason required) and CRUD'd through the repository
(record/update/delete/get_contribution_override), consistent with how
donations already work.

Integration: contribution_claims.py now computes each membership-fee
claim's amount month by month instead of a single rate for the whole
billing period, picking up whichever override (if any) covers each
individual month. That handles an override starting or ending mid
period correctly (e.g. a semiannual payer whose discount begins in
March) without changing behavior for members without overrides.
Already-created claims are never recalculated retroactively, matching
how changes to the global contribution rates already behave.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 00:57:51 +02:00
Marcel PeterkauandClaude Sonnet 5 4906db3d55 Prefill the allocation amount when clicking an unallocated claim/donation
Clicking a claim or donation row in the payment allocation table used to
just show "0.00" if nothing was allocated to it yet, requiring the board
to type the amount by hand every time. It now suggests whichever is
smaller: what that claim/donation still has open, or what's left unused
of the payment -- so the common case is click the row, then Zuordnung
setzen, done. Rows that already have an allocation still show their
current value instead, so editing one isn't overwritten by the
suggestion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 00:02:37 +02:00
Marcel PeterkauandClaude Sonnet 5 195ae0e228 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>
2026-08-14 23:43:55 +02:00
Marcel PeterkauandClaude Sonnet 5 e25accd35b Fix column order/widths and sizing in Hausmeister and GnuCash import
Hausmeister: reorder columns to Level, Fällig, Vorgang, Details. Level,
Fällig and Vorgang get a fixed width; only Details stretches to fill the
remaining space.

GnuCash-Import dialog: opens at 80% of the current monitor's size
(monitor-aware via the existing screeninfo-based helpers, not just the
raw/virtual screen size) instead of a fixed 960x640. In its booking
table, Import, Datum, Betrag and Status get a fixed width; Beschreibung
and Memo share the remaining space.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 23:26:07 +02:00
Marcel PeterkauandClaude Sonnet 5 65bcc9f447 Fix column order/widths and Chronik pane share in the member view
Forderungen: reorder columns to Fällig, Betrag, Status, Forderung.
Zahlungen and Spenden already had the requested order. Across all three
tables, the date/amount-like columns now share one fixed width, the
status column is a third wider (it tends to hold longer label text),
and only the trailing description/reference column stretches to fill
the remaining space.

The Chronik pane now defaults to a third of the member view's width
instead of three fifths, matching the other panes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 23:11:44 +02:00
Marcel PeterkauandClaude Sonnet 5 42cde2a0c8 Remember the last-used GnuCash account per file
AppConfig now keeps a gnucash_last_accounts map (file path -> account
guid). When the import dialog opens a file it already knows, it
auto-selects whichever account was picked last time for that specific
file instead of always defaulting to the first one alphabetically;
picking a different account updates and persists the mapping right away.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 23:02:37 +02:00
Marcel PeterkauandClaude Sonnet 5 745e634a8b 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>
2026-08-14 23:02:37 +02:00
Marcel PeterkauandClaude Sonnet 5 4dd625c09f Only list open or already-linked claims/donations in the payment dialog
Show a claim or donation in the Zahlung anlegen/bearbeiten allocation
table only if it still has an open balance, or is already linked to this
payment -- fully settled ones no longer clutter the list.

Visibility is decided from a snapshot taken when the dialog opens (or
when a donation is created inline), not recomputed live, so a row never
disappears just because the user temporarily unassigned it with
"Zuordnung lösen" -- it stays there to add back with "Zuordnung setzen"
until the dialog is saved.

Also fixes a capacity display bug this surfaced: "Maximal zuordenbar"
was computed by mixing the persisted claim/donation balance with the
live, currently-edited allocation amount, so it inflated every time the
amount was changed during a session. It's now computed against a fixed
baseline captured at dialog open, so it stays accurate throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:28:52 +02:00
Marcel PeterkauandClaude Sonnet 5 a5d9abd59a Allocate open claims and donations directly from the payment dialog
Recording a payment previously meant saving it bare, then separately
opening a claim to allocate money to it. Zahlung anlegen now lists a
member's open claims and donations right in the same dialog with a
select + amount field to assign parts of the payment on the spot, and a
"Neue Spende anlegen" button to create a donation inline and allocate to
it immediately -- covering members who pay more than the membership fee
in one transfer.

Shared the same allocation table in the existing payment-edit dialog so
editing a payment shows and preserves donation allocations too; before
this, saving an edited payment silently dropped any donation allocation
because update_payment only round-tripped claim allocations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:22:08 +02:00
Marcel PeterkauandClaude Sonnet 5 42fb4c4224 Allow deleting claims, creating standalone payments, and add a donations tab
Claims could previously only be cancelled (stornieren), which blocks once a
payment is allocated. Add a hard delete that releases any linked
payments/credits back to being unallocated instead of destroying them.

Payments could only be created from within a claim, forcing immediate
allocation. Add a bare payment creation flow in the Zahlungen tab so
incoming transfers can be logged first and allocated later.

Add a per-member Spenden tab (donations, backed by a new donations list on
ContributionData) so amounts paid beyond the membership fee can be tracked
and existing free payments allocated to them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 21:02:51 +02:00
Marcel Peterkau 5bacc005f6 Merge branch 'dev' into feature/payment-frequency-per-member 2026-07-30 17:40:45 +00:00
Marcel Peterkau 7a0cdbc04e feat: add application date to members 2026-07-30 19:35:48 +02:00
Marcel Peterkau ba3238e61a ui: move payment frequency to banking tab 2026-07-30 19:22:12 +02:00
Marcel Peterkau c3cdf71506 feat: add per-member payment frequencies 2026-07-30 19:17:26 +02:00
Marcel Peterkau cfd5de42f2 feat: configure member number allocation strategy 2026-07-30 19:07:18 +02:00
Marcel Peterkau fc4eacb591 feat: add reminder email drafts 2026-07-30 01:17:33 +02:00
Marcel Peterkau 3d3b845d9f feat: add SEPA direct debit exports and notifications 2026-07-30 01:00:32 +02:00
Marcel Peterkau 9942e7aa22 fix: improve member list defaults and highlighting 2026-07-24 18:14:49 +02:00
Marcel Peterkau 070684d9bc feat: extend inventory and administration workflows 2026-07-22 22:26:02 +02:00
Marcel Peterkau d8900d9767 Show credit settlements as positive amounts 2026-06-27 15:39:52 +02:00
Marcel Peterkau 3876f8c5ab Fix ruff lint violations 2026-06-27 10:46:54 +02:00
Marcel Peterkau 8fba13aea0 Unify list filter sections 2026-06-27 10:36:18 +02:00
Marcel Peterkau 04c23fbdf9 Refine member and asset detail layouts 2026-06-27 10:36:03 +02:00
Marcel Peterkau 87e972bb43 Add JSON integrity hash checks 2026-06-27 10:35:35 +02:00
Marcel Peterkau d1dab793a6 Add asset records, claims, and credit workflows 2026-06-26 23:03:06 +02:00
Marcel Peterkau 30b6d253b2 Update member UI and related app changes 2026-06-26 21:57:11 +02:00
Marcel Peterkau 6c7bf63280 feat: add member address and SEPA data 2026-06-21 22:14:45 +02:00
Marcel Peterkau 0622a22794 feat: add OpenDocument PDF templates 2026-06-21 22:10:16 +02:00
Marcel Peterkau b34135b34a feat: unify claim activity view 2026-06-21 21:51:47 +02:00
Marcel Peterkau 2c89732228 fix: align splash progress fill 2026-06-21 21:44:38 +02:00
Marcel Peterkau e6200f4a02 feat: pace startup housekeeper runs 2026-06-21 21:42:38 +02:00
Marcel Peterkau fc042f6711 feat: style timed splash progress 2026-06-21 18:51:31 +02:00
Marcel Peterkau e1d2b87ca1 feat: configure minimum splash duration 2026-06-21 18:48:08 +02:00
Marcel Peterkau dadcdb8b4a feat: use branded splash background 2026-06-21 18:44:27 +02:00
Marcel Peterkau e6d2f77d1e feat: add staged reminder workflow 2026-06-21 18:40:54 +02:00
Marcel Peterkau 288b5f6247 refactor: rename member claims tab 2026-06-21 18:31:27 +02:00