Use the club's legal name and home address on the dues invoice header

Same convention as the referee payment form (official_name, home
Location) -- a dues invoice previously just showed the everyday name
with no address at all. Also gave the referee form's PDF preview a
sample external game ID so that part of the template shows up there
too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 09:19:09 +02:00
parent 5b155fe4d4
commit 916f9bf8ab
4 changed files with 56 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
from club.models import ClubMembership, DuesInvoice
from club.services.fees import remaining_balance
from events.models import Location
class DuesInvoicePDFError(Exception):
@@ -158,5 +159,10 @@ def render_pdf(html: str) -> bytes:
def invoice_pdf(invoice: DuesInvoice) -> bytes:
html = render_to_string("club/dues_invoice_pdf.html", {"club": invoice.club, "invoice": invoice, "membership": invoice.membership, "member": invoice.membership.member})
# Same header convention as management/event_referee_form_pdf.html: the club's
# legal name (official_name falls back to the everyday name when unset) and its
# home location -- never an event-specific location, since a dues invoice isn't
# tied to any one event.
home_location = Location.objects.filter(club=invoice.club, is_home=True).first()
html = render_to_string("club/dues_invoice_pdf.html", {"club": invoice.club, "invoice": invoice, "membership": invoice.membership, "member": invoice.membership.member, "home_location": home_location})
return render_pdf(html)

View File

@@ -42,7 +42,11 @@
<body>
<div class="header">
<div>
<h1>{{ club.name }}</h1>
<h1>{{ club.official_name }}</h1>
{% if home_location %}
<div class="muted">{{ home_location.address }}</div>
<div class="muted">{{ home_location.zip_code }} {{ home_location.city }}</div>
{% endif %}
{% if club.contact_email %}<div class="muted">{{ club.contact_email }}</div>{% endif %}
</div>
<div class="right">

View File

@@ -3,6 +3,7 @@ import uuid
from contextlib import contextmanager
from decimal import Decimal
from io import StringIO
from unittest import mock
from allauth.mfa.models import Authenticator
from dateutil.relativedelta import relativedelta
@@ -17,7 +18,7 @@ from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from events.models import Event
from events.models import Event, Location
from members.models import Family, FamilyMembership, Member
from teams.models import Position, StaffAssignment, Team, TeamMembership
from teams.services import eligible_roster_members
@@ -39,7 +40,7 @@ from .services.access import (
teams_staffed_by,
)
from .services.fees import mark_as_paid, record_payment, remaining_balance
from .services.invoicing import create_or_resend_invoice, invoices_due_for_reminder, recipient_for
from .services.invoicing import create_or_resend_invoice, invoice_pdf, invoices_due_for_reminder, recipient_for
from .services.onboarding import (
annotate_onboarding_status,
approve_all_clean,
@@ -1643,6 +1644,46 @@ class CreateOrResendInvoiceTests(TestCase):
self.assertEqual(DuesInvoice.objects.filter(membership=self.membership).count(), 1)
class InvoicePdfTests(TestCase):
"""club.services.invoicing.invoice_pdf -- same header convention as
management/event_referee_form_pdf.html: legal name, and the club's home
location, never any other one."""
@classmethod
def setUpTestData(cls):
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united", legal_name="Ajax United VZW")
cls.season = make_season(cls.club)
cls.member = Member.objects.create(first_name="Jane", last_name="Doe", email="jane@example.com")
cls.membership = ClubMembership.objects.create(club=cls.club, member=cls.member, season=cls.season, status=ClubMembership.StatusChoices.PENDING, fee_amount=Decimal("150.00"))
cls.invoice = create_or_resend_invoice(cls.membership, due_in_days=14, recipient_email="jane@example.com", sent_to_guardian=False)
def render(self):
with mock.patch("club.services.invoicing.render_pdf", side_effect=lambda html: html) as renderer:
invoice_pdf(self.invoice)
return renderer.call_args[0][0]
def test_the_header_uses_the_legal_name(self):
html = self.render()
self.assertIn("Ajax United VZW", html)
def test_the_header_uses_the_clubs_home_location(self):
Location.objects.create(club=self.club, name="Sports Hall", address="Sportlaan 1", zip_code="1000", city="Brussels", is_home=True)
Location.objects.create(club=self.club, name="Away ground", address="Elsewhere 2", zip_code="2000", city="Antwerp", is_home=False)
html = self.render()
self.assertIn("Sportlaan 1", html)
self.assertNotIn("Elsewhere 2", html)
def test_renders_fine_with_no_home_location_set(self):
self.assertEqual(Location.objects.filter(club=self.club, is_home=True).count(), 0)
html = self.render()
self.assertIn(self.invoice.number, html)
class InvoicesDueForReminderTests(TestCase):
"""club.services.invoicing.invoices_due_for_reminder -- sent, unpaid, past
their own due date; never paid, waived, or not yet due."""

View File

@@ -56,7 +56,7 @@ def _referee_form_pdf_context(club, request):
start=timezone.now() + datetime.timedelta(days=7),
teams=SimpleNamespace(all=lambda: [SimpleNamespace(short_name="U16")]),
opponent="Leuven",
external_game_id="",
external_game_id="BE-2026-00417",
)
home_location = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels")
grand_total = sum((referee.total_payable for referee in referees), Decimal("0"))