mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-09-06 03:20:49 +02:00
Once the direct debit bounced and the board sent the Rücklastschrift reminder,
the claim is expected as a transfer by the deadline that letter states. Three
places still treated it as a claim the mandate covers, and the housekeeper's was
the one the board kept running into: after the reminder's deadline lapsed, the
finding went back to "Lastschrift überfällig -- Einzug prüfen, eine postalische
Mahnung ist hier nicht vorgesehen", for a claim that had just been dunned.
The rule now asks whether the claim was dunned before treating it as one for the
direct debit. If it was, it continues in the ordinary dunning sequence: the
running deadline shows as the usual "Frist läuft noch" note, and once that has
passed the next dunning level comes due. The SEPA-specific pending-reminder
detour that used to cover the deadline window is gone with it -- the ordinary
path reports the same thing.
The SEPA run now skips a dunned claim as well, instead of quietly collecting the
money the letter asked the member to transfer (which can bounce a second time,
with a second fee). The skip is reported like the incomplete mandates are, so
nothing disappears from the run without saying why; the dialog's wording is no
longer specific to mandates.
And a dunning mail asks for a transfer even from a member with an active
mandate. The shipped template spells the bank details out, but the ready-made
{{payment.instructions}} paragraph, offered by the template editor for exactly
this mail, told them "wir ziehen den Betrag ein, du musst nichts weiter tun" --
in the letter demanding payment.
Reverting the sent reminder is what puts the claim back into the direct-debit
run; the read of "dunned" is a sent reminder, not a draft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
304 lines
12 KiB
Markdown
304 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.
|
||
|
||
An overdue claim of a member with an active mandate is not dunned but reported as
|
||
a direct debit to look into -- until a reminder for it has actually been sent. A
|
||
sent reminder (typically the "Rücklastschrift" preset for a bounced debit) takes
|
||
that claim off the direct-debit track for good: it is expected as a transfer by
|
||
the stated deadline, so the housekeeper continues it in the ordinary dunning
|
||
sequence, the SEPA run leaves it out and says so, and its dunning mail asks for a
|
||
transfer even though the mandate is still active. Reverting the sent reminder puts
|
||
the claim back into the direct-debit run.
|
||
|
||
Do not place a real member store inside the source repository.
|