The club has to keep its member data current, and until now that meant writing
to each member by hand. "Datenüberprüfung anfragen" in the members tab sends one
mail per member, each listing that member's own record: number, name, nickname,
birth date, contact data, address, status, member since, payment frequency, and
the bank details only for members who have any.
Two decisions the record itself forced:
A field with no value is printed as "(nicht hinterlegt)" rather than left out.
The point of the mail is to have gaps filled in, and a missing line is a gap
nobody sees.
The IBAN is masked down to its country code and last four digits. That is enough
to recognise the account, and it keeps a full account number out of a mail the
club sends to dozens of people at once.
Every member is listed as a recipient, with the live memberships that have an
address preselected -- a member who resigned at year's end may still need to
confirm their address, so the board can add them by hand. Members without an
address are skipped and reported instead of failing the run.
Delivery reuses the existing mail machinery: the configured delivery mode, one
SMTP/IMAP connection for the whole run, an archive copy in the member file, a
"data_review_email_sent" event, and the read-only guard before anything is
rendered. A member whose mail fails is reported as a warning and the run
continues -- one bad address must not stop a mailing to the whole club halfway
through. Because the run cannot be taken back, the recipient count is confirmed
once more before it starts.
Subject and text come from a new "Datenüberprüfung" template, editable like the
others, with {{data.sheet}} for the whole record and a {{#data}} block for a
layout of the board's own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
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.
Mail templates
Every e-mail CCMA sends -- the welcome mail with its first invoice, dunning mails,
SEPA pre-notifications and the data-review request -- is rendered from a plain text
template. The shipped
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; a deleted one is restored from the shipped default.
A template holds the subject in its first line and the message body after a blank line:
Betreff: {{reminder.name}} – {{claim.title}}
Hallo {{member.first_name}},
...
Placeholders use the same {{ ... }} syntax as the document templates. Which ones
are available depends on the mail; the options dialog lists them per template and
rejects unknown ones when saving. A line that contains nothing but placeholders
which render empty (an optional hint, an empty list) is dropped, and
{{#claims}} ... {{/claims}} repeats its content once per claim.
Data-review mailing
"Datenüberprüfung anfragen" in the members tab asks members to check the data the club stores about them. Every member appears in the recipient list; preselected are the live memberships (accepted, active, suspended, resigned at year's end, honorary) that have an e-mail address. Members without one are skipped and reported.
Each recipient gets one mail listing their own record -- number, name, nickname,
birth date, contact data, address, status, member since, payment frequency, and the
bank details only for members who have any. A field with no value is printed as
(nicht hinterlegt) so the gap is visible, and the IBAN is masked down to its
country code and last four digits, which is enough to recognise the account.
The text comes from the Datenüberprüfung template. Besides the usual member and
organization placeholders it offers {{data.sheet}} (the whole record, one field per
line), {{data.count}}, {{member.iban_masked}}, and the repeat block
{{#data}}{{field.label}}: {{field.value}}{{/data}} for a custom layout.
Delivery follows the configured e-mail mode (local .eml files, direct send, or
IMAP drafts); the run is confirmed once more with the recipient count because it
cannot be taken back. Every mail is archived below the member's
files/documents/Datenpruefung/ directory and logged as a data_review_email_sent
event. A member whose mail fails is reported as a warning and the run continues with
the rest.
Read-only stores
The member store may be mounted read-only -- the encrypted volume holding the member data does not have to be writable to look something up. CCMA detects this at startup by probing the store with an actual write, skips the housekeeper (every one of its passes writes) and opens in a read-only session: a permanent warning banner above the tabs, a "NUR LESEN" marker in the window title and status bar, and every write refused with one clear message instead of an operating system error.
Program settings still save normally -- they live in the user's config directory, not in the store. Settings that belong to the store (club data, member numbers, reminders, e-mail, mail templates) are skipped with a notice. When the volume is remounted with write access, "Erneut prüfen" in the banner picks that up without a restart: title, status bar and banner drop their markers, and the housekeeper pass that was skipped at startup is offered right away.
A store that was never initialized cannot be opened read-only: creating it needs write access, and CCMA says so instead of failing obscurely.
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.