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
2026-06-23 20:19:53 +02:00
2026-06-23 20:19:53 +02:00
2026-06-23 20:19:53 +02:00
2026-06-23 20:19:53 +02:00
2026-06-23 20:19:53 +02:00
2026-06-23 20:19:53 +02:00

CCMA - Chaotic Creature Member Administration

File-based member administration for Chaos Computer Club Mannheim e.V.

The member store remains readable without this application. Every member has a directory containing member.json, contributions.json, an append-only events.jsonl, and a files/ directory.

Development

Requires Python 3.11+ with Tk support. PDF generation from document templates additionally requires LibreOffice or OpenOffice with a soffice command available on the system.

Windows (PowerShell)

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\ccma.exe

For development tools and tests, install the dev extra as well:

.\.venv\Scripts\python.exe -m pip install -e .[dev]

Alternatively, without installation:

$env:PYTHONPATH = "src"
.\.venv\Scripts\python.exe -m ccma

Linux/macOS

python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/ccma

For development tools and tests, install the dev extra as well:

.venv/bin/pip install -e '.[dev]'

Alternatively, without installation:

PYTHONPATH=src python -m ccma

If LibreOffice is installed on Windows but soffice is not in PATH, CCMA also checks the default install locations under %PROGRAMFILES% and %PROGRAMFILES(X86)%.

Building standalone executables

CCMA uses the same PyInstaller-based release flow locally and in CI. The single-file executables are built from build/ccma.spec.

Windows standalone build

cd build
.\build.ps1

This creates two executables in the dist/ directory:

  • ccma.exe (generic)
  • ccma-<version>-windows-<arch>.exe (version and architecture-specific, e.g. ccma-0.1.0-windows-amd64.exe)

The build script automatically:

  • Creates a venv if missing
  • Installs dependencies and PyInstaller
  • Generates a Windows icon from the CCMA icon
  • Runs PyInstaller to bundle the application

Linux standalone build

python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
pip install -r requirements.txt
pip install pyinstaller
python -m PyInstaller --noconfirm --clean build/ccma.spec

This creates dist/ccma. In CI, release builds additionally rename artifacts to ccma-<version>-linux-<arch> and ccma-<version>-windows-<arch>.exe.

CI release builds

The Gitea workflows use the central ci-templates submodule and ci-config.yaml to run PR checks, Linux smoke builds, and release packaging for Linux amd64, Linux arm64, and Windows amd64. A release build is skipped when the commit message contains [nobuild].

On first start, select or create the central member-store directory. The VERSION file is the single source for application and package versions.

The splash screen remains visible for at least five seconds by default. This advanced setting is intentionally not exposed in the options dialog. It can be changed directly in config.json, including disabling the minimum with 0:

"splash_minimum_seconds": 0

Store layout

member-store/
├── repository.json
├── housekeeper.json
├── rules/
├── templates/
│   ├── Forderung mit Positionen.fodt
│   ├── Mahnung.fodt
│   └── Mitglied.fodt
└── members/
    └── <uuid>/
        ├── member.json
        ├── contributions.json
        ├── events.jsonl
        └── files/

Document templates

CCMA reads OpenDocument templates (.fodt, .odt, or .ott) from the store's templates/ directory. The three initial .fodt files are editable with LibreOffice/OpenOffice and are copied only when their filename does not already exist. PDF files are generated locally and stored below the member's files/documents/ directory. No office document content is sent to an external service.

Placeholders use {{group.field}}. Available values include:

  • Member: member.number, member.first_name, member.last_name, member.full_name, member.email, member.birth_date, member.status, member.accepted_at, member.started_at, member.phone, member.street, member.address_addition, member.postal_code, member.city, member.country, member.address_line, member.account_holder, member.iban, member.bic, member.mandate_reference, member.mandate_signed_at, member.mandate_revoked_at, member.mandate_active
  • Claim: claim.id, claim.title, claim.due_date, claim.total, claim.created_date, claim.created_at, claim.paid, claim.balance, claim.status, claim.items
  • Reminder: reminder.id, reminder.level, reminder.name, reminder.status, reminder.created_at, reminder.sent_at, reminder.payment_deadline, reminder.payment_deadline_days, reminder.fee, reminder.detail, reminder.channel
  • Document: document.created_date, document.created_at
  • Current time: current_date, current_datetime
  • Organization: organization.name, organization.street, organization.postal_code, organization.city, organization.country, organization.address_line, organization.email, organization.phone, organization.website, organization.iban, organization.bic, organization.creditor_id

Claim placeholders are available from a claim tab. Reminder placeholders are available when a reminder row is selected before opening the document dialog. Unknown or unavailable placeholders stop generation with a clear error rather than producing an incomplete letter.

To repeat a formatted table row for every claim item, place both loop markers inside the same template row:

{{#claim.items}}
{{item.description}} | {{item.type}} | {{item.quantity}} | {{item.unit_price}} | {{item.amount}}
{{/claim.items}}

The opening marker may share the first cell with its value and the closing marker may share the last cell. CCMA removes both markers and clones the whole row, including its formatting, once per item. With no items, the template row is removed. A loop that is not closed in the same row is rejected.

Housekeeper rules

The housekeeper runs every rule for every member. Built-in Python rules live in ccma/rules/scripts/. A member store can add rules in its rules/ directory. If a store rule has the same filename as a built-in rule, the store version replaces the built-in version.

Store rules are trusted executable Python code. Only place reviewed rules from trusted sources in this directory. Rules return structured RuleAction objects; CCMA performs all file writes, duplicate checks, audit events, and atomic updates.

housekeeper.json is written only after a complete run. Each refreshed task gets the pending run ID. A failed run therefore cannot advance the stored counter or silently resolve existing tasks.

Claims and payments

Claims are stored in the member's contributions.json. A claim consists of signed line items; fees increase and credits reduce its total. Payments remain separate records and allocations connect one payment to one or more claims. This supports partial payments, shared annual payments, unallocated credit, and optional GnuCash transaction IDs without duplicating a bank transaction.

Claim status and outstanding balance are derived from line items and payment allocations. Reminders are separate processes and only change the amount when they explicitly add a fee line item. Every change is also appended to the member's events.jsonl audit trail.

Overdue claims are evaluated by the reminder rule. It creates housekeeper tasks for the next configured reminder level. Reminder drafts do not change a claim; only confirming actual dispatch starts the new payment deadline and adds an optional fee line item. A claim-level dunning hold suppresses automatic and manual reminder preparation until it is removed or expires.

Do not place a real member store inside the source repository.

S
Description
No description provided
Readme MIT
5 MiB
Languages
Python 95.3%
Tcl 4.3%
PowerShell 0.4%