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:
@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from club.models import ClubMembership, DuesInvoice
|
from club.models import ClubMembership, DuesInvoice
|
||||||
from club.services.fees import remaining_balance
|
from club.services.fees import remaining_balance
|
||||||
|
from events.models import Location
|
||||||
|
|
||||||
|
|
||||||
class DuesInvoicePDFError(Exception):
|
class DuesInvoicePDFError(Exception):
|
||||||
@@ -158,5 +159,10 @@ def render_pdf(html: str) -> bytes:
|
|||||||
|
|
||||||
|
|
||||||
def invoice_pdf(invoice: DuesInvoice) -> 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)
|
return render_pdf(html)
|
||||||
|
|||||||
@@ -42,7 +42,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div>
|
<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 %}
|
{% if club.contact_email %}<div class="muted">{{ club.contact_email }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import uuid
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from allauth.mfa.models import Authenticator
|
from allauth.mfa.models import Authenticator
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
@@ -17,7 +18,7 @@ from django.test import RequestFactory, TestCase, override_settings
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from events.models import Event
|
from events.models import Event, Location
|
||||||
from members.models import Family, FamilyMembership, Member
|
from members.models import Family, FamilyMembership, Member
|
||||||
from teams.models import Position, StaffAssignment, Team, TeamMembership
|
from teams.models import Position, StaffAssignment, Team, TeamMembership
|
||||||
from teams.services import eligible_roster_members
|
from teams.services import eligible_roster_members
|
||||||
@@ -39,7 +40,7 @@ from .services.access import (
|
|||||||
teams_staffed_by,
|
teams_staffed_by,
|
||||||
)
|
)
|
||||||
from .services.fees import mark_as_paid, record_payment, remaining_balance
|
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 (
|
from .services.onboarding import (
|
||||||
annotate_onboarding_status,
|
annotate_onboarding_status,
|
||||||
approve_all_clean,
|
approve_all_clean,
|
||||||
@@ -1643,6 +1644,46 @@ class CreateOrResendInvoiceTests(TestCase):
|
|||||||
self.assertEqual(DuesInvoice.objects.filter(membership=self.membership).count(), 1)
|
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):
|
class InvoicesDueForReminderTests(TestCase):
|
||||||
"""club.services.invoicing.invoices_due_for_reminder -- sent, unpaid, past
|
"""club.services.invoicing.invoices_due_for_reminder -- sent, unpaid, past
|
||||||
their own due date; never paid, waived, or not yet due."""
|
their own due date; never paid, waived, or not yet due."""
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def _referee_form_pdf_context(club, request):
|
|||||||
start=timezone.now() + datetime.timedelta(days=7),
|
start=timezone.now() + datetime.timedelta(days=7),
|
||||||
teams=SimpleNamespace(all=lambda: [SimpleNamespace(short_name="U16")]),
|
teams=SimpleNamespace(all=lambda: [SimpleNamespace(short_name="U16")]),
|
||||||
opponent="Leuven",
|
opponent="Leuven",
|
||||||
external_game_id="",
|
external_game_id="BE-2026-00417",
|
||||||
)
|
)
|
||||||
home_location = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels")
|
home_location = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels")
|
||||||
grand_total = sum((referee.total_payable for referee in referees), Decimal("0"))
|
grand_total = sum((referee.total_payable for referee in referees), Decimal("0"))
|
||||||
|
|||||||
Reference in New Issue
Block a user