Add a Payments & dues section to the Me page, matching the design's M5 row
Home already had the M1 dues card; Me's own "Payments & dues" row was explicitly stubbed out with nowhere to lead. Give it a real destination: a Payments screen (open balances for everyone managed, reusing Home's dues-card layout via a shared _dues_row.html partial) and a "N OPEN" pill on the Me row itself, only shown once something is actually owed. club.services.fees.open_dues_rows is factored out so Home and Payments can never drift apart on what counts as "still open". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
@@ -16,6 +16,23 @@ def remaining_balance(membership):
|
||||
return max(membership.fee_amount - membership.amount_paid, Decimal("0.00"))
|
||||
|
||||
|
||||
def open_dues_rows(club, people, season):
|
||||
"""Every season-dues row still owed by ``people`` in ``season`` -- shared by
|
||||
mobile's Home dues card and its Payments & dues screen so the two never
|
||||
drift out of sync on what counts as "still open". WAIVED memberships and
|
||||
fully-paid balances are excluded."""
|
||||
if season is None or not people:
|
||||
return []
|
||||
|
||||
memberships = ClubMembership.objects.filter(club=club, member__in=people, season=season).exclude(fee_status=ClubMembership.FeeStatus.WAIVED).select_related("dues_invoice", "member")
|
||||
rows = []
|
||||
for membership in memberships:
|
||||
balance = remaining_balance(membership)
|
||||
if balance > 0:
|
||||
rows.append({"membership": membership, "balance": balance, "invoice": getattr(membership, "dues_invoice", None)})
|
||||
return rows
|
||||
|
||||
|
||||
def record_payment(membership, *, amount, method=FeePayment.Method.BANK_TRANSFER, reference="", note="", recorded_by=None):
|
||||
"""Record money received against one membership's fee. Several payments may
|
||||
land on one membership -- a family paying in two installments must not read as
|
||||
|
||||
@@ -39,7 +39,7 @@ from .services.access import (
|
||||
teams_managed_by,
|
||||
teams_staffed_by,
|
||||
)
|
||||
from .services.fees import mark_as_paid, record_payment, remaining_balance
|
||||
from .services.fees import mark_as_paid, open_dues_rows, record_payment, remaining_balance
|
||||
from .services.invoicing import create_or_resend_invoice, invoice_pdf, invoices_due_for_reminder, recipient_for, resolve_document_address
|
||||
from .services.onboarding import (
|
||||
annotate_onboarding_status,
|
||||
@@ -1562,6 +1562,69 @@ class FeeServiceTests(TestCase):
|
||||
self.assertEqual(payment.recorded_by, user)
|
||||
|
||||
|
||||
class OpenDuesRowsTests(TestCase):
|
||||
"""club.services.fees.open_dues_rows -- the shared source behind mobile's
|
||||
Home dues card and its Payments & dues screen (mobile/views.py's HomeView
|
||||
and PaymentsView)."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
cls.season = make_season(cls.club)
|
||||
cls.member = Member.objects.create(first_name="Jane", last_name="Doe")
|
||||
|
||||
def test_returns_nothing_without_a_season(self):
|
||||
ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
|
||||
self.assertEqual(open_dues_rows(self.club, [self.member], None), [])
|
||||
|
||||
def test_returns_nothing_without_any_people(self):
|
||||
self.assertEqual(open_dues_rows(self.club, [], self.season), [])
|
||||
|
||||
def test_a_membership_with_a_remaining_balance_is_included(self):
|
||||
membership = ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
|
||||
rows = open_dues_rows(self.club, [self.member], self.season)
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["membership"], membership)
|
||||
self.assertEqual(rows[0]["balance"], Decimal("150.00"))
|
||||
self.assertIsNone(rows[0]["invoice"])
|
||||
|
||||
def test_a_fully_paid_membership_is_excluded(self):
|
||||
membership = ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
record_payment(membership, amount=Decimal("150.00"))
|
||||
|
||||
self.assertEqual(open_dues_rows(self.club, [self.member], self.season), [])
|
||||
|
||||
def test_a_waived_membership_is_excluded_even_with_an_unpaid_balance(self):
|
||||
ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"), fee_status=ClubMembership.FeeStatus.WAIVED)
|
||||
|
||||
self.assertEqual(open_dues_rows(self.club, [self.member], self.season), [])
|
||||
|
||||
def test_a_membership_with_no_fee_priced_is_excluded(self):
|
||||
ClubMembership.objects.create(club=self.club, member=self.member, season=self.season)
|
||||
|
||||
self.assertEqual(open_dues_rows(self.club, [self.member], self.season), [])
|
||||
|
||||
def test_the_linked_invoice_is_included_when_one_exists(self):
|
||||
membership = ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
invoice = DuesInvoice.objects.create(club=self.club, membership=membership, amount=Decimal("150.00"), due_date=timezone.now().date(), sent_at=timezone.now())
|
||||
|
||||
rows = open_dues_rows(self.club, [self.member], self.season)
|
||||
|
||||
self.assertEqual(rows[0]["invoice"], invoice)
|
||||
|
||||
def test_only_includes_the_given_people(self):
|
||||
other_member = Member.objects.create(first_name="Tom", last_name="Roe")
|
||||
ClubMembership.objects.create(club=self.club, member=self.member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
ClubMembership.objects.create(club=self.club, member=other_member, season=self.season, fee_amount=Decimal("150.00"))
|
||||
|
||||
rows = open_dues_rows(self.club, [self.member], self.season)
|
||||
|
||||
self.assertEqual([row["membership"].member for row in rows], [self.member])
|
||||
|
||||
|
||||
class RecipientForTests(TestCase):
|
||||
"""club.services.invoicing.recipient_for -- the member's own email, else the
|
||||
first parent/guardian who has one, else nobody reachable at all."""
|
||||
|
||||
Reference in New Issue
Block a user