Commit Graph
34 Commits
Author SHA1 Message Date
Marcel PeterkauandClaude Opus 5 c46662561e Reject unbalanced repeat markers in mail templates
validate_mail_template() only looked at placeholder names, and "{{#claims}}" is
not a placeholder -- so an unclosed block or a stray "{{/claims}}" passed the
check and rendered as itself: the member would read the marker in their mail.

Repeat markers are now checked structurally: every one names a block the template
actually has, openers and closers pair up in order, blocks do not nest (the
renderer does not support it either), and the subject takes no markers at all.
Each case explains what is wrong and what is missing.

The same check runs before sending, not just before saving: the templates are
plain files in the store and can be edited outside CCMA, where refusing to send
beats mailing a marker. Saving several edited templates now validates all of
them before writing the first, so a mistake in one no longer leaves the others
half-saved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:19:35 +02:00
Marcel PeterkauandClaude Opus 5 592c5482d6 Merge branch 'dev' into feature/mail-templates
dev gained the delivery/rollback semantics for the dunning and SEPA mails while
the mail texts were being moved into templates here. Both services keep dev's
flow -- roll back only while nothing has gone out, log the event even when
archiving fails afterwards -- and render their content from the template.

The welcome mail follows the same rule now, including its archiving-failure
path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:50:51 +02:00
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 Peterkau a4536eedd0 Cover archive-directory setup in the sent/delivered rollback path
archive_dir.mkdir() and the archive path lookup ran outside the
try/except that decides whether to revert mark_reminder_sent (reminder
mail) or keep a SEPA batch running for the other debits. A failure there
(read-only store, full disk) left a reminder booked as "sent" with no
mail ever having gone out, and could still abort an entire SEPA batch
for one member's directory problem.

Moved that setup inside the same try blocks so it's treated exactly like
any other pre-delivery failure: reminder_mail reverts to draft, and
sepa_mail records a warning and continues with the remaining debits.
Also moved debit_mail_bytes() into the per-debit try in sepa_mail for
the same reason. Added a targeted mkdir-failure test for each.
2026-08-20 12:51:17 +02:00
Marcel Peterkau 5fc6d7aec9 Stop rolling back reminders/SEPA mails after a successful send
The previous fix reverted the reminder to "draft" on any failure after
mark_reminder_sent, including failures that happened after the mail had
already been handed to send_via_smtp or appended to an IMAP folder. That
made a successful send followed by a Sent-folder-copy or archive-move
failure look like nothing was sent, inviting a duplicate send/fee booking
on retry -- the same "sent" flag needs to be preserved once delivery is
no longer reversible, per follow-up review.

reminder_mail.generate_and_send_reminder_mail and
sepa_mail.generate_debit_mails now track whether the mail actually left
the building (SMTP accepted / IMAP append succeeded / local file written)
separately from the later archiving step:
- Failure before that point: reminder_mail reverts to draft (unchanged);
  sepa_mail now records a warning and continues with the remaining
  debits instead of aborting the whole batch.
- Failure after that point (Sent-copy append, moving the archive file
  into place): the reminder stays "sent" / the debit stays in the
  batch's results, an event is still logged for traceability (with an
  archive_error note and no document reference), and the caller gets a
  clear error to follow up on manually -- no rollback, no silent loss of
  the fact that the mail already went out.
2026-08-20 12:22:37 +02:00
Marcel Peterkau aab43cc0fe Roll back reminder status when direct send/draft delivery fails
generate_and_send_reminder_mail marked a reminder as "sent" before
attempting SMTP send/IMAP append. If the connection, login, or append
failed, the reminder stayed persisted as sent even though no mail was
ever delivered or filed, so it could not be corrected and resent.

Wrap content generation, archiving, and delivery in a try/except that
calls the existing revert_reminder_sent on any failure and re-raises,
putting the reminder back into "draft" for all three delivery modes.
2026-08-20 12:08:44 +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 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 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 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 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 87e972bb43 Add JSON integrity hash checks 2026-06-27 10:35:35 +02:00
Marcel Peterkau 30b6d253b2 Update member UI and related app changes 2026-06-26 21:57:11 +02:00
Marcel Peterkau 0e3087a780 ci: add CCMA release builds 2026-06-23 20:19:53 +02:00
Marcel Peterkau 302170230a feat: Add Windows compatibility, PyInstaller build setup, and custom icon
- Add Windows LibreOffice detection fallback for soffice path
  - Check standard install locations (%PROGRAMFILES%, %PROGRAMFILES(X86)%)
  - Graceful fallback with clear error messages

- Add PyInstaller build infrastructure
  - build/ccma.spec: PyInstaller configuration with icon generation
  - build/build.ps1: Automated build script for standalone exe
  - main.py: Entry point for PyInstaller
  - Supports versioning and architecture tagging

- Create custom CCMA icon
  - Person + Gear symbol representing member administration
  - Cyan/White/Orange color scheme
  - Auto-converts PNG to ICO during build

- Update documentation
  - README: Windows PowerShell setup instructions
  - README: Linux/macOS bash setup instructions
  - README: Standalone executable build guide
  - pyproject.toml: Add 'build' extra with pyinstaller+pillow

- Add regression tests
  - Test office executable detection and Windows fallback
  - Verify all tests pass (80 passed)
2026-06-23 11:08:18 +02:00
Marcel Peterkau 576220ccd5 refactor: remove template timestamp aliases 2026-06-21 22:44:09 +02:00
Marcel Peterkau 3c842f29a3 feat: add template creation timestamps 2026-06-21 22:41:53 +02:00
Marcel Peterkau c58072fe45 feat: repeat claim items in document tables 2026-06-21 22:33:24 +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 e6200f4a02 feat: pace startup housekeeper runs 2026-06-21 21:42:38 +02:00
Marcel Peterkau e7962f77e1 refactor: localize UI labels and store filenames 2026-06-21 18:25:58 +02:00
Marcel Peterkau 80d4d5ef90 feat: add itemized claims and payments 2026-06-21 18:20:55 +02:00
Marcel Peterkau c717d6806b feat: allow deleting housekeeper tasks 2026-06-21 18:09:04 +02:00
Marcel Peterkau 7596e47981 fix: preflight member records before rules 2026-06-21 17:54:09 +02:00
Marcel Peterkau 55bc3b666e fix: tolerate damaged contribution files 2026-06-21 17:50:56 +02:00
Marcel Peterkau 4bc1a8a200 feat: add scriptable housekeeper rule engine 2026-06-21 17:43:04 +02:00
Marcel Peterkau e63abbae81 fix: tolerate invalid member dates in views 2026-06-21 16:49:51 +02:00
Marcel Peterkau dfd5b1192b feat: initialize CCMA member administration 2026-06-21 16:46:15 +02:00