mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
feat: add per-member payment frequencies
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
"Offene Forderungen von Mitgliedern mit aktivem Lastschriftmandat können für einen frei wählbaren Fälligkeitszeitraum gesammelt und als bankfähige SEPA-XML-Datei oder als CSV-Übersicht exportiert werden; unvollständige Mandate werden dabei verständlich ausgewiesen.",
|
||||
"Für geplante SEPA-Einzüge können personalisierte, mit Thunderbird kompatible E-Mail-Entwürfe erzeugt werden. Die Mitteilungen enthalten Betrag, Einzugsdatum und Mandatsdaten und werden automatisch in der jeweiligen Mitgliederakte archiviert.",
|
||||
"Mahnungsentwürfe können direkt als personalisierte, mit Thunderbird kompatible E-Mail-Datei ausgegeben und in der Mitgliederakte archiviert werden; dabei werden der Versand verbucht sowie Zahlungsfrist und gegebenenfalls Mahngebühr wirksam.",
|
||||
"Für automatisch vergebene Mitgliedsnummern kann gewählt werden, ob vorhandene Lücken mit der nächsten freien Nummer gefüllt werden oder stets die höchste bestehende Nummer um eins erhöht wird; die Vergabe ist gegen parallele Doppelbelegungen abgesichert."
|
||||
"Für automatisch vergebene Mitgliedsnummern kann gewählt werden, ob vorhandene Lücken mit der nächsten freien Nummer gefüllt werden oder stets die höchste bestehende Nummer um eins erhöht wird; die Vergabe ist gegen parallele Doppelbelegungen abgesichert.",
|
||||
"Die Zahlweise kann pro Mitglied als monatlich, quartalsweise, halbjährlich oder jährlich festgelegt werden; Hausmeister und Lastschriftläufe erzeugen und berücksichtigen die dazu passenden Beitragsforderungen."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,13 @@ MEMBERSHIP_STATUS_LABELS = {
|
||||
"ended": "BEENDET",
|
||||
}
|
||||
|
||||
PAYMENT_FREQUENCY_LABELS = {
|
||||
"monthly": "MONATLICH",
|
||||
"quarterly": "QUARTALSWEISE",
|
||||
"semiannual": "HALBJÄHRLICH",
|
||||
"annual": "JÄHRLICH",
|
||||
}
|
||||
|
||||
ASSET_STATUS_LABELS = {
|
||||
"available": "VERFUEGBAR",
|
||||
"issued": "AUSGEGEBEN",
|
||||
@@ -91,7 +98,7 @@ class Member:
|
||||
accepted_at: str = ""
|
||||
membership_started_at: str = ""
|
||||
membership_ended_at: str = ""
|
||||
payment_frequency: str = "annual"
|
||||
payment_frequency: str = "semiannual"
|
||||
contribution_rule_id: str = "standard-2022"
|
||||
honorary: bool = False
|
||||
notes: str = ""
|
||||
@@ -183,7 +190,7 @@ class Member:
|
||||
membership_ended_at=str(membership.get("ended_at", "")),
|
||||
honorary=bool(membership.get("honorary", False)),
|
||||
contribution_rule_id=str(contribution.get("rule_id", "standard-2022")),
|
||||
payment_frequency=str(contribution.get("payment_frequency", "annual")),
|
||||
payment_frequency=str(contribution.get("payment_frequency", "semiannual")),
|
||||
notes=str(data.get("notes", "")),
|
||||
created_at=str(data.get("created_at", _iso_now())),
|
||||
updated_at=str(data.get("updated_at", _iso_now())),
|
||||
|
||||
@@ -9,6 +9,21 @@ from ccma.rules.scripts._shared import CONTRIBUTION_STATUSES
|
||||
RULE_ID = "contribution-claims"
|
||||
ORDER = 40
|
||||
CENT = Decimal("0.01")
|
||||
MONTH_NAMES = (
|
||||
"",
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember",
|
||||
)
|
||||
|
||||
|
||||
def evaluate(context: RuleContext):
|
||||
@@ -85,6 +100,26 @@ def _membership_claims(
|
||||
("first-half", 1, 6, _due_date(year, configured_due_dates[0], "01-31")),
|
||||
("second-half", 7, 12, _due_date(year, configured_due_dates[1], "07-31")),
|
||||
]
|
||||
elif member.payment_frequency == "quarterly":
|
||||
periods = [
|
||||
(
|
||||
f"quarter-{quarter}",
|
||||
first_month,
|
||||
first_month + 2,
|
||||
_recurring_due_date(year, first_month, rule.get("annual_due"), "01-31"),
|
||||
)
|
||||
for quarter, first_month in enumerate((1, 4, 7, 10), start=1)
|
||||
]
|
||||
elif member.payment_frequency == "monthly":
|
||||
periods = [
|
||||
(
|
||||
f"month-{month:02d}",
|
||||
month,
|
||||
month,
|
||||
_recurring_due_date(year, month, rule.get("annual_due"), "01-31"),
|
||||
)
|
||||
for month in range(1, 13)
|
||||
]
|
||||
else:
|
||||
periods = [("annual", 1, 12, _due_date(year, rule.get("annual_due"), "01-31"))]
|
||||
|
||||
@@ -168,7 +203,21 @@ def _due_date(year: int, value, fallback: str) -> date:
|
||||
return date(year, month, day)
|
||||
|
||||
|
||||
def _recurring_due_date(year: int, month: int, value, fallback: str) -> date:
|
||||
try:
|
||||
_configured_month, day = (int(part) for part in str(value or fallback).split("-", 1))
|
||||
except (TypeError, ValueError):
|
||||
day = int(fallback.split("-", 1)[1])
|
||||
return date(year, month, min(day, calendar.monthrange(year, month)[1]))
|
||||
|
||||
|
||||
def _title(year: int, suffix: str) -> str:
|
||||
if suffix.startswith("month-"):
|
||||
month = int(suffix.removeprefix("month-"))
|
||||
return f"Mitgliedsbeitrag {MONTH_NAMES[month]} {year}"
|
||||
if suffix.startswith("quarter-"):
|
||||
quarter = int(suffix.removeprefix("quarter-"))
|
||||
return f"Mitgliedsbeitrag {quarter}. Quartal {year}"
|
||||
if suffix == "first-half":
|
||||
return f"Mitgliedsbeitrag 1. Halbjahr {year}"
|
||||
if suffix == "second-half":
|
||||
|
||||
@@ -29,6 +29,7 @@ from ccma.domain.models import (
|
||||
ASSET_OWNER_TYPE_LABELS,
|
||||
ASSET_STATUS_LABELS,
|
||||
MEMBERSHIP_STATUS_LABELS,
|
||||
PAYMENT_FREQUENCY_LABELS,
|
||||
Asset,
|
||||
ContributionData,
|
||||
Event,
|
||||
@@ -391,6 +392,8 @@ class MemberRepository:
|
||||
raise RepositoryError(
|
||||
"Ein aktives Lastschriftmandat benötigt IBAN, Mandatsreferenz und Erteilungsdatum."
|
||||
)
|
||||
if member.payment_frequency not in PAYMENT_FREQUENCY_LABELS:
|
||||
raise RepositoryError("Ungültige Zahlweise.")
|
||||
if member.member_number != existing.member_number:
|
||||
self._assert_member_number_available(member.member_number, exclude_member_id=member.member_id)
|
||||
changes = self._summarize_changes(existing, member)
|
||||
|
||||
@@ -15,7 +15,7 @@ from ccma.domain.contributions import (
|
||||
payment_allocated_total,
|
||||
)
|
||||
from ccma.domain.dates import age_label, date_input_hint, format_date_for_display
|
||||
from ccma.domain.models import ASSET_STATUS_LABELS, Event
|
||||
from ccma.domain.models import ASSET_STATUS_LABELS, PAYMENT_FREQUENCY_LABELS, Event
|
||||
from ccma.domain.models import MEMBERSHIP_STATUS_LABELS as STATUS_LABELS
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
from ccma.ui.dialogs import IntegrityWarningDialog
|
||||
@@ -204,11 +204,22 @@ class MemberTab(ttk.Frame):
|
||||
state="readonly",
|
||||
width=39,
|
||||
).grid(row=len(fields), column=1, sticky="ew", pady=5)
|
||||
self._add_variable("payment_frequency", tk.StringVar(), "data")
|
||||
ttk.Label(data_tab, text="Zahlweise").grid(
|
||||
row=len(fields) + 1, column=0, sticky="w", pady=5, padx=(0, 12)
|
||||
)
|
||||
ttk.Combobox(
|
||||
data_tab,
|
||||
textvariable=self.variables["payment_frequency"],
|
||||
values=list(PAYMENT_FREQUENCY_LABELS.values()),
|
||||
state="readonly",
|
||||
width=39,
|
||||
).grid(row=len(fields) + 1, column=1, sticky="ew", pady=5)
|
||||
ttk.Label(data_tab, text="Interne Notiz").grid(
|
||||
row=len(fields) + 1, column=0, sticky="nw", pady=5, padx=(0, 12)
|
||||
row=len(fields) + 2, column=0, sticky="nw", pady=5, padx=(0, 12)
|
||||
)
|
||||
self.notes_text = tk.Text(data_tab, width=42, height=6, wrap="word")
|
||||
self.notes_text.grid(row=len(fields) + 1, column=1, sticky="ew", pady=5)
|
||||
self.notes_text.grid(row=len(fields) + 2, column=1, sticky="ew", pady=5)
|
||||
self.notes_text.bind("<<Modified>>", lambda _event: self._mark_dirty_from_text("data"), add="+")
|
||||
data_tab.columnconfigure(1, weight=1)
|
||||
|
||||
@@ -474,6 +485,8 @@ class MemberTab(ttk.Frame):
|
||||
value = getattr(self.member, key)
|
||||
if key == "status":
|
||||
variable.set(display_label(STATUS_LABELS, str(value)))
|
||||
elif key == "payment_frequency":
|
||||
variable.set(display_label(PAYMENT_FREQUENCY_LABELS, str(value)))
|
||||
else:
|
||||
variable.set(format_date_for_display(value) if key in date_fields else value)
|
||||
if self.notes_text is not None:
|
||||
@@ -688,6 +701,8 @@ class MemberTab(ttk.Frame):
|
||||
value = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
||||
if key == "status":
|
||||
value = storage_key(STATUS_LABELS, value)
|
||||
elif key == "payment_frequency":
|
||||
value = storage_key(PAYMENT_FREQUENCY_LABELS, value)
|
||||
setattr(self.member, key, value)
|
||||
if self.notes_text is not None:
|
||||
self.member.notes = self.notes_text.get("1.0", "end-1c").strip()
|
||||
|
||||
Reference in New Issue
Block a user