Merge branch 'dev' into feature/read-only-store

dev gained the editable mail templates, which live in the store like everything
else -- so they follow the same read-only rules: the options dialog skips saving
them, save_mail_template() reports the store instead of a raw PermissionError,
and a store that cannot keep its own copy simply renders from the shipped
default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-28 21:26:40 +02:00
co-authored by Claude Opus 5
24 changed files with 2276 additions and 168 deletions
+8 -58
View File
@@ -24,7 +24,8 @@ from ccma.domain.contributions import (
reminder_items_total,
)
from ccma.domain.dates import format_date_for_display
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS, Member
from ccma.domain.models import Member
from ccma.domain.placeholders import base_placeholder_values
from ccma.storage.repository import MemberRepository
SUPPORTED_TEMPLATE_SUFFIXES = {".fodt", ".odt", ".ott"}
@@ -183,65 +184,14 @@ def _template_values(
reminder: dict | None = None,
organization: dict | None = None,
) -> tuple[dict[str, str], dict[str, list[dict[str, str]]]]:
organization = organization or {}
created_at = datetime.now().astimezone()
created_date = format_date_for_display(created_at.date().isoformat())
created_timestamp = created_at.strftime("%d.%m.%Y %H:%M")
organization_address = " ".join(
part
for part in (
str(organization.get("street", "")),
str(organization.get("postal_code", "")),
str(organization.get("city", "")),
)
if part
values = base_placeholder_values(member, organization, created_at=created_at)
values.update(
{
"document.created_date": values["current_date"],
"document.created_at": values["current_datetime"],
}
)
values = {
"document.created_date": created_date,
"document.created_at": created_timestamp,
"current_date": created_date,
"current_datetime": created_timestamp,
"member.id": member.member_id,
"member.number": member.member_number,
"member.first_name": member.first_name,
"member.last_name": member.last_name,
"member.nickname": member.nickname,
"member.full_name": member.display_name,
"member.email": member.email,
"member.phone": member.phone,
"member.birth_date": format_date_for_display(member.birth_date),
"member.status": MEMBERSHIP_STATUS_LABELS.get(member.status, member.status),
"member.accepted_at": format_date_for_display(member.accepted_at),
"member.started_at": format_date_for_display(member.membership_started_at),
"member.ended_at": format_date_for_display(member.membership_ended_at),
"member.street": member.street,
"member.address_addition": member.address_addition,
"member.postal_code": member.postal_code,
"member.city": member.city,
"member.country": member.country,
"member.address_line": " ".join(
part for part in (member.street, member.postal_code, member.city) if part
),
"member.account_holder": member.account_holder,
"member.iban": member.iban,
"member.bic": member.bic,
"member.mandate_reference": member.mandate_reference,
"member.mandate_signed_at": format_date_for_display(member.mandate_signed_at),
"member.mandate_revoked_at": format_date_for_display(member.mandate_revoked_at),
"member.mandate_active": "Ja" if member.mandate_active else "Nein",
"organization.name": str(organization.get("name", "")),
"organization.street": str(organization.get("street", "")),
"organization.postal_code": str(organization.get("postal_code", "")),
"organization.city": str(organization.get("city", "")),
"organization.country": str(organization.get("country", "")),
"organization.address_line": organization_address,
"organization.email": str(organization.get("email", "")),
"organization.phone": str(organization.get("phone", "")),
"organization.website": str(organization.get("website", "")),
"organization.iban": str(organization.get("iban", "")),
"organization.bic": str(organization.get("bic", "")),
"organization.creditor_id": str(organization.get("creditor_id", "")),
}
repeats: dict[str, list[dict[str, str]]] = {}
if claim is not None and data is not None:
claim_id = str(claim.get("claim_id", ""))