feat: add template creation timestamps

This commit is contained in:
Marcel Peterkau
2026-06-21 22:41:53 +02:00
parent c58072fe45
commit 3c842f29a3
7 changed files with 56 additions and 8 deletions
+5 -2
View File
@@ -80,12 +80,15 @@ Placeholders use `{{group.field}}`. Available values include:
`member.mandate_signed_at`, `member.mandate_revoked_at`, `member.mandate_signed_at`, `member.mandate_revoked_at`,
`member.mandate_active` `member.mandate_active`
- Claim: `claim.id`, `claim.title`, `claim.due_date`, `claim.total`, - Claim: `claim.id`, `claim.title`, `claim.due_date`, `claim.total`,
`claim.paid`, `claim.balance`, `claim.status`, `claim.items` `claim.created_date`, `claim.created_at`, `claim.paid`, `claim.balance`,
`claim.status`, `claim.items`
- Reminder: `reminder.id`, `reminder.level`, `reminder.name`, - Reminder: `reminder.id`, `reminder.level`, `reminder.name`,
`reminder.status`, `reminder.created_at`, `reminder.sent_at`, `reminder.status`, `reminder.created_at`, `reminder.sent_at`,
`reminder.payment_deadline`, `reminder.payment_deadline_days`, `reminder.payment_deadline`, `reminder.payment_deadline_days`,
`reminder.fee`, `reminder.detail`, `reminder.channel` `reminder.fee`, `reminder.detail`, `reminder.channel`
- Document: `document.date`, `document.datetime` - Document: `document.created_date`, `document.created_at`; compatibility
aliases: `document.date`, `document.datetime`, `current_date`,
`current_datetime`
- Organization: `organization.name`, `organization.street`, - Organization: `organization.name`, `organization.street`,
`organization.postal_code`, `organization.city`, `organization.country`, `organization.postal_code`, `organization.city`, `organization.country`,
`organization.address_line`, `organization.email`, `organization.phone`, `organization.address_line`, `organization.email`, `organization.phone`,
+1
View File
@@ -28,6 +28,7 @@
"OpenDocument-Templates mit Platzhaltern, lokaler PDF-Erzeugung, Audit-Verknüpfung und Dokumentöffnung aus der Mitgliederakte ergänzt.", "OpenDocument-Templates mit Platzhaltern, lokaler PDF-Erzeugung, Audit-Verknüpfung und Dokumentöffnung aus der Mitgliederakte ergänzt.",
"Zentrale Vereins- und Absenderdaten sowie getrennte Mitgliedsbereiche für Anschrift, Telefon und validierte Bank-/SEPA-Daten ergänzt.", "Zentrale Vereins- und Absenderdaten sowie getrennte Mitgliedsbereiche für Anschrift, Telefon und validierte Bank-/SEPA-Daten ergänzt.",
"Wiederholbare OpenDocument-Tabellenzeilen für beliebig viele Forderungspositionen eingeführt.", "Wiederholbare OpenDocument-Tabellenzeilen für beliebig viele Forderungspositionen eingeführt.",
"Eindeutige Templatefelder für Dokument-, aktuelle und Forderungs-Erstellungszeit ergänzt.",
"Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.", "Dropdowns zeigen deutsche Begriffe bei weiterhin englischen Speicher-Keys; der Hausmeisterstatus liegt einheitlich in housekeeper.json.",
"Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.", "Mehrstufiger Mahnworkflow mit Hausmeister-Regel, Entwurf, Versandbestätigung, Zahlungsfrist, optionaler Gebühr und Mahnsperre ergänzt.",
"Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.", "Splash-Screen auf das eingebettete CCMA-Hintergrundmotiv umgestellt und redundante Titeltexte entfernt.",
+1 -1
View File
@@ -8,7 +8,7 @@
<office:text> <office:text>
<text:p text:style-name="Title">{{organization.name}}</text:p> <text:p text:style-name="Title">{{organization.name}}</text:p>
<text:p>{{organization.address_line}} · {{organization.email}}</text:p> <text:p>{{organization.address_line}} · {{organization.email}}</text:p>
<text:p>Erstellt am {{document.date}}</text:p> <text:p>Erstellt am {{document.created_date}}</text:p>
<text:p text:style-name="Heading">Forderung: {{claim.title}}</text:p> <text:p text:style-name="Heading">Forderung: {{claim.title}}</text:p>
<text:p>Mitglied: {{member.full_name}} ({{member.number}})</text:p> <text:p>Mitglied: {{member.full_name}} ({{member.number}})</text:p>
<text:p>E-Mail: {{member.email}}</text:p> <text:p>E-Mail: {{member.email}}</text:p>
+1 -1
View File
@@ -8,7 +8,7 @@
<office:text> <office:text>
<text:p text:style-name="Title">{{organization.name}}</text:p> <text:p text:style-name="Title">{{organization.name}}</text:p>
<text:p>{{organization.address_line}} · {{organization.email}}</text:p> <text:p>{{organization.address_line}} · {{organization.email}}</text:p>
<text:p>{{document.date}}</text:p> <text:p>{{document.created_date}}</text:p>
<text:p>An {{member.full_name}} ({{member.number}})</text:p> <text:p>An {{member.full_name}} ({{member.number}})</text:p>
<text:p>{{member.address_line}}</text:p> <text:p>{{member.address_line}}</text:p>
<text:p text:style-name="Heading">{{reminder.name}}</text:p> <text:p text:style-name="Heading">{{reminder.name}}</text:p>
+1 -1
View File
@@ -7,7 +7,7 @@
<office:body> <office:body>
<office:text> <office:text>
<text:p text:style-name="Title">{{organization.name}}</text:p> <text:p text:style-name="Title">{{organization.name}}</text:p>
<text:p>Erstellt am {{document.date}}</text:p> <text:p>Erstellt am {{document.created_date}}</text:p>
<text:p text:style-name="Heading">Mitgliedsdaten</text:p> <text:p text:style-name="Heading">Mitgliedsdaten</text:p>
<text:p>Name: {{member.full_name}}</text:p> <text:p>Name: {{member.full_name}}</text:p>
<text:p>Mitgliedsnummer: {{member.number}}</text:p> <text:p>Mitgliedsnummer: {{member.number}}</text:p>
+22 -2
View File
@@ -182,6 +182,9 @@ def _template_values(
organization: dict | None = None, organization: dict | None = None,
) -> tuple[dict[str, str], dict[str, list[dict[str, str]]]]: ) -> tuple[dict[str, str], dict[str, list[dict[str, str]]]]:
organization = organization or {} 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( organization_address = " ".join(
part part
for part in ( for part in (
@@ -192,8 +195,12 @@ def _template_values(
if part if part
) )
values = { values = {
"document.date": format_date_for_display(date.today().isoformat()), "document.date": created_date,
"document.datetime": datetime.now().astimezone().strftime("%d.%m.%Y %H:%M"), "document.datetime": created_timestamp,
"document.created_date": created_date,
"document.created_at": created_timestamp,
"current_date": created_date,
"current_datetime": created_timestamp,
"member.id": member.member_id, "member.id": member.member_id,
"member.number": member.member_number, "member.number": member.member_number,
"member.first_name": member.first_name, "member.first_name": member.first_name,
@@ -246,6 +253,10 @@ def _template_values(
"claim.id": claim_id, "claim.id": claim_id,
"claim.title": str(claim.get("title", "")), "claim.title": str(claim.get("title", "")),
"claim.due_date": format_date_for_display(str(claim.get("due_date", ""))), "claim.due_date": format_date_for_display(str(claim.get("due_date", ""))),
"claim.created_date": _display_date_from_timestamp(
str(claim.get("created_at", ""))
),
"claim.created_at": _display_timestamp(str(claim.get("created_at", ""))),
"claim.total": f"{money_text(claim_total(claim))} EUR", "claim.total": f"{money_text(claim_total(claim))} EUR",
"claim.paid": f"{money_text(allocated_total(data, claim_id))} EUR", "claim.paid": f"{money_text(allocated_total(data, claim_id))} EUR",
"claim.balance": f"{money_text(claim_balance(data, claim))} EUR", "claim.balance": f"{money_text(claim_balance(data, claim))} EUR",
@@ -516,5 +527,14 @@ def _display_timestamp(value: str) -> str:
return value[:16] return value[:16]
def _display_date_from_timestamp(value: str) -> str:
if not value:
return ""
try:
return format_date_for_display(datetime.fromisoformat(value).date().isoformat())
except ValueError:
return value[:10]
def _local_name(tag: str) -> str: def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1] return tag.rsplit("}", 1)[-1]
+25 -1
View File
@@ -6,7 +6,13 @@ from xml.etree import ElementTree
import pytest import pytest
import ccma.services.documents as document_module import ccma.services.documents as document_module
from ccma.services.documents import DocumentError, DocumentService, _replace_xml_placeholders from ccma.domain.models import ContributionData, Member
from ccma.services.documents import (
DocumentError,
DocumentService,
_replace_xml_placeholders,
_template_values,
)
from ccma.services.housekeeper import Housekeeper from ccma.services.housekeeper import Housekeeper
from ccma.storage.repository import MemberRepository, RepositoryError from ccma.storage.repository import MemberRepository, RepositoryError
@@ -32,6 +38,24 @@ def test_unknown_placeholder_is_reported() -> None:
_replace_xml_placeholders(source, {}) _replace_xml_placeholders(source, {})
def test_document_and_claim_creation_time_placeholders() -> None:
member = Member("member-1", "CCMA-1", "Ada", "Lovelace")
claim = {
"claim_id": "claim-1",
"title": "Test",
"amount": "10.00",
"created_at": "2026-06-21T14:35:00+02:00",
}
data = ContributionData(claims=[claim])
values, _repeats = _template_values(member, data=data, claim=claim)
assert values["document.created_date"] == values["document.date"] == values["current_date"]
assert values["document.created_at"] == values["document.datetime"] == values["current_datetime"]
assert values["claim.created_date"] in {"21.06.2026", "2026-06-21"}
assert values["claim.created_at"] == "21.06.2026 14:35"
def test_claim_item_loop_clones_formatted_table_row() -> None: def test_claim_item_loop_clones_formatted_table_row() -> None:
source = b"""<office:document source = b"""<office:document
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"