From ee701d698733294805f18ea000d53a5a542d3312 Mon Sep 17 00:00:00 2001 From: Steve Singer Date: Sun, 24 Nov 2024 09:47:57 +0100 Subject: [PATCH] Allow sponsorship payment terms(due date) to be changed Allow the due date to be configured on an individual sponsorship level, both specifying a number-of-days-to-pay and a hard final date. Closes #170 --- docs/confreg/sponsors.md | 11 ++++++ postgresqleu/confsponsor/backendforms.py | 8 +++-- postgresqleu/confsponsor/invoicehandler.py | 23 +++++------- .../migrations/0033_payment_terms.py | 35 +++++++++++++++++++ postgresqleu/confsponsor/models.py | 5 +++ 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 postgresqleu/confsponsor/migrations/0033_payment_terms.py diff --git a/docs/confreg/sponsors.md b/docs/confreg/sponsors.md index a05061019..17a083fe1 100644 --- a/docs/confreg/sponsors.md +++ b/docs/confreg/sponsors.md @@ -341,6 +341,17 @@ Instant buy available administrator must manually move the sponsorship forward in the process once a signed contract is received. +Number of days until payment is due +: The number of days until a sponsorship invoice is due. This defaults to 30 + to give net 30 terms. The actual due date for an invoice might be restricted + by either *The Date the payment is due by* field. + +The Date the payment is due by +: The latest date that *Number of days until payment is due* applies until. + Invoices that would be due after this date are instead due at this time or + now(if this time is in the past). This defaults to 5 days before the conference + starts. + Payment methods for generated invoices : Which payment methods will be listed on the generated invoices. Typically the instant buy levels support payment by diff --git a/postgresqleu/confsponsor/backendforms.py b/postgresqleu/confsponsor/backendforms.py index d85656a19..acd295fa1 100644 --- a/postgresqleu/confsponsor/backendforms.py +++ b/postgresqleu/confsponsor/backendforms.py @@ -4,6 +4,7 @@ from django.conf import settings from collections import OrderedDict +import datetime import json from postgresqleu.util.db import exec_to_scalar @@ -309,11 +310,12 @@ class BackendSponsorshipLevelForm(BackendForm): }) allow_copy_previous = True auto_cascade_delete_to = ['sponsorshiplevel_paymentmethods', 'sponsorshipbenefit'] + exclude_date_validators = ['paymentdueby', ] class Meta: model = SponsorshipLevel fields = ['levelname', 'urlname', 'levelcost', 'available', 'public', 'maxnumber', 'instantbuy', - 'paymentmethods', 'invoiceextradescription', 'contract', 'canbuyvoucher', 'canbuydiscountcode'] + 'paymentdays', 'paymentdueby', 'paymentmethods', 'invoiceextradescription', 'contract', 'canbuyvoucher', 'canbuydiscountcode'] widgets = { 'paymentmethods': django.forms.CheckboxSelectMultiple, } @@ -327,7 +329,7 @@ class Meta: { 'id': 'contract', 'legend': 'Contract information', - 'fields': ['instantbuy', 'contract', ], + 'fields': ['instantbuy', 'contract', 'paymentdays', 'paymentdueby'], }, { 'id': 'payment', @@ -344,6 +346,8 @@ class Meta: def fix_fields(self): self.fields['contract'].queryset = SponsorshipContract.objects.filter(conference=self.conference) self.fields['paymentmethods'].label_from_instance = lambda x: "{0}{1}".format(x.internaldescription, x.active and " " or " (INACTIVE)") + if not self.initial.get('paymentdueby', None): + self.initial['paymentdueby'] = self.conference.startdate - datetime.timedelta(days=5) def clean(self): cleaned_data = super(BackendSponsorshipLevelForm, self).clean() diff --git a/postgresqleu/confsponsor/invoicehandler.py b/postgresqleu/confsponsor/invoicehandler.py index ccdc6f496..c74f19294 100644 --- a/postgresqleu/confsponsor/invoicehandler.py +++ b/postgresqleu/confsponsor/invoicehandler.py @@ -1,7 +1,6 @@ from django.utils import timezone from django.conf import settings - -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time import base64 import os @@ -172,23 +171,19 @@ def create_sponsor_invoice(user, sponsor, override_duedate=None): level = sponsor.level invoicerows, reverse_vat = _invoicerows_for_sponsor(sponsor) + daystopay = timedelta(days=level.paymentdays) if override_duedate: duedate = override_duedate - elif conference.startdate < today_conference() + timedelta(days=5): - # If conference happens in the next 5 days, invoice is due immediately + elif level.paymentdueby < today_conference(): + # The payment deadline has passed. Invoices are due immediately duedate = timezone.now() - elif conference.startdate < today_conference() + timedelta(days=30): - # Less than 30 days before the conference, set the due date to - # 5 days before the conference - duedate = timezone.make_aware(datetime.combine( - conference.startdate - timedelta(days=5), - timezone.now().time() - )) + elif level.paymentdueby < today_conference() + daystopay: + # The payment terms go beyond the payment deadline. The payment is due + # at the deadline + duedate = datetime.combine(level.paymentdueby, time(0, 0, 0, 0), conference.tzobj) else: - # More than 30 days before the conference, set the due date - # to 30 days from now. - duedate = timezone.now() + timedelta(days=30) + duedate = timezone.now() + daystopay manager = InvoiceManager() processor = invoicemodels.InvoiceProcessor.objects.get(processorname="confsponsor processor") diff --git a/postgresqleu/confsponsor/migrations/0033_payment_terms.py b/postgresqleu/confsponsor/migrations/0033_payment_terms.py new file mode 100644 index 000000000..d7fa7aa76 --- /dev/null +++ b/postgresqleu/confsponsor/migrations/0033_payment_terms.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.22 on 2024-11-15 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('confsponsor', '0032_sponsorshipbenefit_include_in_data'), + ] + + operations = [ + migrations.AddField( + model_name='sponsorshiplevel', + name='paymentdueby', + field=models.DateField(blank=True, help_text='The last acceptable due date for payments. If payment terms go beyond this date then the invoice is due at this date', null=True, verbose_name='The latest date the payment is due by'), + ), + migrations.RunSQL( + """ + update confsponsor_sponsorshiplevel set paymentdueby=conf.startdate-'5 days'::interval from confreg_conference conf + where conf.id = conference_id + """, + "" + ), + migrations.AlterField( + model_name='sponsorshiplevel', + name='paymentdueby', + field=models.DateField(blank=False, help_text='The last acceptable due date for payments. If payment terms go beyond this date then the invoice is due at this date', null=False, verbose_name='The latest date the payment is due by'), + ), + migrations.AddField( + model_name='sponsorshiplevel', + name='paymentdays', + field=models.IntegerField(default=30, null=False, verbose_name='Number of days until payment is due'), + ), + ] diff --git a/postgresqleu/confsponsor/models.py b/postgresqleu/confsponsor/models.py index 510071b3a..f6ab9b594 100644 --- a/postgresqleu/confsponsor/models.py +++ b/postgresqleu/confsponsor/models.py @@ -56,6 +56,11 @@ class SponsorshipLevel(models.Model): contract = models.ForeignKey(SponsorshipContract, blank=True, null=True, on_delete=models.CASCADE) canbuyvoucher = models.BooleanField(null=False, blank=False, default=True, verbose_name="Can buy vouchers") canbuydiscountcode = models.BooleanField(null=False, blank=False, default=True, verbose_name="Can buy discount codes") + paymentdays = models.IntegerField(null=False, blank=False, default=30, verbose_name="Number of days until payment is due") + paymentdueby = models.DateField( + null=False, blank=False, verbose_name="The latest date the payment is due by", + help_text="The last acceptable due date for payments. If payment terms go beyond this date then the invoice is due at this date", + ) def __str__(self): return self.levelname