From 361789f0b6a26a5d399f7721ecfb6133c04b01e8 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Fri, 28 Feb 2025 16:59:03 +0100 Subject: [PATCH] Replace "instantbuy" setting on sponsorship levels with contract levels This allows explicit specification of "No contract, Click-through contract or Full contract", instead of the definitely-hard-to-grok combination of whether a contract existed and if the checkbox was set or not. --- postgresqleu/confsponsor/backendforms.py | 26 ++++++++------ .../migrations/0034_rename_instantbuy.py | 29 +++++++++++++++ postgresqleu/confsponsor/models.py | 17 ++++++++- postgresqleu/confsponsor/util.py | 3 +- postgresqleu/confsponsor/views.py | 22 ++++++------ template/confsponsor/admin_dashboard.html | 2 +- template/confsponsor/admin_sponsor.html | 8 ++--- .../confsponsor/admin_sponsor_details.html | 12 +++---- template/confsponsor/signupform.html | 35 +++++++++++-------- template/confsponsor/sponsor.html | 16 +++++---- 10 files changed, 115 insertions(+), 55 deletions(-) create mode 100644 postgresqleu/confsponsor/migrations/0034_rename_instantbuy.py diff --git a/postgresqleu/confsponsor/backendforms.py b/postgresqleu/confsponsor/backendforms.py index 10f945f0..ffa9b959 100644 --- a/postgresqleu/confsponsor/backendforms.py +++ b/postgresqleu/confsponsor/backendforms.py @@ -304,7 +304,7 @@ class BackendSponsorshipLevelBenefitCopyForm(django.forms.Form): class BackendSponsorshipLevelForm(BackendForm): helplink = 'sponsors#level' - list_fields = ['levelname', 'levelcost', 'available', 'public', ] + list_fields = ['levelname', 'levelcost', 'available', 'public', 'contractlevel'] linked_objects = OrderedDict({ 'benefit': BackendSponsorshipLevelBenefitManager(), }) @@ -315,7 +315,7 @@ class BackendSponsorshipLevelForm(BackendForm): class Meta: model = SponsorshipLevel - fields = ['levelname', 'urlname', 'levelcost', 'available', 'public', 'maxnumber', 'instantbuy', + fields = ['levelname', 'urlname', 'levelcost', 'available', 'public', 'maxnumber', 'contractlevel', 'paymentdays', 'paymentdueby', 'paymentmethods', 'invoiceextradescription', 'contract', 'canbuyvoucher', 'canbuydiscountcode'] widgets = { 'paymentmethods': django.forms.CheckboxSelectMultiple, @@ -330,7 +330,7 @@ class BackendSponsorshipLevelForm(BackendForm): { 'id': 'contract', 'legend': 'Contract information', - 'fields': ['instantbuy', 'contract', 'paymentdays', 'paymentdueby'], + 'fields': ['contractlevel', 'contract', 'paymentdays', 'paymentdueby'], }, { 'id': 'payment', @@ -353,13 +353,19 @@ class BackendSponsorshipLevelForm(BackendForm): def clean(self): cleaned_data = super(BackendSponsorshipLevelForm, self).clean() - if not (cleaned_data.get('instantbuy', False) or cleaned_data['contract']): - self.add_error('instantbuy', 'Sponsorship level must either be instant signup or have a contract') - self.add_error('contract', 'Sponsorship level must either be instant signup or have a contract') - - if int(cleaned_data['levelcost'] == 0) and cleaned_data.get('instantbuy', False): - self.add_error('levelcost', 'Sponsorships with zero cost can not be instant signup') - self.add_error('instantbuy', 'Sponsorships with zero cost can not be instant signup') + if cleaned_data['contractlevel'] == 0: + if cleaned_data['contract']: + self.add_error('contract', 'Contracts cannot be specified when contract level is No contract') + if cleaned_data['levelcost'] == 0: + self.add_error('levelcost', 'Cost cannot be zero when contract level is No contract') + elif cleaned_data['contractlevel'] == 1: + if not cleaned_data['contract']: + self.add_error('contract', 'Contract is required when contract level is Click-through') + if cleaned_data['levelcost'] == 0: + self.add_error('levelcost', 'Cost cannot be zero when contract level is Click-through') + else: + if not cleaned_data['contract']: + self.add_error('contract', 'Contract is required when contract level is Full') return cleaned_data diff --git a/postgresqleu/confsponsor/migrations/0034_rename_instantbuy.py b/postgresqleu/confsponsor/migrations/0034_rename_instantbuy.py new file mode 100644 index 00000000..0debf896 --- /dev/null +++ b/postgresqleu/confsponsor/migrations/0034_rename_instantbuy.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.11 on 2025-02-26 20:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('confsponsor', '0033_payment_terms'), + ] + + operations = [ + migrations.AddField( + model_name='sponsorshiplevel', + name='contractlevel', + field=models.IntegerField(choices=[(0, 'No contract'), (1, 'Click-through contract'), (2, 'Full contract')], default=0, verbose_name='Contract level'), + ), + migrations.RunSQL(""" +UPDATE confsponsor_sponsorshiplevel SET contractlevel=CASE +WHEN instantbuy AND contract_id IS NOT NULL THEN 1 +WHEN instantbuy AND contract_id IS NULL THEN 0 +ELSE 2 END""", + """ +UPDATE confsponsor_sponsorshiplevel SET instantbuy=(contractlevel < 2)"""), + migrations.RemoveField( + model_name='sponsorshiplevel', + name='instantbuy', + ), + ] diff --git a/postgresqleu/confsponsor/models.py b/postgresqleu/confsponsor/models.py index 5536c9b2..d6d9b404 100644 --- a/postgresqleu/confsponsor/models.py +++ b/postgresqleu/confsponsor/models.py @@ -23,6 +23,13 @@ vat_status_choices = ( (2, 'Company is from outside EU'), ) +CONTRACT_LEVEL_CHOICES = ( + (0, 'No contract'), + (1, 'Click-through contract'), + (2, 'Full contract'), +) +CONTRACT_LEVEL_MAP = dict(CONTRACT_LEVEL_CHOICES) + class SponsorshipContract(models.Model): conference = models.ForeignKey(Conference, null=False, blank=False, on_delete=models.CASCADE) @@ -47,7 +54,8 @@ class SponsorshipLevel(models.Model): public = models.BooleanField(null=False, blank=False, default=True, verbose_name="Publicly visible", help_text="If unchecked the sponsorship level will be treated as internal, for example for testing") maxnumber = models.IntegerField(null=False, blank=False, default=0, verbose_name="Maximum number of sponsors") - instantbuy = models.BooleanField(null=False, blank=False, default=False, verbose_name="Instant buy available") + contractlevel = models.IntegerField(null=False, blank=False, default=0, verbose_name="Contract level", + choices=CONTRACT_LEVEL_CHOICES) paymentmethods = models.ManyToManyField(InvoicePaymentMethod, blank=False, verbose_name="Payment methods for generated invoices") invoiceextradescription = models.TextField( blank=True, null=False, verbose_name="Invoice extra description", @@ -90,6 +98,13 @@ class SponsorshipLevel(models.Model): return True return False + @cached_property + def contractlevel_name(self): + return CONTRACT_LEVEL_MAP[self.contractlevel] + + def _display_contractlevel(self, cache): + return self.contractlevel_name + class SponsorshipBenefit(models.Model): level = models.ForeignKey(SponsorshipLevel, null=False, blank=False, on_delete=models.CASCADE) diff --git a/postgresqleu/confsponsor/util.py b/postgresqleu/confsponsor/util.py index c88d87e1..f379e393 100644 --- a/postgresqleu/confsponsor/util.py +++ b/postgresqleu/confsponsor/util.py @@ -53,7 +53,8 @@ ORDER BY max(b.sortkey), a.overview_name""", { 'cost': format_currency(lvl.levelcost), 'available': lvl.available, 'maxnumber': lvl.maxnumber, - 'instantbuy': lvl.instantbuy, + 'instantbuy': lvl.contractlevel == 1 or (lvl.contractlevel == 0 and cost > 0), # legacy + 'contractlevel': lvl.contractlevel_name, 'benefits': [dict(_get_benefit_data(b)) for b in lvl.sponsorshipbenefit_set.all() ], } diff --git a/postgresqleu/confsponsor/views.py b/postgresqleu/confsponsor/views.py index 81069648..78a0be03 100644 --- a/postgresqleu/confsponsor/views.py +++ b/postgresqleu/confsponsor/views.py @@ -222,7 +222,7 @@ def sponsor_contractview(request, sponsorid): # Should not happen raise Http404("No contract at this level") - if sponsor.level.instantbuy: + if sponsor.level.contractlevel == 1: # Click-through contract resp = HttpResponse(content_type='application/pdf') @@ -375,6 +375,9 @@ def _generate_and_send_sponsor_contract(sponsor): conference = sponsor.conference level = sponsor.level + if level.contractlevel == 0: + return + pdf = fill_pdf_fields( level.contract.contractpdf, get_pdf_fields_for_conference(conference, sponsor), @@ -386,7 +389,7 @@ def _generate_and_send_sponsor_contract(sponsor): send_sponsor_manager_email( sponsor, 'Your contract for {}'.format(conference.conferencename), - 'confsponsor/mail/{}.txt'.format('sponsor_contract_instant' if level.instantbuy else 'sponsor_contract_manual'), + 'confsponsor/mail/{}.txt'.format('sponsor_contract_instant' if level.contractlevel == 1 else 'sponsor_contract_manual'), { 'conference': conference, 'sponsor': sponsor, @@ -452,7 +455,7 @@ def sponsor_signup(request, confurlname, levelurlname): # Stage 2 = contract choice. When submitted, sign up. # If there is no contract needed on this level, or there is no choice # of contract because only one available, we bypass stage 1. - if stage == '1' and (level.instantbuy or not conference.contractprovider or not conference.manualcontracts): + if stage == '1' and (level.contractlevel != 2 or not conference.contractprovider or not conference.manualcontracts): stage = '2' def _render_contract_choices(): @@ -485,7 +488,6 @@ def sponsor_signup(request, confurlname, levelurlname): 'level': level, 'form': form, 'noform': 1, - 'needscontract': not (level.instantbuy or not conference.contractprovider), 'sponsorname': form.cleaned_data['name'], 'vatnumber': form.cleaned_data['vatnumber'] if settings.EU_VAT else None, 'previewaddr': get_sponsor_invoice_address(form.cleaned_data['name'], @@ -502,7 +504,7 @@ def sponsor_signup(request, confurlname, levelurlname): # If the Continue editing button is selected we should go back # to just rendering the normal form. Otherwise, go ahead and create the record. if request.POST.get('submit', '') != 'Continue editing': - if request.POST.get('contractchoice', '') not in ('0', '1') and not level.instantbuy: + if request.POST.get('contractchoice', '') not in ('0', '1') and level.contractlevel == 2: return _render_contract_choices() social = { @@ -520,7 +522,7 @@ def sponsor_signup(request, confurlname, levelurlname): level=level, social=social, invoiceaddr=form.cleaned_data['address'], - signmethod=1 if request.POST.get('contractchoice', '') == '1' or not conference.contractprovider or level.instantbuy else 0, + signmethod=1 if request.POST.get('contractchoice', '') == '1' or not conference.contractprovider or level.contractlevel < 2 else 0, autoapprovesigned=conference.autocontracts, ) if settings.EU_VAT: @@ -534,10 +536,10 @@ def sponsor_signup(request, confurlname, levelurlname): error = None - if level.instantbuy: - if sponsor.level.contract: - # Instantbuy levels that has a contract should get an implicit contract - # attached to an email. + if level.contractlevel < 2: + # No contract or click-through contract + if level.contractlevel == 1: + # Click-through contract _generate_and_send_sponsor_contract(sponsor) mailstr += "Level does not require a signed contract. Verify the details and approve\nthe sponsorship using:\n\n{0}/events/sponsor/admin/{1}/{2}/".format( diff --git a/template/confsponsor/admin_dashboard.html b/template/confsponsor/admin_dashboard.html index 3cfbdb84..9c243094 100644 --- a/template/confsponsor/admin_dashboard.html +++ b/template/confsponsor/admin_dashboard.html @@ -86,7 +86,7 @@ sponsor manually, you may want to confirm them manually as well... {%else%} Invoiced {%endif%} - {%elif s.level.instantbuy%} + {%elif s.level.contractlevel < 2 %} Pending organizer verification {%else%} {%if s.signmethod == 0 and s.contract %} diff --git a/template/confsponsor/admin_sponsor.html b/template/confsponsor/admin_sponsor.html index 21e48f0a..caaa7b59 100644 --- a/template/confsponsor/admin_sponsor.html +++ b/template/confsponsor/admin_sponsor.html @@ -140,8 +140,8 @@

This sponsorship is awaiting an invoice to be paid.

-{%if not sponsor.level.instantbuy%} -{%comment%}Instant buy sponsorships should never be manually confirmed{%endcomment%} +{%if sponsor.level.contractlevel == 2 %} +{%comment%}Only full contract sponsorships should be manually confirmed{%endcomment%}

Iff there is a signed and countersigned contract available for this sponsor, it can be confirmed before the invoice is paid. @@ -161,7 +161,7 @@ for this sponsor, it can be confirmed before the invoice is paid. {%else%} {%comment%}Sponsor has no invoice{%endcomment%} {%if sponsor.level.levelcost %} -{%if sponsor.level.instantbuy%} +{%if sponsor.level.contractlevel < 2%}

This sponsorship has not yet been issued an invoice. This is an "instant buy" level sponsorship, so as soon as the sponsorship @@ -191,7 +191,7 @@ This sponsorship has not yet been issued an invoice. Once the contract has been received, go ahead and generate the invoice.

{%endif%}{%comment%}Digital contracts{%endcomment%} -{%endif%}{%comment%}Instant buy{%endcomment%} +{%endif%}{%comment%}contractlevel < 2{%endcomment%} {%else%}{%comment%}levelcost != 0 {%endcomment%}

This sponsorship has zero cost, which means payment is handled manually. diff --git a/template/confsponsor/admin_sponsor_details.html b/template/confsponsor/admin_sponsor_details.html index 371c3148..dac8b191 100644 --- a/template/confsponsor/admin_sponsor_details.html +++ b/template/confsponsor/admin_sponsor_details.html @@ -39,13 +39,11 @@ Contract: - {%if sponsor.level.instantbuy %} - {%if sponsor.level.contract %} - Click-through contract completed.

{% csrf_token %}
- {%else%} + {%if sponsor.level.contractlevel == 0 %} No contract needed for this level. - {%endif%} - {%else%} + {%elif sponsor.level.contractlevel == 1 %} + Click-through contract completed. {%if not sponsor.confirmed%}
{% csrf_token %}
{%endif%} + {%else%}{%comment%}Full contract{%endcomment%} {%if sponsor.signmethod == 0%} Digital contract.
{%if sponsor.contract.completed%}Signed ({{sponsor.contract.firstsigned}}) and countersigned ({{sponsor.contract.completed}}). @@ -63,7 +61,7 @@
{% csrf_token %}
{%endif%} {%endif%}{# can resend #} - {%endif%}{# instant buy #} + {%endif%}{# contractlevel #} {%if sponsor.invoice%} diff --git a/template/confsponsor/signupform.html b/template/confsponsor/signupform.html index 534e5584..689e4d1b 100644 --- a/template/confsponsor/signupform.html +++ b/template/confsponsor/signupform.html @@ -7,14 +7,6 @@ Thank you for your interest in sponsoring {{conference}}! Please fill out the form below to initiate your sponsorship!

-{%if level.contract%} -

-Before you complete the signup form, please make sure you have read the -contract, and -agree with the contents in it. -

-{%endif%} - {%if form.errors%}

NOTE! Your submitted form contained errors and has not been saved! @@ -46,16 +38,31 @@ the invoice cannot be changed. {%if previewaddr%} -{%if needscontract%} +{%if level.contractlevel > 0%}

Contract details

+{% if level.contractlevel == 1 %} +

+ This sponsorship level uses a click-through contract. This + means that by signing up, you agree to the + contract, and + accept that no changes can be made to it. +

+{% else %} +

+ This sponsorship level requires a signed contract. Please review the + contract before + completing the sign-up, and only continue if you agree with it. + Once signed up, you will receive a contract or signing. +

+{% endif %}

The contract for the sponsorship will be issued to the company name {{sponsorname}}{%if vatnumber%} with VAT number {{vatnumber}}{%endif%}. -

-

- It will not be possible to change this after this - step, so if anything about it is incorrect please click Continue + + It will not be possible to change this, or any other + details in the contract, after this + step. If anything about it is incorrect please click Continue editing and correct it before proceeding.

{%endif%} @@ -92,7 +99,7 @@ the invoice cannot be changed. {%endif%}{# contractchoices #} -{%if not level.instantbuy and not noform %} +{%if level.contractlevle == 2 and not noform %}

Please note that due to the level of this sponsorship contract, we will require a signed contract, apart from the confirmation diff --git a/template/confsponsor/sponsor.html b/template/confsponsor/sponsor.html index da8f878e..fd81d3e3 100644 --- a/template/confsponsor/sponsor.html +++ b/template/confsponsor/sponsor.html @@ -34,20 +34,22 @@ Status: {%if sponsor.confirmed%}Confirmed ({{sponsor.confirmedat}}){%else%}Awaiting confirmation{%endif%} -{% if sponsor.confirmed and sponsor.level.contract %} +{% if sponsor.confirmed %} Contract: -{% if sponsor.level.instantbuy %} +{% if sponsor.level.contractlevel == 0 %} +This level requires no contract. +{% elif sponsor.level.contractlevel == 1 %} Click-through contract agreed to. View copy of contract {% else %} {%if sponsor.signmethod == 0%} Digital contract completed {{sponsor.contract.completed}}. {%if sponsor.contract.completed and sponsor.contract.has_completed_pdf %}View signed contract{%endif%} {% else %} -Manual contract. -{% endif %} -{% endif %} +Manual contract signed. +{% endif %}{%comment%}digital contract/manual contract{%endcomment%} +{% endif %}{%comment%}contractlevel{%endcomment%} {% endif %} @@ -73,8 +75,8 @@ Manual contract. {%endwith%} {%else%} {%comment%}No invoice generated{%endcomment%} -{%if sponsor.level.instantbuy%} -{%comment%}No invoice generated but instantbuy active, so awaiting admin approval{%endcomment%} +{%if sponsor.level.contractlevel < 2 %} +{%comment%}No invoice generated but clickthrough contract or no contract, so awaiting admin approval{%endcomment%}

Your sponsorship request has been submitted, and is currently awaiting confirmation from the conference organizers. As soon as it has been, an invoice will be automatically -- 2.39.5