Add a club legal address, used for invoices/referee forms ahead of the home ground

New Club.legal_address/legal_zip_code/legal_city, editable from the
identity page (management:club_settings). Official document headers
(the dues invoice, the referee payment form) previously borrowed the
club's home Location for this -- conflating "where we play" with "our
registered address", which aren't always the same place. New
club.services.invoicing.resolve_document_address(club) picks the club's
own legal address when set, falling back to the home Location exactly as
before when it isn't, so nothing breaks for a club that hasn't set one
yet. Location.is_home now means only what it always should have: telling
a home game from an away one.

Renamed the shared "home_location" template/context variable to
"document_address" on both PDFs to match -- it was never accurate once a
legal address could win instead.

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 17:50:08 +02:00
parent 80fda6a4a4
commit c7887637ff
12 changed files with 159 additions and 27 deletions

View File

@@ -905,7 +905,7 @@ class ClubSettingsForm(forms.ModelForm):
class Meta:
model = Club
fields = ["name", "legal_name", "contact_email", "website", "logo", "primary_color", "secondary_color"]
fields = ["name", "legal_name", "legal_address", "legal_zip_code", "legal_city", "contact_email", "website", "logo", "primary_color", "secondary_color"]
widgets = {
"primary_color": forms.TextInput(attrs={"placeholder": "#1e40af"}),
"secondary_color": forms.TextInput(attrs={"placeholder": "#be185d"}),

View File

@@ -58,9 +58,9 @@ def _referee_form_pdf_context(club, request):
opponent="Leuven",
external_game_id="BE-2026-00417",
)
home_location = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels")
document_address = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels")
grand_total = sum((referee.total_payable for referee in referees), Decimal("0"))
return {"club": club, "event": event, "referees": referees, "home_location": home_location, "grand_total": grand_total} | referee_form_colors(club)
return {"club": club, "event": event, "referees": referees, "document_address": document_address, "grand_total": grand_total} | referee_form_colors(club)
PDF_PREVIEWS = [

View File

@@ -64,9 +64,13 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{% form_field form.name %}
{% form_field form.legal_name %}
<div class="md:col-span-2">{% form_field form.legal_address %}</div>
{% form_field form.legal_zip_code %}
{% form_field form.legal_city %}
{% form_field form.contact_email %}
{% form_field form.website %}
</div>
<p class="mt-1 text-xs text-dim">{% trans "Used on official documents (invoices, the referee payment form). Leave blank to use the club's home location instead." %}</p>
</div>
</div>

View File

@@ -69,9 +69,9 @@
<div class="header">
<div>
<div class="club-name">{{ club.official_name }}</div>
{% if home_location %}
<div class="club-address">{{ home_location.address }}</div>
<div class="club-address">{{ home_location.zip_code }} {{ home_location.city }}</div>
{% if document_address %}
<div class="club-address">{{ document_address.address }}</div>
<div class="club-address">{{ document_address.zip_code }} {{ document_address.city }}</div>
{% endif %}
</div>
<div class="doc-title">

View File

@@ -4863,6 +4863,28 @@ class ClubSettingsPreviewTests(ManagementTestBase):
self.assertEqual(self.club.secondary_color, "#654321")
self.assertEqual(self.club.website, "https://ajax-united.example")
def test_the_legal_address_can_be_set(self):
response = self.club_post(
"club_settings",
{
"name": "Ajax United",
"legal_name": "",
"legal_address": "Registered Office 5",
"legal_zip_code": "9000",
"legal_city": "Ghent",
"contact_email": "",
"website": "",
"primary_color": "",
"secondary_color": "",
},
)
self.assertRedirects(response, reverse("management:club_settings"))
self.club.refresh_from_db()
self.assertEqual(self.club.legal_address, "Registered Office 5")
self.assertEqual(self.club.legal_zip_code, "9000")
self.assertEqual(self.club.legal_city, "Ghent")
class ClubSettingsDocumentTabTests(ManagementTestBase):
"""The Email and PDF tabs on the Club identity page -- every branded
@@ -6197,7 +6219,7 @@ class EventRefereeFormPdfTests(ManagementTestBase):
context = renderer.call_args[0][0]
self.assertEqual(context["club"].official_name, "Ajax United VZW")
self.assertEqual(context["home_location"], self.home_ground)
self.assertEqual(context["document_address"], self.home_ground)
self.assertEqual(list(context["referees"]), [EventReferee.objects.get(event=game)])
def test_the_grand_total_sums_every_referees_total_payable(self):
@@ -6217,7 +6239,7 @@ class EventRefereeFormPdfTests(ManagementTestBase):
game = self.make_game()
EventReferee.objects.create(event=game, external_name="Guest Referee", assigned_by=self.admin_member)
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": list(game.referees.all()), "home_location": self.home_ground, "grand_total": Decimal("0")})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": list(game.referees.all()), "document_address": self.home_ground, "grand_total": Decimal("0")})
self.assertIn("Guest Referee", html)
self.assertNotIn("External", html)
@@ -6226,7 +6248,7 @@ class EventRefereeFormPdfTests(ManagementTestBase):
game = self.make_game()
EventReferee.objects.create(event=game, member=self.referee, assigned_by=self.admin_member, fee=Decimal("25.00"), km=Decimal("40"), km_rate=Decimal("0.083"))
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": list(game.referees.all()), "home_location": self.home_ground, "grand_total": Decimal("28.320")})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": list(game.referees.all()), "document_address": self.home_ground, "grand_total": Decimal("28.320")})
self.assertIn("€28.32<", html)
self.assertNotIn("28.320", html)
@@ -6237,14 +6259,14 @@ class EventRefereeFormPdfTests(ManagementTestBase):
self.club.save(update_fields=["primary_color", "secondary_color"])
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "document_address": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertIn("--accent: #0f766e", html)
def test_the_pdf_falls_back_to_default_colours_when_unset(self):
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "document_address": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertIn("--accent: #3730a3", html)
@@ -6254,7 +6276,7 @@ class EventRefereeFormPdfTests(ManagementTestBase):
# silently renders with no background at all.
game = self.make_game()
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "home_location": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
html = render_to_string("management/event_referee_form_pdf.html", {"club": self.club, "event": game, "referees": [], "document_address": self.home_ground, "grand_total": Decimal("0"), **referee_form_colors(self.club)})
self.assertNotIn("color-mix(", html)
self.assertIn("--info-card-bg: #", html)

View File

@@ -30,7 +30,7 @@ from club.mixins import (
from club.models import ClubMembership, ClubRole, DuesInvoice, MemberRequirementStatus, OnboardingRequirement, Season, Sponsor
from club.services.access import _guardians_only, can_edit_news, can_publish_news, current_season, groups_manageable_by, is_club_admin, members_visible_to, teams_managed_by, teams_staffed_by
from club.services.fees import mark_as_paid, record_payment, remaining_balance
from club.services.invoicing import DuesInvoicePDFError, create_or_resend_invoice, invoice_pdf, invoices_due_for_reminder, recipient_for, send_invoice_email, send_reminders
from club.services.invoicing import DuesInvoicePDFError, create_or_resend_invoice, invoice_pdf, invoices_due_for_reminder, recipient_for, resolve_document_address, send_invoice_email, send_reminders
from club.services.onboarding import annotate_onboarding_status, approve_all_clean, approve_one, blocking_event_kinds, checklist_for, is_signup_clean, mark_bypassed, mark_complete, mark_incomplete, members_with_open_requirements
from controlpanel.messages import notify
from controlpanel.mixins import RedirectOnInvalidMixin
@@ -2642,16 +2642,18 @@ class EventRefereeFeeUpdateView(ClubAdminRequiredMixin, FormView):
class EventRefereeFormPdfView(ClubAdminRequiredMixin, View):
"""Downloadable PDF of the referee payment form for one game, modeled on
the club's existing paper form -- club header (legal name if set, else
plain name; address from the club's home Location, not this specific
event's, so the form still reads right even if called from a page where
the event's own location happens to be blank) plus this game's details,
referees and their fee/km breakdown, and blank signature lines."""
plain name; address from the club's own legal address, or its home
Location when that's blank -- see club.services.invoicing.
resolve_document_address -- never this specific event's, so the form
still reads right even if called from a page where the event's own
location happens to be blank) plus this game's details, referees and
their fee/km breakdown, and blank signature lines."""
def get(self, request, pk):
event = get_object_or_404(Event.objects.filter(club=request.club).prefetch_related("teams", "referees__member"), pk=pk)
home_location = Location.objects.filter(club=request.club, is_home=True).first()
document_address = resolve_document_address(request.club)
referees = list(event.referees.all())
context = {"club": request.club, "event": event, "referees": referees, "home_location": home_location, "grand_total": sum((referee.total_payable for referee in referees), Decimal("0"))} | referee_form_colors(request.club)
context = {"club": request.club, "event": event, "referees": referees, "document_address": document_address, "grand_total": sum((referee.total_payable for referee in referees), Decimal("0"))} | referee_form_colors(request.club)
try:
pdf = event_referee_form_pdf(context)