Add the management app: club-facing UI + real fee-payment tracking
Gives clubs a self-service /manage/ area for members, families, teams, roles, and season memberships, alongside real fee-payment tracking (FeePayment, record_payment/mark_as_paid/remaining_balance) so a membership's paid status reflects actual money received instead of a single manually-set flag.
This commit is contained in:
144
management/forms.py
Normal file
144
management/forms.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from club.models import ClubMembership, ClubRole, FeePayment
|
||||
from members.models import Family, FamilyMembership, Member
|
||||
from members.services.family import find_member_by_email
|
||||
from teams.models import Team
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class MemberForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Member
|
||||
fields = ["first_name", "last_name", "date_of_birth", "email", "phone", "emergency_phone"]
|
||||
widgets = {"date_of_birth": forms.DateInput(attrs={"type": "date"})}
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ["name", "short_name"]
|
||||
|
||||
|
||||
class ClubRoleAssignForm(forms.ModelForm):
|
||||
"""Grant a club-wide role to a member already affiliated with this club."""
|
||||
|
||||
class Meta:
|
||||
model = ClubRole
|
||||
fields = ["member", "role"]
|
||||
|
||||
def __init__(self, *args, club=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Never list members of other clubs -- this isn't a platform-wide picker.
|
||||
self.fields["member"].queryset = Member.objects.filter(member_of__club=club).distinct()
|
||||
|
||||
|
||||
class FamilyCreateForm(forms.Form):
|
||||
"""One new family in one go: a parent (who gets a login) and a child (who
|
||||
doesn't). See members.services.family.register_family."""
|
||||
|
||||
parent_first_name = forms.CharField(label=_("Parent first name"))
|
||||
parent_last_name = forms.CharField(label=_("Parent last name"))
|
||||
parent_email = forms.EmailField(label=_("Parent email"), help_text=_("If this email has no account yet, one is created and they set a password via the reset link."))
|
||||
|
||||
child_first_name = forms.CharField(label=_("Child first name"))
|
||||
child_last_name = forms.CharField(label=_("Child last name"))
|
||||
child_date_of_birth = forms.DateField(label=_("Child date of birth"), required=False, widget=forms.DateInput(attrs={"type": "date"}))
|
||||
|
||||
|
||||
class AddChildForm(forms.Form):
|
||||
"""A family that needs one more child registered -- see
|
||||
members.services.family.add_child_to_family."""
|
||||
|
||||
first_name = forms.CharField(label=_("First name"))
|
||||
last_name = forms.CharField(label=_("Last name"))
|
||||
date_of_birth = forms.DateField(label=_("Date of birth"), required=False, widget=forms.DateInput(attrs={"type": "date"}))
|
||||
|
||||
|
||||
class AddParentForm(forms.Form):
|
||||
"""A family that needs one more parent/guardian registered -- see
|
||||
members.services.family.add_parent_to_family."""
|
||||
|
||||
email = forms.EmailField(label=_("Email address"), help_text=_("If this email has no account yet, one is created and they set a password via the reset link."))
|
||||
first_name = forms.CharField(label=_("First name"), required=False)
|
||||
last_name = forms.CharField(label=_("Last name"), required=False)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
email = cleaned.get("email")
|
||||
|
||||
# Only a brand-new person needs a name; an existing member already has one.
|
||||
if email and find_member_by_email(email) is None:
|
||||
for field in ("first_name", "last_name"):
|
||||
if not cleaned.get(field):
|
||||
self.add_error(field, _("Required: this email has no account yet."))
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
class AttachToFamilyForm(forms.Form):
|
||||
"""Link a standalone member into a family -- a new one, or an existing one they
|
||||
turn out to belong to. See members.services.family.attach_to_family."""
|
||||
|
||||
role = forms.ChoiceField(label=_("Role"), choices=FamilyMembership.FamilyRole.choices)
|
||||
family = forms.ModelChoiceField(label=_("Family"), queryset=Family.objects.none(), required=False, empty_label=_("— start a new family —"))
|
||||
|
||||
def __init__(self, *args, club=None, member=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Same scoping query as management.views.families_of_club -- inlined rather
|
||||
# than imported, since that function lives in views.py, which imports this
|
||||
# module (a module-level import back here would be circular).
|
||||
queryset = Family.objects.filter(memberships__member__member_of__club=club).distinct()
|
||||
if member is not None:
|
||||
# Already a member of it -- offering it again would be a no-op re-add.
|
||||
queryset = queryset.exclude(memberships__member=member)
|
||||
self.fields["family"].queryset = queryset
|
||||
|
||||
|
||||
class MemberImportUploadForm(forms.Form):
|
||||
"""The mass-upload entry point -- one .xlsx file, built from the downloadable
|
||||
template. See management.bulk_import.read_member_import_workbook."""
|
||||
|
||||
file = forms.FileField(label=_("Excel file"), help_text=_("Use the downloaded template — one row per member."))
|
||||
|
||||
|
||||
class GrantLoginForm(forms.Form):
|
||||
"""A login-less family member (a child, typically) getting their own account --
|
||||
see members.services.family.grant_login. Pre-filled from the member's contact
|
||||
email where one is already on file; still editable, and required either way."""
|
||||
|
||||
email = forms.EmailField(label=_("Email"), help_text=_("They'll set a password via the reset link the first time they sign in."))
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data["email"]
|
||||
if User.objects.filter(email__iexact=email).exists():
|
||||
raise forms.ValidationError(_("This email is already in use."))
|
||||
return email
|
||||
|
||||
|
||||
class ClubMembershipForm(forms.ModelForm):
|
||||
"""This season's standing -- shown and edited right on the member's own page,
|
||||
since a Member has no club of its own without one. fee_amount is the only
|
||||
money field here -- amount_paid is exclusively written by club.services.fees,
|
||||
never hand-edited."""
|
||||
|
||||
class Meta:
|
||||
model = ClubMembership
|
||||
fields = ["license", "status", "fee_status", "fee_amount"]
|
||||
|
||||
|
||||
class RecordFeePaymentForm(forms.Form):
|
||||
"""Money received against one membership's fee -- see club.services.fees.record_payment.
|
||||
Reusable for any amount, partial or the exact remaining balance; "Mark fully
|
||||
paid" (management.views.MembershipMarkFullyPaidView) skips this form entirely
|
||||
and settles the balance directly in one click."""
|
||||
|
||||
amount = forms.DecimalField(label=_("Amount"), max_digits=10, decimal_places=2, min_value=Decimal("0.01"))
|
||||
method = forms.ChoiceField(label=_("Method"), choices=FeePayment.Method.choices)
|
||||
reference = forms.CharField(label=_("Reference"), required=False, help_text=_("Bank reference, transaction id — whatever lets you find this again."))
|
||||
note = forms.CharField(label=_("Note"), required=False, widget=forms.Textarea(attrs={"rows": 2}))
|
||||
Reference in New Issue
Block a user