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:
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.6 on 2026-08-21 15:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('club', '0029_duesinvoice'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='legal_address',
|
||||
field=models.CharField(blank=True, help_text="Street address for official documents (invoices, the referee payment form). Falls back to the home location's address when left blank -- set this when the club's registered address isn't where it actually plays.", max_length=255, verbose_name='legal address'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='legal_city',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='legal city'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='club',
|
||||
name='legal_zip_code',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='legal zip code'),
|
||||
),
|
||||
]
|
||||
@@ -50,6 +50,15 @@ class Club(UUIDModel):
|
||||
)
|
||||
website = models.URLField(_("website"), blank=True, help_text=_("The club's own site, if it has one -- shown alongside its RosterChief pages, not used for anything else yet."))
|
||||
|
||||
legal_address = models.CharField(
|
||||
_("legal address"),
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=_("Street address for official documents (invoices, the referee payment form). Falls back to the home location's address when left blank -- set this when the club's registered address isn't where it actually plays."),
|
||||
)
|
||||
legal_zip_code = models.CharField(_("legal zip code"), max_length=255, blank=True)
|
||||
legal_city = models.CharField(_("legal city"), max_length=255, blank=True)
|
||||
|
||||
logo = models.FileField(
|
||||
_("logo"),
|
||||
upload_to=club_logo_path,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<div class="header">
|
||||
<div>
|
||||
<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>
|
||||
{% if document_address %}
|
||||
<div class="muted">{{ document_address.address }}</div>
|
||||
<div class="muted">{{ document_address.zip_code }} {{ document_address.city }}</div>
|
||||
{% endif %}
|
||||
{% if club.contact_email %}<div class="muted">{{ club.contact_email }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -40,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, invoice_pdf, invoices_due_for_reminder, recipient_for
|
||||
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,
|
||||
approve_all_clean,
|
||||
@@ -1683,6 +1683,48 @@ class InvoicePdfTests(TestCase):
|
||||
|
||||
self.assertIn(self.invoice.number, html)
|
||||
|
||||
def test_the_legal_address_takes_precedence_over_the_home_location(self):
|
||||
Location.objects.create(club=self.club, name="Sports Hall", address="Sportlaan 1", zip_code="1000", city="Brussels", is_home=True)
|
||||
self.club.legal_address = "Registered Office 5"
|
||||
self.club.legal_zip_code = "9000"
|
||||
self.club.legal_city = "Ghent"
|
||||
self.club.save(update_fields=["legal_address", "legal_zip_code", "legal_city"])
|
||||
|
||||
html = self.render()
|
||||
|
||||
self.assertIn("Registered Office 5", html)
|
||||
self.assertNotIn("Sportlaan 1", html)
|
||||
|
||||
|
||||
class ResolveDocumentAddressTests(TestCase):
|
||||
"""club.services.invoicing.resolve_document_address -- the club's own
|
||||
legal_address when set, else its home Location, else None."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = Club.objects.create(name="Ajax United", slug="ajax-united")
|
||||
|
||||
def test_returns_none_with_neither_set(self):
|
||||
self.assertIsNone(resolve_document_address(self.club))
|
||||
|
||||
def test_falls_back_to_the_home_location(self):
|
||||
home = Location.objects.create(club=self.club, name="Sports Hall", address="Sportlaan 1", zip_code="1000", city="Brussels", is_home=True)
|
||||
|
||||
self.assertEqual(resolve_document_address(self.club), home)
|
||||
|
||||
def test_legal_address_wins_over_the_home_location(self):
|
||||
Location.objects.create(club=self.club, name="Sports Hall", address="Sportlaan 1", zip_code="1000", city="Brussels", is_home=True)
|
||||
self.club.legal_address = "Registered Office 5"
|
||||
self.club.legal_zip_code = "9000"
|
||||
self.club.legal_city = "Ghent"
|
||||
self.club.save(update_fields=["legal_address", "legal_zip_code", "legal_city"])
|
||||
|
||||
address = resolve_document_address(self.club)
|
||||
|
||||
self.assertEqual(address.address, "Registered Office 5")
|
||||
self.assertEqual(address.zip_code, "9000")
|
||||
self.assertEqual(address.city, "Ghent")
|
||||
|
||||
|
||||
class InvoicesDueForReminderTests(TestCase):
|
||||
"""club.services.invoicing.invoices_due_for_reminder -- sent, unpaid, past
|
||||
|
||||
Reference in New Issue
Block a user