mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 03:04:52 +02:00
feat: add member address and SEPA data
This commit is contained in:
@@ -73,7 +73,12 @@ Placeholders use `{{group.field}}`. Available values include:
|
|||||||
|
|
||||||
- Member: `member.number`, `member.first_name`, `member.last_name`,
|
- Member: `member.number`, `member.first_name`, `member.last_name`,
|
||||||
`member.full_name`, `member.email`, `member.birth_date`, `member.status`,
|
`member.full_name`, `member.email`, `member.birth_date`, `member.status`,
|
||||||
`member.accepted_at`, `member.started_at`
|
`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: `claim.id`, `claim.title`, `claim.due_date`, `claim.total`,
|
||||||
`claim.paid`, `claim.balance`, `claim.status`, `claim.items`
|
`claim.paid`, `claim.balance`, `claim.status`, `claim.items`
|
||||||
- Reminder: `reminder.id`, `reminder.level`, `reminder.name`,
|
- Reminder: `reminder.id`, `reminder.level`, `reminder.name`,
|
||||||
@@ -81,6 +86,11 @@ Placeholders use `{{group.field}}`. Available values include:
|
|||||||
`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.date`, `document.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
|
Claim placeholders are available from a claim tab. Reminder placeholders are
|
||||||
available when a reminder row is selected before opening the document dialog.
|
available when a reminder row is selected before opening the document dialog.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"Forderungen besitzen eigene Arbeitsansichten mit Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
"Forderungen besitzen eigene Arbeitsansichten mit Teilzahlungen, GnuCash-Referenzen, Zahlungszuordnungen, Mahnungen und Gebühren.",
|
||||||
"Positionen, Zahlungen und Mahnungen einer Forderung werden gemeinsam in einer farblich gruppierten Übersicht dargestellt.",
|
"Positionen, Zahlungen und Mahnungen einer Forderung werden gemeinsam in einer farblich gruppierten Übersicht dargestellt.",
|
||||||
"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.",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -6,17 +6,20 @@
|
|||||||
</office:styles>
|
</office:styles>
|
||||||
<office:body>
|
<office:body>
|
||||||
<office:text>
|
<office:text>
|
||||||
<text:p text:style-name="Title">Chaos Computer Club Mannheim e.V.</text:p>
|
<text:p text:style-name="Title">{{organization.name}}</text:p>
|
||||||
|
<text:p>{{organization.address_line}} · {{organization.email}}</text:p>
|
||||||
<text:p>Erstellt am {{document.date}}</text:p>
|
<text:p>Erstellt am {{document.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>
|
||||||
|
<text:p>Anschrift: {{member.address_line}}</text:p>
|
||||||
<text:p>Fällig am: {{claim.due_date}}</text:p>
|
<text:p>Fällig am: {{claim.due_date}}</text:p>
|
||||||
<text:p text:style-name="Heading">Positionen</text:p>
|
<text:p text:style-name="Heading">Positionen</text:p>
|
||||||
<text:p>{{claim.items}}</text:p>
|
<text:p>{{claim.items}}</text:p>
|
||||||
<text:p>Gesamtbetrag: {{claim.total}}</text:p>
|
<text:p>Gesamtbetrag: {{claim.total}}</text:p>
|
||||||
<text:p>Bereits bezahlt: {{claim.paid}}</text:p>
|
<text:p>Bereits bezahlt: {{claim.paid}}</text:p>
|
||||||
<text:p text:style-name="Heading">Offener Betrag: {{claim.balance}}</text:p>
|
<text:p text:style-name="Heading">Offener Betrag: {{claim.balance}}</text:p>
|
||||||
|
<text:p>Zahlung an {{organization.iban}} · BIC {{organization.bic}}</text:p>
|
||||||
<text:p>Forderungs-ID: {{claim.id}}</text:p>
|
<text:p>Forderungs-ID: {{claim.id}}</text:p>
|
||||||
</office:text>
|
</office:text>
|
||||||
</office:body>
|
</office:body>
|
||||||
|
|||||||
@@ -6,13 +6,16 @@
|
|||||||
</office:styles>
|
</office:styles>
|
||||||
<office:body>
|
<office:body>
|
||||||
<office:text>
|
<office:text>
|
||||||
<text:p text:style-name="Title">Chaos Computer Club Mannheim e.V.</text:p>
|
<text:p text:style-name="Title">{{organization.name}}</text:p>
|
||||||
|
<text:p>{{organization.address_line}} · {{organization.email}}</text:p>
|
||||||
<text:p>{{document.date}}</text:p>
|
<text:p>{{document.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 text:style-name="Heading">{{reminder.name}}</text:p>
|
<text:p text:style-name="Heading">{{reminder.name}}</text:p>
|
||||||
<text:p>Zu unserer Forderung „{{claim.title}}“ ist weiterhin ein Betrag von {{claim.balance}} offen.</text:p>
|
<text:p>Zu unserer Forderung „{{claim.title}}“ ist weiterhin ein Betrag von {{claim.balance}} offen.</text:p>
|
||||||
<text:p>Bitte begleiche den offenen Betrag innerhalb von {{reminder.payment_deadline_days}} Tagen.</text:p>
|
<text:p>Bitte begleiche den offenen Betrag innerhalb von {{reminder.payment_deadline_days}} Tagen.</text:p>
|
||||||
<text:p>Mahngebühr: {{reminder.fee}}</text:p>
|
<text:p>Mahngebühr: {{reminder.fee}}</text:p>
|
||||||
|
<text:p>Zahlung an {{organization.iban}} · BIC {{organization.bic}}</text:p>
|
||||||
<text:p>{{reminder.detail}}</text:p>
|
<text:p>{{reminder.detail}}</text:p>
|
||||||
<text:p>Forderungs-ID: {{claim.id}}</text:p>
|
<text:p>Forderungs-ID: {{claim.id}}</text:p>
|
||||||
<text:p>Mahnstufe: {{reminder.level}}</text:p>
|
<text:p>Mahnstufe: {{reminder.level}}</text:p>
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
</office:styles>
|
</office:styles>
|
||||||
<office:body>
|
<office:body>
|
||||||
<office:text>
|
<office:text>
|
||||||
<text:p text:style-name="Title">Chaos Computer Club Mannheim e.V.</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.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>
|
||||||
<text:p>E-Mail: {{member.email}}</text:p>
|
<text:p>E-Mail: {{member.email}}</text:p>
|
||||||
|
<text:p>Telefon: {{member.phone}}</text:p>
|
||||||
|
<text:p>Anschrift: {{member.address_line}}</text:p>
|
||||||
<text:p>Geburtsdatum: {{member.birth_date}}</text:p>
|
<text:p>Geburtsdatum: {{member.birth_date}}</text:p>
|
||||||
<text:p>Mitglied seit: {{member.started_at}}</text:p>
|
<text:p>Mitglied seit: {{member.started_at}}</text:p>
|
||||||
<text:p>Status: {{member.status}}</text:p>
|
<text:p>Status: {{member.status}}</text:p>
|
||||||
|
|||||||
@@ -28,7 +28,20 @@ class Member:
|
|||||||
first_name: str
|
first_name: str
|
||||||
last_name: str
|
last_name: str
|
||||||
email: str = ""
|
email: str = ""
|
||||||
|
phone: str = ""
|
||||||
birth_date: str = ""
|
birth_date: str = ""
|
||||||
|
street: str = ""
|
||||||
|
address_addition: str = ""
|
||||||
|
postal_code: str = ""
|
||||||
|
city: str = ""
|
||||||
|
country: str = "Deutschland"
|
||||||
|
account_holder: str = ""
|
||||||
|
iban: str = ""
|
||||||
|
bic: str = ""
|
||||||
|
mandate_reference: str = ""
|
||||||
|
mandate_signed_at: str = ""
|
||||||
|
mandate_active: bool = False
|
||||||
|
mandate_revoked_at: str = ""
|
||||||
status: str = "application"
|
status: str = "application"
|
||||||
accepted_at: str = ""
|
accepted_at: str = ""
|
||||||
membership_started_at: str = ""
|
membership_started_at: str = ""
|
||||||
@@ -54,6 +67,23 @@ class Member:
|
|||||||
"last_name": self.last_name,
|
"last_name": self.last_name,
|
||||||
"birth_date": self.birth_date,
|
"birth_date": self.birth_date,
|
||||||
"email": self.email,
|
"email": self.email,
|
||||||
|
"phone": self.phone,
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"street": self.street,
|
||||||
|
"addition": self.address_addition,
|
||||||
|
"postal_code": self.postal_code,
|
||||||
|
"city": self.city,
|
||||||
|
"country": self.country,
|
||||||
|
},
|
||||||
|
"banking": {
|
||||||
|
"account_holder": self.account_holder,
|
||||||
|
"iban": self.iban,
|
||||||
|
"bic": self.bic,
|
||||||
|
"mandate_reference": self.mandate_reference,
|
||||||
|
"mandate_signed_at": self.mandate_signed_at,
|
||||||
|
"mandate_active": self.mandate_active,
|
||||||
|
"mandate_revoked_at": self.mandate_revoked_at,
|
||||||
},
|
},
|
||||||
"membership": {
|
"membership": {
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
@@ -75,6 +105,8 @@ class Member:
|
|||||||
person = data.get("person") or {}
|
person = data.get("person") or {}
|
||||||
membership = data.get("membership") or {}
|
membership = data.get("membership") or {}
|
||||||
contribution = data.get("contribution_profile") or {}
|
contribution = data.get("contribution_profile") or {}
|
||||||
|
address = data.get("address") or {}
|
||||||
|
banking = data.get("banking") or {}
|
||||||
return cls(
|
return cls(
|
||||||
schema_version=int(data.get("schema_version", 1)),
|
schema_version=int(data.get("schema_version", 1)),
|
||||||
member_id=str(data["member_id"]),
|
member_id=str(data["member_id"]),
|
||||||
@@ -82,7 +114,20 @@ class Member:
|
|||||||
first_name=str(person.get("first_name", "")),
|
first_name=str(person.get("first_name", "")),
|
||||||
last_name=str(person.get("last_name", "")),
|
last_name=str(person.get("last_name", "")),
|
||||||
email=str(person.get("email", "")),
|
email=str(person.get("email", "")),
|
||||||
|
phone=str(person.get("phone", "")),
|
||||||
birth_date=str(person.get("birth_date", "")),
|
birth_date=str(person.get("birth_date", "")),
|
||||||
|
street=str(address.get("street", "")),
|
||||||
|
address_addition=str(address.get("addition", "")),
|
||||||
|
postal_code=str(address.get("postal_code", "")),
|
||||||
|
city=str(address.get("city", "")),
|
||||||
|
country=str(address.get("country", "Deutschland")),
|
||||||
|
account_holder=str(banking.get("account_holder", "")),
|
||||||
|
iban=str(banking.get("iban", "")),
|
||||||
|
bic=str(banking.get("bic", "")),
|
||||||
|
mandate_reference=str(banking.get("mandate_reference", "")),
|
||||||
|
mandate_signed_at=str(banking.get("mandate_signed_at", "")),
|
||||||
|
mandate_active=bool(banking.get("mandate_active", False)),
|
||||||
|
mandate_revoked_at=str(banking.get("mandate_revoked_at", "")),
|
||||||
status=str(membership.get("status", "application")),
|
status=str(membership.get("status", "application")),
|
||||||
accepted_at=str(membership.get("accepted_at", "")),
|
accepted_at=str(membership.get("accepted_at", "")),
|
||||||
membership_started_at=str(membership.get("started_at", "")),
|
membership_started_at=str(membership.get("started_at", "")),
|
||||||
|
|||||||
@@ -116,7 +116,16 @@ class DocumentService:
|
|||||||
if reminder is None:
|
if reminder is None:
|
||||||
raise DocumentError("Die ausgewählte Mahnung wurde nicht gefunden.")
|
raise DocumentError("Die ausgewählte Mahnung wurde nicht gefunden.")
|
||||||
|
|
||||||
values = _template_values(member, data=data, claim=claim, reminder=reminder)
|
organization = self.repository.get_configuration().get("organization") or {}
|
||||||
|
if not isinstance(organization, dict):
|
||||||
|
organization = {}
|
||||||
|
values = _template_values(
|
||||||
|
member,
|
||||||
|
data=data,
|
||||||
|
claim=claim,
|
||||||
|
reminder=reminder,
|
||||||
|
organization=organization,
|
||||||
|
)
|
||||||
destination_dir = self.repository.members_root / member_id / "files" / "documents"
|
destination_dir = self.repository.members_root / member_id / "files" / "documents"
|
||||||
destination_dir.mkdir(parents=True, exist_ok=True)
|
destination_dir.mkdir(parents=True, exist_ok=True)
|
||||||
destination = _available_path(destination_dir, _safe_pdf_name(output_name))
|
destination = _available_path(destination_dir, _safe_pdf_name(output_name))
|
||||||
@@ -168,7 +177,18 @@ def _template_values(
|
|||||||
data=None,
|
data=None,
|
||||||
claim: dict | None = None,
|
claim: dict | None = None,
|
||||||
reminder: dict | None = None,
|
reminder: dict | None = None,
|
||||||
|
organization: dict | None = None,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
|
organization = organization or {}
|
||||||
|
organization_address = " ".join(
|
||||||
|
part
|
||||||
|
for part in (
|
||||||
|
str(organization.get("street", "")),
|
||||||
|
str(organization.get("postal_code", "")),
|
||||||
|
str(organization.get("city", "")),
|
||||||
|
)
|
||||||
|
if part
|
||||||
|
)
|
||||||
values = {
|
values = {
|
||||||
"document.date": format_date_for_display(date.today().isoformat()),
|
"document.date": format_date_for_display(date.today().isoformat()),
|
||||||
"document.datetime": datetime.now().astimezone().strftime("%d.%m.%Y %H:%M"),
|
"document.datetime": datetime.now().astimezone().strftime("%d.%m.%Y %H:%M"),
|
||||||
@@ -178,10 +198,38 @@ def _template_values(
|
|||||||
"member.last_name": member.last_name,
|
"member.last_name": member.last_name,
|
||||||
"member.full_name": member.display_name,
|
"member.full_name": member.display_name,
|
||||||
"member.email": member.email,
|
"member.email": member.email,
|
||||||
|
"member.phone": member.phone,
|
||||||
"member.birth_date": format_date_for_display(member.birth_date),
|
"member.birth_date": format_date_for_display(member.birth_date),
|
||||||
"member.status": MEMBERSHIP_STATUS_LABELS.get(member.status, member.status),
|
"member.status": MEMBERSHIP_STATUS_LABELS.get(member.status, member.status),
|
||||||
"member.accepted_at": format_date_for_display(member.accepted_at),
|
"member.accepted_at": format_date_for_display(member.accepted_at),
|
||||||
"member.started_at": format_date_for_display(member.membership_started_at),
|
"member.started_at": format_date_for_display(member.membership_started_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", "")),
|
||||||
}
|
}
|
||||||
if claim is not None and data is not None:
|
if claim is not None and data is not None:
|
||||||
claim_id = str(claim.get("claim_id", ""))
|
claim_id = str(claim.get("claim_id", ""))
|
||||||
|
|||||||
@@ -32,7 +32,19 @@ DEFAULT_MEMBER_NUMBER_PATTERN = "CCMA-{number:04d}"
|
|||||||
|
|
||||||
DEFAULT_CONFIGURATION = {
|
DEFAULT_CONFIGURATION = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"organization": "Chaos Computer Club Mannheim e.V.",
|
"organization": {
|
||||||
|
"name": "Chaos Computer Club Mannheim e.V.",
|
||||||
|
"street": "",
|
||||||
|
"postal_code": "",
|
||||||
|
"city": "Mannheim",
|
||||||
|
"country": "Deutschland",
|
||||||
|
"email": "",
|
||||||
|
"phone": "",
|
||||||
|
"website": "",
|
||||||
|
"iban": "",
|
||||||
|
"bic": "",
|
||||||
|
"creditor_id": "",
|
||||||
|
},
|
||||||
"member_number_policy": {
|
"member_number_policy": {
|
||||||
"mode": "automatic",
|
"mode": "automatic",
|
||||||
"pattern": DEFAULT_MEMBER_NUMBER_PATTERN,
|
"pattern": DEFAULT_MEMBER_NUMBER_PATTERN,
|
||||||
@@ -165,7 +177,7 @@ class MemberRepository:
|
|||||||
raw = read_json(path)
|
raw = read_json(path)
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
raise TypeError("Wurzelelement muss ein JSON-Objekt sein")
|
raise TypeError("Wurzelelement muss ein JSON-Objekt sein")
|
||||||
for section_name in ("person", "membership", "contribution_profile"):
|
for section_name in ("person", "address", "banking", "membership", "contribution_profile"):
|
||||||
if section_name in raw and not isinstance(raw[section_name], dict):
|
if section_name in raw and not isinstance(raw[section_name], dict):
|
||||||
raise TypeError(f"{section_name} muss ein JSON-Objekt sein")
|
raise TypeError(f"{section_name} muss ein JSON-Objekt sein")
|
||||||
return Member.from_dict(raw)
|
return Member.from_dict(raw)
|
||||||
@@ -193,6 +205,7 @@ class MemberRepository:
|
|||||||
first_name: str,
|
first_name: str,
|
||||||
last_name: str,
|
last_name: str,
|
||||||
email: str = "",
|
email: str = "",
|
||||||
|
phone: str = "",
|
||||||
birth_date: str = "",
|
birth_date: str = "",
|
||||||
member_number: str = "",
|
member_number: str = "",
|
||||||
) -> Member:
|
) -> Member:
|
||||||
@@ -221,6 +234,7 @@ class MemberRepository:
|
|||||||
first_name=first_name.strip(),
|
first_name=first_name.strip(),
|
||||||
last_name=last_name.strip(),
|
last_name=last_name.strip(),
|
||||||
email=email.strip(),
|
email=email.strip(),
|
||||||
|
phone=phone.strip(),
|
||||||
birth_date=birth_date,
|
birth_date=birth_date,
|
||||||
)
|
)
|
||||||
write_json_atomic(directory / "member.json", member.to_dict())
|
write_json_atomic(directory / "member.json", member.to_dict())
|
||||||
@@ -240,6 +254,10 @@ class MemberRepository:
|
|||||||
member.birth_date = normalize_date_input(member.birth_date, "Geburtsdatum")
|
member.birth_date = normalize_date_input(member.birth_date, "Geburtsdatum")
|
||||||
member.accepted_at = normalize_date_input(member.accepted_at, "Aufnahmebeschluss")
|
member.accepted_at = normalize_date_input(member.accepted_at, "Aufnahmebeschluss")
|
||||||
member.membership_started_at = normalize_date_input(member.membership_started_at, "Mitglied seit")
|
member.membership_started_at = normalize_date_input(member.membership_started_at, "Mitglied seit")
|
||||||
|
member.mandate_signed_at = normalize_date_input(member.mandate_signed_at, "Mandat erteilt am")
|
||||||
|
member.mandate_revoked_at = normalize_date_input(
|
||||||
|
member.mandate_revoked_at, "Mandat widerrufen am"
|
||||||
|
)
|
||||||
validate_member_dates(
|
validate_member_dates(
|
||||||
birth_date=member.birth_date,
|
birth_date=member.birth_date,
|
||||||
accepted_at=member.accepted_at,
|
accepted_at=member.accepted_at,
|
||||||
@@ -247,6 +265,16 @@ class MemberRepository:
|
|||||||
)
|
)
|
||||||
except DateValidationError as exc:
|
except DateValidationError as exc:
|
||||||
raise RepositoryError(str(exc)) from exc
|
raise RepositoryError(str(exc)) from exc
|
||||||
|
member.iban = normalize_iban(member.iban)
|
||||||
|
member.bic = member.bic.replace(" ", "").upper()
|
||||||
|
validate_iban(member.iban)
|
||||||
|
validate_bic(member.bic)
|
||||||
|
if member.mandate_active and not (
|
||||||
|
member.iban and member.mandate_reference.strip() and member.mandate_signed_at
|
||||||
|
):
|
||||||
|
raise RepositoryError(
|
||||||
|
"Ein aktives Lastschriftmandat benötigt IBAN, Mandatsreferenz und Erteilungsdatum."
|
||||||
|
)
|
||||||
if member.member_number != existing.member_number:
|
if member.member_number != existing.member_number:
|
||||||
self._assert_member_number_available(member.member_number, exclude_member_id=member.member_id)
|
self._assert_member_number_available(member.member_number, exclude_member_id=member.member_id)
|
||||||
changes = self._summarize_changes(existing, member)
|
changes = self._summarize_changes(existing, member)
|
||||||
@@ -721,6 +749,10 @@ class MemberRepository:
|
|||||||
member.last_name,
|
member.last_name,
|
||||||
member.display_name,
|
member.display_name,
|
||||||
member.email,
|
member.email,
|
||||||
|
member.phone,
|
||||||
|
member.street,
|
||||||
|
member.postal_code,
|
||||||
|
member.city,
|
||||||
member.birth_date,
|
member.birth_date,
|
||||||
_german_date(member.birth_date),
|
_german_date(member.birth_date),
|
||||||
]
|
]
|
||||||
@@ -767,6 +799,18 @@ class MemberRepository:
|
|||||||
config.setdefault("member_number_sequences", {})
|
config.setdefault("member_number_sequences", {})
|
||||||
write_json_atomic(self.root / "repository.json", config)
|
write_json_atomic(self.root / "repository.json", config)
|
||||||
|
|
||||||
|
def save_organization(self, values: dict[str, str]) -> None:
|
||||||
|
organization = {key: str(value).strip() for key, value in values.items()}
|
||||||
|
organization["iban"] = normalize_iban(organization.get("iban", ""))
|
||||||
|
organization["bic"] = organization.get("bic", "").replace(" ", "").upper()
|
||||||
|
validate_iban(organization["iban"])
|
||||||
|
validate_bic(organization["bic"])
|
||||||
|
if not organization.get("name"):
|
||||||
|
raise RepositoryError("Der Vereinsname ist erforderlich.")
|
||||||
|
config = self.get_configuration()
|
||||||
|
config["organization"] = organization
|
||||||
|
write_json_atomic(self.root / "repository.json", config)
|
||||||
|
|
||||||
def preview_member_number(self, pattern: str | None = None) -> str:
|
def preview_member_number(self, pattern: str | None = None) -> str:
|
||||||
selected_pattern = pattern or self.get_member_number_policy()["pattern"]
|
selected_pattern = pattern or self.get_member_number_policy()["pattern"]
|
||||||
validate_member_number_pattern(selected_pattern)
|
validate_member_number_pattern(selected_pattern)
|
||||||
@@ -832,12 +876,25 @@ class MemberRepository:
|
|||||||
"first_name": "Vorname",
|
"first_name": "Vorname",
|
||||||
"last_name": "Nachname",
|
"last_name": "Nachname",
|
||||||
"email": "E-Mail-Adresse",
|
"email": "E-Mail-Adresse",
|
||||||
|
"phone": "Telefonnummer",
|
||||||
"birth_date": "Geburtsdatum",
|
"birth_date": "Geburtsdatum",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"payment_frequency": "Zahlungsweise",
|
"payment_frequency": "Zahlungsweise",
|
||||||
"contribution_rule_id": "Beitragsregel",
|
"contribution_rule_id": "Beitragsregel",
|
||||||
"honorary": "Ehrenmitgliedschaft",
|
"honorary": "Ehrenmitgliedschaft",
|
||||||
"notes": "interne Notiz",
|
"notes": "interne Notiz",
|
||||||
|
"street": "Anschrift",
|
||||||
|
"address_addition": "Adresszusatz",
|
||||||
|
"postal_code": "Postleitzahl",
|
||||||
|
"city": "Ort",
|
||||||
|
"country": "Land",
|
||||||
|
"account_holder": "Kontoinhaber",
|
||||||
|
"iban": "IBAN",
|
||||||
|
"bic": "BIC",
|
||||||
|
"mandate_reference": "SEPA-Mandatsreferenz",
|
||||||
|
"mandate_signed_at": "Mandat erteilt am",
|
||||||
|
"mandate_active": "Lastschriftmandat aktiv",
|
||||||
|
"mandate_revoked_at": "Mandat widerrufen am",
|
||||||
}
|
}
|
||||||
changes: list[str] = []
|
changes: list[str] = []
|
||||||
for field, label in labels.items():
|
for field, label in labels.items():
|
||||||
@@ -854,6 +911,29 @@ class MemberRepository:
|
|||||||
return changes
|
return changes
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_iban(value: str) -> str:
|
||||||
|
return "".join(value.split()).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_iban(value: str) -> None:
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if not 15 <= len(value) <= 34 or not value[:2].isalpha() or not value[2:].isalnum():
|
||||||
|
raise RepositoryError("Die IBAN hat kein gültiges Format.")
|
||||||
|
rearranged = value[4:] + value[:4]
|
||||||
|
numeric = "".join(
|
||||||
|
str(ord(character) - 55) if character.isalpha() else character
|
||||||
|
for character in rearranged
|
||||||
|
)
|
||||||
|
if int(numeric) % 97 != 1:
|
||||||
|
raise RepositoryError("Die IBAN-Prüfsumme ist ungültig.")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bic(value: str) -> None:
|
||||||
|
if value and (len(value) not in {8, 11} or not value.isalnum()):
|
||||||
|
raise RepositoryError("Der BIC muss 8 oder 11 Zeichen enthalten.")
|
||||||
|
|
||||||
|
|
||||||
def _normalize(value: str) -> str:
|
def _normalize(value: str) -> str:
|
||||||
normalized = unicodedata.normalize("NFKD", value.casefold().strip())
|
normalized = unicodedata.normalize("NFKD", value.casefold().strip())
|
||||||
return "".join(character for character in normalized if not unicodedata.combining(character))
|
return "".join(character for character in normalized if not unicodedata.combining(character))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class NewMemberDialog(tk.Toplevel):
|
|||||||
self.number_policy = repository.get_member_number_policy()
|
self.number_policy = repository.get_member_number_policy()
|
||||||
self.variables = {
|
self.variables = {
|
||||||
name: tk.StringVar()
|
name: tk.StringVar()
|
||||||
for name in ("first_name", "last_name", "email", "birth_date", "member_number")
|
for name in ("first_name", "last_name", "email", "phone", "birth_date", "member_number")
|
||||||
}
|
}
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self.bind("<Escape>", lambda _event: self.destroy())
|
self.bind("<Escape>", lambda _event: self.destroy())
|
||||||
@@ -33,6 +33,7 @@ class NewMemberDialog(tk.Toplevel):
|
|||||||
("Vorname *", "first_name"),
|
("Vorname *", "first_name"),
|
||||||
("Nachname *", "last_name"),
|
("Nachname *", "last_name"),
|
||||||
("E-Mail-Adresse", "email"),
|
("E-Mail-Adresse", "email"),
|
||||||
|
("Telefonnummer", "phone"),
|
||||||
(f"Geburtsdatum ({date_input_hint()})", "birth_date"),
|
(f"Geburtsdatum ({date_input_hint()})", "birth_date"),
|
||||||
]
|
]
|
||||||
if self.number_policy["mode"] == "manual":
|
if self.number_policy["mode"] == "manual":
|
||||||
|
|||||||
@@ -83,9 +83,13 @@ class MemberTab(ttk.Frame):
|
|||||||
notebook = ttk.Notebook(parent)
|
notebook = ttk.Notebook(parent)
|
||||||
notebook.grid(row=0, column=0, sticky="nsew")
|
notebook.grid(row=0, column=0, sticky="nsew")
|
||||||
data_tab = ttk.Frame(notebook, padding=16)
|
data_tab = ttk.Frame(notebook, padding=16)
|
||||||
|
address_tab = ttk.Frame(notebook, padding=16)
|
||||||
|
banking_tab = ttk.Frame(notebook, padding=16)
|
||||||
contribution_tab = ttk.Frame(notebook, padding=16)
|
contribution_tab = ttk.Frame(notebook, padding=16)
|
||||||
documents_tab = ttk.Frame(notebook, padding=16)
|
documents_tab = ttk.Frame(notebook, padding=16)
|
||||||
notebook.add(data_tab, text="Stammdaten")
|
notebook.add(data_tab, text="Stammdaten")
|
||||||
|
notebook.add(address_tab, text="Anschrift")
|
||||||
|
notebook.add(banking_tab, text="Bank / SEPA")
|
||||||
notebook.add(contribution_tab, text="Forderungen")
|
notebook.add(contribution_tab, text="Forderungen")
|
||||||
notebook.add(documents_tab, text="Dokumente")
|
notebook.add(documents_tab, text="Dokumente")
|
||||||
|
|
||||||
@@ -94,6 +98,7 @@ class MemberTab(ttk.Frame):
|
|||||||
("Vorname", "first_name"),
|
("Vorname", "first_name"),
|
||||||
("Nachname", "last_name"),
|
("Nachname", "last_name"),
|
||||||
("E-Mail-Adresse", "email"),
|
("E-Mail-Adresse", "email"),
|
||||||
|
("Telefonnummer", "phone"),
|
||||||
(f"Geburtsdatum ({date_input_hint()})", "birth_date"),
|
(f"Geburtsdatum ({date_input_hint()})", "birth_date"),
|
||||||
(f"Aufnahmebeschluss ({date_input_hint()})", "accepted_at"),
|
(f"Aufnahmebeschluss ({date_input_hint()})", "accepted_at"),
|
||||||
(f"Mitglied seit ({date_input_hint()})", "membership_started_at"),
|
(f"Mitglied seit ({date_input_hint()})", "membership_started_at"),
|
||||||
@@ -139,6 +144,50 @@ class MemberTab(ttk.Frame):
|
|||||||
row=len(fields) + 2, column=1, sticky="e", pady=(18, 0)
|
row=len(fields) + 2, column=1, sticky="e", pady=(18, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
address_fields = (
|
||||||
|
("Straße und Hausnummer", "street"),
|
||||||
|
("Adresszusatz", "address_addition"),
|
||||||
|
("Postleitzahl", "postal_code"),
|
||||||
|
("Ort", "city"),
|
||||||
|
("Land", "country"),
|
||||||
|
)
|
||||||
|
address_tab.columnconfigure(1, weight=1)
|
||||||
|
for row, (label, key) in enumerate(address_fields):
|
||||||
|
self.variables[key] = tk.StringVar()
|
||||||
|
ttk.Label(address_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
||||||
|
ttk.Entry(address_tab, textvariable=self.variables[key], width=42).grid(
|
||||||
|
row=row, column=1, sticky="ew", pady=5
|
||||||
|
)
|
||||||
|
ttk.Button(
|
||||||
|
address_tab, text="Anschrift speichern", style="Accent.TButton", command=self._save
|
||||||
|
).grid(row=len(address_fields), column=1, sticky="e", pady=(18, 0))
|
||||||
|
|
||||||
|
banking_fields = (
|
||||||
|
("Kontoinhaber", "account_holder"),
|
||||||
|
("IBAN", "iban"),
|
||||||
|
("BIC", "bic"),
|
||||||
|
("Mandatsreferenz", "mandate_reference"),
|
||||||
|
(f"Mandat erteilt am ({date_input_hint()})", "mandate_signed_at"),
|
||||||
|
(f"Mandat widerrufen am ({date_input_hint()})", "mandate_revoked_at"),
|
||||||
|
)
|
||||||
|
banking_tab.columnconfigure(1, weight=1)
|
||||||
|
for row, (label, key) in enumerate(banking_fields):
|
||||||
|
self.variables[key] = tk.StringVar()
|
||||||
|
ttk.Label(banking_tab, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
||||||
|
ttk.Entry(banking_tab, textvariable=self.variables[key], width=42).grid(
|
||||||
|
row=row, column=1, sticky="ew", pady=5
|
||||||
|
)
|
||||||
|
self.variables["mandate_active"] = tk.BooleanVar()
|
||||||
|
ttk.Checkbutton(
|
||||||
|
banking_tab,
|
||||||
|
text="SEPA-Lastschriftmandat ist aktiv",
|
||||||
|
variable=self.variables["mandate_active"],
|
||||||
|
style="Switch",
|
||||||
|
).grid(row=len(banking_fields), column=0, columnspan=2, sticky="w", pady=(12, 5))
|
||||||
|
ttk.Button(
|
||||||
|
banking_tab, text="Bankdaten speichern", style="Accent.TButton", command=self._save
|
||||||
|
).grid(row=len(banking_fields) + 1, column=1, sticky="e", pady=(18, 0))
|
||||||
|
|
||||||
contribution_tab.columnconfigure(0, weight=1)
|
contribution_tab.columnconfigure(0, weight=1)
|
||||||
contribution_tab.rowconfigure(1, weight=1)
|
contribution_tab.rowconfigure(1, weight=1)
|
||||||
self.contribution_summary = tk.StringVar()
|
self.contribution_summary = tk.StringVar()
|
||||||
@@ -225,7 +274,13 @@ class MemberTab(ttk.Frame):
|
|||||||
self.member = self.repository.get_member(self.member_id)
|
self.member = self.repository.get_member(self.member_id)
|
||||||
self.title_var.set(f"{self.member.member_number or '—'} · {self.member.display_name}")
|
self.title_var.set(f"{self.member.member_number or '—'} · {self.member.display_name}")
|
||||||
self.status_var.set(STATUS_LABELS.get(self.member.status, self.member.status.upper()))
|
self.status_var.set(STATUS_LABELS.get(self.member.status, self.member.status.upper()))
|
||||||
date_fields = {"birth_date", "accepted_at", "membership_started_at"}
|
date_fields = {
|
||||||
|
"birth_date",
|
||||||
|
"accepted_at",
|
||||||
|
"membership_started_at",
|
||||||
|
"mandate_signed_at",
|
||||||
|
"mandate_revoked_at",
|
||||||
|
}
|
||||||
for key, variable in self.variables.items():
|
for key, variable in self.variables.items():
|
||||||
value = getattr(self.member, key)
|
value = getattr(self.member, key)
|
||||||
if key == "status":
|
if key == "status":
|
||||||
@@ -303,7 +358,8 @@ class MemberTab(ttk.Frame):
|
|||||||
|
|
||||||
def _save(self) -> None:
|
def _save(self) -> None:
|
||||||
for key, variable in self.variables.items():
|
for key, variable in self.variables.items():
|
||||||
value = variable.get().strip()
|
raw_value = variable.get()
|
||||||
|
value = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
||||||
if key == "status":
|
if key == "status":
|
||||||
value = storage_key(STATUS_LABELS, value)
|
value = storage_key(STATUS_LABELS, value)
|
||||||
setattr(self.member, key, value)
|
setattr(self.member, key, value)
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
self.manual_numbers_var = tk.BooleanVar(value=number_policy["mode"] == "manual")
|
self.manual_numbers_var = tk.BooleanVar(value=number_policy["mode"] == "manual")
|
||||||
self.number_pattern_var = tk.StringVar(value=number_policy["pattern"])
|
self.number_pattern_var = tk.StringVar(value=number_policy["pattern"])
|
||||||
self.number_preview_var = tk.StringVar()
|
self.number_preview_var = tk.StringVar()
|
||||||
|
organization = repository.get_configuration().get("organization") or {}
|
||||||
|
if not isinstance(organization, dict):
|
||||||
|
organization = {}
|
||||||
|
self.organization_vars = {
|
||||||
|
key: tk.StringVar(value=str(organization.get(key, "")))
|
||||||
|
for key in (
|
||||||
|
"name", "street", "postal_code", "city", "country", "email", "phone",
|
||||||
|
"website", "iban", "bic", "creditor_id",
|
||||||
|
)
|
||||||
|
}
|
||||||
self.title("Optionen")
|
self.title("Optionen")
|
||||||
self.geometry("900x650")
|
self.geometry("900x650")
|
||||||
self.minsize(760, 540)
|
self.minsize(760, 540)
|
||||||
@@ -65,16 +75,19 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
paths = ttk.Frame(notebook, padding=16)
|
paths = ttk.Frame(notebook, padding=16)
|
||||||
appearance = ttk.Frame(notebook, padding=16)
|
appearance = ttk.Frame(notebook, padding=16)
|
||||||
member_numbers = ttk.Frame(notebook, padding=16)
|
member_numbers = ttk.Frame(notebook, padding=16)
|
||||||
|
organization = ttk.Frame(notebook, padding=16)
|
||||||
automation = ttk.Frame(notebook, padding=16)
|
automation = ttk.Frame(notebook, padding=16)
|
||||||
changelog = ChangelogView(notebook)
|
changelog = ChangelogView(notebook)
|
||||||
notebook.add(paths, text="Pfade")
|
notebook.add(paths, text="Pfade")
|
||||||
notebook.add(appearance, text="Darstellung")
|
notebook.add(appearance, text="Darstellung")
|
||||||
notebook.add(member_numbers, text="Mitgliedsnummern")
|
notebook.add(member_numbers, text="Mitgliedsnummern")
|
||||||
|
notebook.add(organization, text="Verein / Absender")
|
||||||
notebook.add(automation, text="Hausmeister")
|
notebook.add(automation, text="Hausmeister")
|
||||||
notebook.add(changelog, text="Changelog")
|
notebook.add(changelog, text="Changelog")
|
||||||
self._build_paths(paths)
|
self._build_paths(paths)
|
||||||
self._build_appearance(appearance)
|
self._build_appearance(appearance)
|
||||||
self._build_member_numbers(member_numbers)
|
self._build_member_numbers(member_numbers)
|
||||||
|
self._build_organization(organization)
|
||||||
self._build_automation(automation)
|
self._build_automation(automation)
|
||||||
|
|
||||||
buttons = ttk.Frame(root)
|
buttons = ttk.Frame(root)
|
||||||
@@ -148,6 +161,27 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
style="Muted.TLabel",
|
style="Muted.TLabel",
|
||||||
).grid(row=1, column=1, sticky="w")
|
).grid(row=1, column=1, sticky="w")
|
||||||
|
|
||||||
|
def _build_organization(self, parent: ttk.Frame) -> None:
|
||||||
|
parent.columnconfigure(1, weight=1)
|
||||||
|
fields = (
|
||||||
|
("Vereinsname", "name"),
|
||||||
|
("Straße und Hausnummer", "street"),
|
||||||
|
("Postleitzahl", "postal_code"),
|
||||||
|
("Ort", "city"),
|
||||||
|
("Land", "country"),
|
||||||
|
("E-Mail-Adresse", "email"),
|
||||||
|
("Telefonnummer", "phone"),
|
||||||
|
("Webseite", "website"),
|
||||||
|
("IBAN", "iban"),
|
||||||
|
("BIC", "bic"),
|
||||||
|
("SEPA-Gläubiger-ID", "creditor_id"),
|
||||||
|
)
|
||||||
|
for row, (label, key) in enumerate(fields):
|
||||||
|
ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
||||||
|
ttk.Entry(parent, textvariable=self.organization_vars[key], width=48).grid(
|
||||||
|
row=row, column=1, sticky="ew", pady=5
|
||||||
|
)
|
||||||
|
|
||||||
def _build_automation(self, parent: ttk.Frame) -> None:
|
def _build_automation(self, parent: ttk.Frame) -> None:
|
||||||
parent.columnconfigure(1, weight=1)
|
parent.columnconfigure(1, weight=1)
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
@@ -350,6 +384,9 @@ class OptionsDialog(tk.Toplevel):
|
|||||||
try:
|
try:
|
||||||
self.config_obj.save()
|
self.config_obj.save()
|
||||||
self.repository.save_member_number_policy(mode=number_mode, pattern=number_pattern)
|
self.repository.save_member_number_policy(mode=number_mode, pattern=number_pattern)
|
||||||
|
self.repository.save_organization(
|
||||||
|
{key: variable.get() for key, variable in self.organization_vars.items()}
|
||||||
|
)
|
||||||
except (OSError, RepositoryError) as exc:
|
except (OSError, RepositoryError) as exc:
|
||||||
messagebox.showerror("Optionen konnten nicht gespeichert werden", str(exc), parent=self)
|
messagebox.showerror("Optionen konnten nicht gespeichert werden", str(exc), parent=self)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -168,3 +168,69 @@ def test_invalid_member_number_patterns_are_rejected(pattern) -> None:
|
|||||||
|
|
||||||
def test_member_number_formatter_supports_padding_and_year() -> None:
|
def test_member_number_formatter_supports_padding_and_year() -> None:
|
||||||
assert format_member_number("CCMA-{year}-{number:05d}", 42, year=2026) == "CCMA-2026-00042"
|
assert format_member_number("CCMA-{year}-{number:05d}", 42, year=2026) == "CCMA-2026-00042"
|
||||||
|
|
||||||
|
|
||||||
|
def test_member_address_and_sepa_data_are_structured_and_audited_without_values(tmp_path) -> None:
|
||||||
|
repository = MemberRepository(tmp_path)
|
||||||
|
repository.initialize()
|
||||||
|
member = repository.create_member(first_name="Bank", last_name="Member", phone="0621 12345")
|
||||||
|
member.street = "Teststraße 23"
|
||||||
|
member.postal_code = "68159"
|
||||||
|
member.city = "Mannheim"
|
||||||
|
member.account_holder = "Bank Member"
|
||||||
|
member.iban = "DE89 3704 0044 0532 0130 00"
|
||||||
|
member.bic = "COBADEFFXXX"
|
||||||
|
member.mandate_reference = "CCMA-M-42"
|
||||||
|
member.mandate_signed_at = "21.06.2026"
|
||||||
|
member.mandate_active = True
|
||||||
|
|
||||||
|
repository.save_member(member)
|
||||||
|
|
||||||
|
loaded = repository.get_member(member.member_id)
|
||||||
|
assert loaded.iban == "DE89370400440532013000"
|
||||||
|
assert loaded.mandate_signed_at == "2026-06-21"
|
||||||
|
raw = json.loads((repository.members_root / member.member_id / "member.json").read_text())
|
||||||
|
assert raw["address"]["city"] == "Mannheim"
|
||||||
|
assert raw["banking"]["mandate_active"] is True
|
||||||
|
event = repository.get_events(member.member_id)[-1]
|
||||||
|
assert "IBAN" in event.summary
|
||||||
|
assert loaded.iban not in event.summary
|
||||||
|
assert loaded.street not in event.summary
|
||||||
|
|
||||||
|
|
||||||
|
def test_active_sepa_mandate_requires_valid_complete_data(tmp_path) -> None:
|
||||||
|
repository = MemberRepository(tmp_path)
|
||||||
|
repository.initialize()
|
||||||
|
member = repository.create_member(first_name="Invalid", last_name="Mandate")
|
||||||
|
member.iban = "DE001234"
|
||||||
|
with pytest.raises(RepositoryError, match="IBAN"):
|
||||||
|
repository.save_member(member)
|
||||||
|
|
||||||
|
member.iban = "DE89370400440532013000"
|
||||||
|
member.mandate_active = True
|
||||||
|
with pytest.raises(RepositoryError, match="aktives Lastschriftmandat"):
|
||||||
|
repository.save_member(member)
|
||||||
|
|
||||||
|
|
||||||
|
def test_organization_sender_data_is_stored_centrally(tmp_path) -> None:
|
||||||
|
repository = MemberRepository(tmp_path)
|
||||||
|
repository.initialize()
|
||||||
|
repository.save_organization(
|
||||||
|
{
|
||||||
|
"name": "Chaos Computer Club Mannheim e.V.",
|
||||||
|
"street": "Testweg 1",
|
||||||
|
"postal_code": "68159",
|
||||||
|
"city": "Mannheim",
|
||||||
|
"country": "Deutschland",
|
||||||
|
"email": "vorstand@example.org",
|
||||||
|
"phone": "",
|
||||||
|
"website": "https://example.org",
|
||||||
|
"iban": "DE89370400440532013000",
|
||||||
|
"bic": "COBADEFFXXX",
|
||||||
|
"creditor_id": "DE98ZZZ09999999999",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
organization = repository.get_configuration()["organization"]
|
||||||
|
assert organization["street"] == "Testweg 1"
|
||||||
|
assert organization["iban"] == "DE89370400440532013000"
|
||||||
|
|||||||
Reference in New Issue
Block a user