Add a dues-invoicing feature: send, track and remind on membership fees

New DuesInvoice model (one per membership, resendable) plus
club.services.invoicing: resolves the best email to invoice (the
member's own, else a parent/guardian's), snapshots the outstanding
balance and a due date on send, and mails a branded HTML invoice
(same club-colour email shell as the parent-claim email) with a
WeasyPrint PDF attached when the native libs are available.

Dues & billing gains a bulk "Send invoice" action (checkbox selection
+ a shared due-in-days prompt), a per-row invoice status column, a
staff-facing invoice detail/PDF page, and a push-button "Send
reminders" action for every sent, unpaid invoice past its own due
date.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-20 21:23:06 +02:00
parent 44ddf7b6eb
commit 94d6cbd4e9
19 changed files with 1334 additions and 8 deletions

View File

@@ -872,6 +872,19 @@ class RecordFeePaymentForm(forms.Form):
note = forms.CharField(label=_("Note"), required=False, widget=forms.Textarea(attrs={"rows": 2}))
class SendDuesInvoicesForm(forms.Form):
"""The one shared setting for a batch of invoices sent from the Dues & billing
page -- see club.services.invoicing.create_or_resend_invoice. Applies to every
membership selected in the same submit, not chosen per row.
The widget's own `form` attr (rendered as-is by templatetags/field.html's attrs
passthrough) is what lets this field live inside the confirm dialog while still
posting through the row-checkboxes' #membership-form -- see membership_list.html's
send_invoices_modal."""
due_in_days = forms.IntegerField(label=_("Due in"), min_value=1, max_value=365, initial=14, help_text=_("Days from today."), widget=forms.NumberInput(attrs={"form": "membership-form"}))
class ClubSettingsForm(forms.ModelForm):
"""A club's own self-service identity/branding editor (management:club_settings) --
the club-facing equivalent of controlpanel's ClubForm, minus everything only