mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
A data-review mail that lists the payment frequency but not how to pay leaves the
board matching anonymous transfers by hand. The mail now closes with the
reference to quote -- member number and full name, joined only where both exist
so a missing half cannot leave a dangling dash. Both are offered because either
identifies the payment on its own: a member who has forgotten their number falls
back on the name, and two members sharing a name are told apart by the number.
The note about the shortened IBAN moved out of the fixed template text into
{{data.iban_hint}}, filled only for members whose bank details are actually
listed -- it used to explain a masked IBAN to members who pay by transfer and see
no IBAN at all. It sits directly under the record as a parenthesised footnote,
which also keeps the dropped line from leaving a blank one behind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
295 lines
12 KiB
Markdown
295 lines
12 KiB
Markdown
# 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)
|
||
|
||
```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:
|
||
|
||
```powershell
|
||
.\.venv\Scripts\python.exe -m pip install -e .[dev]
|
||
```
|
||
|
||
Alternatively, without installation:
|
||
|
||
```powershell
|
||
$env:PYTHONPATH = "src"
|
||
.\.venv\Scripts\python.exe -m ccma
|
||
```
|
||
|
||
### Linux/macOS
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
.venv/bin/pip install -e '.[dev]'
|
||
```
|
||
|
||
Alternatively, without installation:
|
||
|
||
```bash
|
||
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
|
||
|
||
```powershell
|
||
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
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```json
|
||
"splash_minimum_seconds": 0
|
||
```
|
||
|
||
## Store layout
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
{{#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
|
||
mail closes with the reference to put on a transfer -- member number and full name,
|
||
either of which identifies the payment on its own.
|
||
|
||
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}}`, `{{payment.reference}}` (the
|
||
suggested transfer reference), `{{data.iban_hint}}` (the note about the shortened
|
||
IBAN, filled only for members whose bank details are listed) 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.
|