New DuesInvoice model (one per membership, resendable) plus club.services.invoicing: resolves the best email to invoice (the member's own, else a parent/guardian's), snapshots the outstanding balance and a due date on send, and mails a branded HTML invoice (same club-colour email shell as the parent-claim email) with a WeasyPrint PDF attached when the native libs are available. Dues & billing gains a bulk "Send invoice" action (checkbox selection + a shared due-in-days prompt), a per-row invoice status column, a staff-facing invoice detail/PDF page, and a push-button "Send reminders" action for every sent, unpaid invoice past its own due date. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
40 lines
2.3 KiB
Python
40 lines
2.3 KiB
Python
# Generated by Django 6.0.6 on 2026-08-20 07:46
|
|
|
|
import django.db.models.deletion
|
|
import uuid
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
dependencies = [
|
|
('club', '0028_alter_onboardingrequirement_options_and_more'),
|
|
]
|
|
|
|
operations = [
|
|
migrations.CreateModel(
|
|
name='DuesInvoice',
|
|
fields=[
|
|
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
|
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('number', models.CharField(blank=True, max_length=255, verbose_name='number')),
|
|
('amount', models.DecimalField(decimal_places=2, help_text='The outstanding balance at the time this was sent — not re-read from the membership afterwards.', max_digits=10, verbose_name='amount')),
|
|
('due_date', models.DateField(verbose_name='due date')),
|
|
('sent_at', models.DateTimeField(blank=True, null=True, verbose_name='sent at')),
|
|
('sent_to_email', models.EmailField(blank=True, max_length=254, verbose_name='sent to')),
|
|
('sent_to_guardian', models.BooleanField(default=False, help_text="The member had no email on file, so a parent/guardian's was used instead.", verbose_name='sent to a parent/guardian')),
|
|
('last_reminder_sent_at', models.DateTimeField(blank=True, null=True, verbose_name='last reminder sent at')),
|
|
('reminder_count', models.PositiveIntegerField(default=0, verbose_name='reminders sent')),
|
|
('club', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='club.club')),
|
|
('membership', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dues_invoice', to='club.clubmembership', verbose_name='membership')),
|
|
],
|
|
options={
|
|
'verbose_name': 'dues invoice',
|
|
'verbose_name_plural': 'dues invoices',
|
|
'ordering': ['-sent_at'],
|
|
'constraints': [models.UniqueConstraint(fields=('club', 'number'), name='unique_dues_invoice_number_per_club')],
|
|
},
|
|
),
|
|
]
|