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

@@ -8,6 +8,7 @@ membership.fee_status -- never a flag duplicated here that could drift out of st
"""
from datetime import timedelta
from types import SimpleNamespace
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
@@ -158,11 +159,30 @@ def render_pdf(html: str) -> bytes:
return HTML(string=html).write_pdf()
def resolve_document_address(club):
"""The address to print on an official document header (a dues invoice,
the referee payment form) -- the club's own ``legal_address`` when set,
else its home ground (``events.models.Location``, ``is_home=True``), so
a club that hasn't set a legal address yet still gets *something* rather
than a blank header.
``Location.is_home`` itself is purely about telling a home game from an
away one -- this is the one place its address doubles as a stand-in for
an actual registered/mailing address, and only when the club hasn't set
one of its own. Returns an object exposing ``.address``/``.zip_code``/
``.city`` either way (a plain namespace for the legal-address branch, the
real ``Location`` for the fallback), or ``None`` when neither is set.
"""
if club.legal_address:
return SimpleNamespace(address=club.legal_address, zip_code=club.legal_zip_code, city=club.legal_city)
return Location.objects.filter(club=club, is_home=True).first()
def invoice_pdf(invoice: DuesInvoice) -> bytes:
# 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})
# document address -- never an event-specific location, since a dues invoice
# isn't tied to any one event.
document_address = resolve_document_address(invoice.club)
html = render_to_string("club/dues_invoice_pdf.html", {"club": invoice.club, "invoice": invoice, "membership": invoice.membership, "member": invoice.membership.member, "document_address": document_address})
return render_pdf(html)