Implement "additional contracts" for sponsors
authorMagnus Hagander <magnus@hagander.net>
Fri, 27 Oct 2023 14:17:18 +0000 (16:17 +0200)
committerMagnus Hagander <magnus@hagander.net>
Fri, 27 Oct 2023 14:19:22 +0000 (16:19 +0200)
This allows the management of additional contracts for each sponsor,
such as training contracts. If configured, this integrates with the
digital contract signing system, making it possible to handle these
contracts digitally as well. And if not, it still gets contract
templating the same way as the main sponsor contracts.

docs/confreg/sponsors.md
postgresqleu/confsponsor/apps.py
postgresqleu/confsponsor/forms.py
postgresqleu/confsponsor/invoicehandler.py
postgresqleu/confsponsor/migrations/0027_sponsoradditionalcontract.py [new file with mode: 0644]
postgresqleu/confsponsor/models.py
postgresqleu/confsponsor/urls.py
postgresqleu/confsponsor/views.py
template/confsponsor/admin_dashboard.html
template/confsponsor/admin_sponsor.html

index 9d6429270539a3ed03ce6419bca0212bc069f9ae..d077f197e02b6c357c5d747a38e493025ad1dc78 100644 (file)
@@ -270,6 +270,34 @@ Number of parcels arrived is changed
 :  An email is generated to the *sponsor*, as well as to the
    conference sponsorship address.
 
+## Additional contracts
+
+Other than the contract for the sponsorship itself, it is also
+possible to send "additional contracts" to the sponsors. This can for
+example be used for training contracts, or any other special activity
+that requires a contract.
+
+In particular this is useful when the sponsor uses digital contracts
+as this process is then also handled by digital contracts using the
+same address and provider. There is no automatic processing for the
+additional contracts, other than updating the status of the contract
+indicating who has signed and when.
+
+Additional contracts can be used for manual contracts as well, in
+which case just like with the digital contracts, the only real benefit
+is that some fields like the sponsor name can be pre-filled in the
+contract using the same template system as for the main sponsor
+contracts.
+
+Each contract is given a subject (used as e-mail subject and the title
+of the contract in the digital signature provider) and message (used
+as the body of the email sent, whether manual or digital).
+
+The status of the contracts can be tracked on the page of each
+individual sponsor (where manual contracts should also be marked when
+they are signed by the different actors) as well as in a global
+overview on the sponsorship dashboard.
+
 ## Reference
 
 ### Sponsorship <a name="sponsor"></a>
index 33968b04604be14b64987873f48d55922a66761a..a538d0ad4267d185b18931375310000ddf49bbb8 100644 (file)
@@ -6,6 +6,7 @@ class ConfsponsorAppConfig(AppConfig):
 
     def ready(self):
         from postgresqleu.digisign.util import register_digisign_handler
-        from postgresqleu.confsponsor.invoicehandler import SponsorDigisignHandler
+        from postgresqleu.confsponsor.invoicehandler import SponsorDigisignHandler, SponsorAdditionalDigisignHandler
 
         register_digisign_handler('confsponsor', SponsorDigisignHandler)
+        register_digisign_handler('confsponsoradditional', SponsorAdditionalDigisignHandler)
index 1ff57b8ccd497c5e73921dca31407c99d141aa4a..58f6bcd27601bfe00dbab04e59a4e2dc44f115f4 100644 (file)
@@ -3,9 +3,10 @@ from django.forms import ValidationError
 from django.forms.utils import ErrorList
 from django.db.models import Q
 from django.core.validators import MaxValueValidator, MinValueValidator
+from django.contrib.auth.models import User
 from django.conf import settings
 
-from .models import Sponsor, SponsorMail, SponsorshipLevel
+from .models import Sponsor, SponsorMail, SponsorshipLevel, SponsorshipContract
 from .models import vat_status_choices
 from .models import Shipment
 from postgresqleu.confreg.models import RegistrationType, DiscountCode
@@ -13,6 +14,7 @@ from postgresqleu.countries.models import EuropeCountry
 
 from postgresqleu.confreg.models import ConferenceAdditionalOption
 from postgresqleu.confreg.twitter import get_all_conference_social_media
+from postgresqleu.util.fields import UserModelChoiceField
 from postgresqleu.util.validators import BeforeValidator, AfterValidator
 from postgresqleu.util.validators import Http200Validator
 from postgresqleu.util.widgets import Bootstrap4CheckboxSelectMultiple, EmailTextWidget
@@ -314,3 +316,18 @@ class ShipmentReceiverForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super(ShipmentReceiverForm, self).__init__(*args, **kwargs)
         self.fields['arrived_parcels'].choices = [(str(x), str(x)) for x in range(1, 20)]
+
+
+class SponsorAddContractForm(forms.Form):
+    subject = forms.CharField(max_length=100, required=True)
+    contract = forms.ModelChoiceField(SponsorshipContract.objects.all())
+    manager = UserModelChoiceField(User.objects.all(), label="Manager to send to")
+    message = forms.CharField(label="Message to send in signing email", widget=forms.Textarea)
+
+    def __init__(self, sponsor, *args, **kwargs):
+        self.sponsor = sponsor
+        super().__init__(*args, **kwargs)
+
+        self.fields['subject'].help_text = "Subject of contract, for example 'Training contract'. Will be prefixed with '[{}]' in all emails.".format(self.sponsor.conference.conferencename)
+        self.fields['contract'].queryset = SponsorshipContract.objects.filter(conference=self.sponsor.conference, sponsorshiplevel=None)
+        self.fields['manager'].queryset = self.sponsor.managers
index e022708aa5e8ccce8fbe4eb7d72bb7bfe453981e..869506e7fb3e9b77e0edb4722e4dd0eecf4af8b7 100644 (file)
@@ -456,3 +456,65 @@ class SponsorDigisignHandler(DigisignHandlerBase):
                 "Contract signed for sponsor {}".format(self.sponsor.name),
                 "The digital contract for sponsor\n{}\n has been signed by\n{}.\n It is now pending signature from {}.\n".format(self.sponsor.name, signedby, self.sponsor.conference.contractsendername),
             )
+
+
+class SponsorAdditionalDigisignHandler(DigisignHandlerBase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        if not hasattr(self.doc, 'sponsoradditionalcontract'):
+            raise Exception("No sponsor additional contract found for this document, something got unlinked?")
+        self.acontract = self.doc.sponsoradditionalcontract
+        self.sponsor = self.acontract.sponsor
+
+    def completed(self):
+        super().completed()
+
+        self.acontract.completed = timezone.now()
+        self.acontract.save(update_fields=['completed'])
+        send_conference_sponsor_notification(
+            self.sponsor.conference,
+            "Digital contract signed for {}".format(self.sponsor.conference.conferencename),
+            "A digital contract with the subject '{}' sent to {} has been signed by both the sponsor and {}.".format(self.acontract.subject, self.sponsor.name, self.sponsor.conference.contractsendername),
+        )
+
+    def expired(self):
+        super().expired()
+
+        send_conference_sponsor_notification(
+            self.sponsor.conference,
+            "Digital contract expired for {}".format(self.sponsor.conference.conferencename),
+            "A digital contract with the subject '{}' sent to {} has expired.".format(self.acontract.subject, self.sponsor.name),
+        )
+
+    def declined(self):
+        super().declined()
+
+        send_conference_sponsor_notification(
+            self.sponsor.conference,
+            "Digital contract declined for {}".format(self.sponsor.conference.conferencename),
+            "A digital contract with the subject '{}' sent to {} has been declined.".format(self.acontract.subject, self.sponsor.name),
+        )
+
+    def canceled(self):
+        super().canceled()
+
+        send_conference_sponsor_notification(
+            self.sponsor.conference,
+            "Digital contract canceled for {}".format(self.sponsor.conference.conferencename),
+            "A digital contract with the subject '{}' sent to {} has been canceled.".format(self.acontract.subject, self.sponsor.name),
+        )
+
+    def signed(self, signedby):
+        super().signed(signedby)
+
+        if signedby != self.sponsor.conference.contractsendername:
+            # If it's the other party that signed, send an email to notify the administrators,
+            # for the record. When the organizers sign, the "completed" notification is fired,
+            # and the email is sent from there.
+            send_conference_sponsor_notification(
+                self.sponsor.conference,
+                "Digital contract signed by sponsor {}".format(self.sponsor.name),
+                "A digital contract with the subject '{}' has been signed by {}.\nIt is now pending signature from {}.\n".format(self.acontract.subject, signedby, self.sponsor.conference.contractsendername),
+            )
+            self.acontract.sponsorsigned = timezone.now()
+            self.acontract.save(update_fields=['sponsorsigned'])
diff --git a/postgresqleu/confsponsor/migrations/0027_sponsoradditionalcontract.py b/postgresqleu/confsponsor/migrations/0027_sponsoradditionalcontract.py
new file mode 100644 (file)
index 0000000..b0a2164
--- /dev/null
@@ -0,0 +1,30 @@
+# Generated by Django 3.2.14 on 2023-10-27 13:01
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('digisign', '0002_contract_dates'),
+        ('confsponsor', '0026_sponsor_twittername_social'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SponsorAdditionalContract',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('subject', models.CharField(max_length=100)),
+                ('sponsorsigned', models.DateTimeField(blank=True, null=True)),
+                ('completed', models.DateTimeField(blank=True, null=True)),
+                ('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='confsponsor.sponsorshipcontract')),
+                ('digitalcontract', models.OneToOneField(blank=True, help_text='Contract, when using digital signatures', null=True, on_delete=django.db.models.deletion.SET_NULL, to='digisign.digisigndocument')),
+                ('sent_to_manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+                ('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='confsponsor.sponsor')),
+            ],
+        ),
+    ]
index b13d0ff8547168e079b41cc13633cb02e35866da..d7c5cfb7d17fa25e300c02a5d7d09a24c5a346eb 100644 (file)
@@ -289,3 +289,13 @@ class Shipment(models.Model):
         if self.sponsor:
             return self.sponsor.name
         return "{0} organizers".format(self.conference)
+
+
+class SponsorAdditionalContract(models.Model):
+    sponsor = models.ForeignKey(Sponsor, null=False, blank=False, on_delete=models.CASCADE)
+    subject = models.CharField(max_length=100, null=False, blank=False)
+    contract = models.ForeignKey(SponsorshipContract, null=False, blank=False, on_delete=models.CASCADE)
+    sent_to_manager = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
+    digitalcontract = models.OneToOneField(DigisignDocument, null=True, blank=True, help_text="Contract, when using digital signatures", on_delete=models.SET_NULL)
+    sponsorsigned = models.DateTimeField(null=True, blank=True)
+    completed = models.DateTimeField(null=True, blank=True)
index d2ffc04d0ed77f02cfcb9331c2fc5436df88c45c..2ad5fcfe5b36bcc277686d7b33b7bf436807bc9b 100644 (file)
@@ -32,6 +32,8 @@ urlpatterns = [
     url(r'^admin/(\w+)/(\d+)/edit/$', backendviews.edit_sponsor),
     url(r'^admin/(\w+)/(\d+)/contractlog/$', views.sponsor_admin_sponsor_contractlog),
     url(r'^admin/(\w+)/(\d+)/resendcontract/$', views.sponsor_admin_sponsor_resendcontract),
+    url(r'^admin/(\w+)/(\d+)/addcontract/$', views.sponsor_admin_addcontract),
+    url(r'^admin/(\w+)/(\d+)/markaddcontract/$', views.sponsor_admin_markaddcontract),
     url(r'^admin/(\w+)/benefit/(\d+)/$', views.sponsor_admin_benefit),
     url(r'^admin/(\w+)/sendmail/$', views.sponsor_admin_send_mail),
     url(r'^admin/(\w+)/viewmail/(\d+)/$', views.sponsor_admin_view_mail),
index 67a7f0c3c6d50b33fb83aadfcdd375d78854c911..6db589d198656b820cd22f07bd46ef6afe6a2734 100644 (file)
@@ -8,6 +8,7 @@ from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.models import User
 from django.utils import timezone
+from django.template.defaultfilters import slugify
 
 from datetime import timedelta
 import io
@@ -31,20 +32,21 @@ from postgresqleu.digisign.pdfutil import fill_pdf_fields, pdf_watermark_preview
 from postgresqleu.digisign.models import DigisignDocument, DigisignLog
 
 from .models import Sponsor, SponsorshipLevel, SponsorshipBenefit
-from .models import SponsorClaimedBenefit, SponsorMail, SponsorshipContract
+from .models import SponsorClaimedBenefit, SponsorMail, SponsorshipContract, SponsorAdditionalContract
 from .models import PurchasedVoucher
 from .models import ShipmentAddress, Shipment
 from .forms import SponsorSignupForm, SponsorSendEmailForm, SponsorDetailsForm
 from .forms import PurchaseVouchersForm, PurchaseDiscountForm
 from .forms import SponsorShipmentForm, ShipmentReceiverForm
 from .forms import SponsorRefundForm, SponsorReissueForm
+from .forms import SponsorAddContractForm
 
 from .benefits import get_benefit_class
 from .invoicehandler import create_sponsor_invoice, confirm_sponsor
 from .invoicehandler import get_sponsor_invoice_address, get_sponsor_invoice_rows
 from .invoicehandler import create_voucher_invoice
 from .vatutil import validate_eu_vat_number
-from .util import send_conference_sponsor_notification, send_sponsor_manager_email
+from .util import send_conference_sponsor_notification, send_sponsor_manager_email, send_sponsor_manager_simple_email
 from .util import get_mails_for_sponsor
 from .util import get_pdf_fields_for_conference
 
@@ -944,6 +946,7 @@ ORDER BY l.levelcost DESC, l.levelname, s.name, b.sortkey, b.benefitname""", {'c
         'benefitmatrix': benefitmatrix,
         'has_shipment_tracking': has_shipment_tracking,
         'shipments': shipments,
+        'additionalcontracts': SponsorAdditionalContract.objects.select_related('sponsor').filter(sponsor__conference=conference).order_by('id'),
         'helplink': 'sponsors',
         })
 
@@ -1144,6 +1147,9 @@ def sponsor_admin_sponsor(request, confurlname, sponsorid):
         'claimedbenefits': claimedbenefits,
         'unclaimedbenefits': unclaimedbenefits,
         'noclaimbenefits': noclaimbenefits,
+        'conference_has_contracts': SponsorshipContract.objects.filter(conference=conference, sponsorshiplevel=None).exists(),
+        'additionalcontracts': SponsorAdditionalContract.objects.filter(sponsor=sponsor).order_by('id'),
+        'addcontractform': SponsorAddContractForm(sponsor),
         'breadcrumbs': (('/events/sponsor/admin/{0}/'.format(conference.urlname), 'Sponsors'),),
         'euvat': settings.EU_VAT,
         'helplink': 'sponsors',
@@ -1733,3 +1739,146 @@ def sponsor_admin_reissue(request, confurlname, sponsorid):
         'breadcrumbs': [('../../', 'Sponsors'), ('../', sponsor.name), ],
         'helplink': 'sponsors',
     })
+
+
+@login_required
+@transaction.atomic
+def sponsor_admin_addcontract(request, confurlname, sponsorid):
+    if request.method != "POST":
+        return HttpResponse("Invalid method.", status=400)
+
+    conference = get_authenticated_conference(request, confurlname)
+    sponsor = get_object_or_404(Sponsor, id=sponsorid, conference=conference)
+
+    if not sponsor.confirmed:  # Cannot-happen
+        raise Http404("Page not valid for unconfirmed sponsors")
+
+    form = SponsorAddContractForm(sponsor, data=request.POST)
+    if not form.is_valid():
+        # Should not be possible unless the browser is broken, so we accept a bad error message
+        messages.error(request, "Form does not validate.")
+        return HttpResponseRedirect("../")
+
+    contract = form.cleaned_data['contract']
+    subject = form.cleaned_data['subject']
+
+    # Send the actual contract
+    pdf = fill_pdf_fields(
+        contract.contractpdf,
+        get_pdf_fields_for_conference(conference, sponsor),
+        contract.fieldjson,
+    )
+    if sponsor.signmethod == 1:
+        send_sponsor_manager_simple_email(
+            sponsor,
+            subject,
+            form.cleaned_data['message'],
+            attachments=[
+                ('{}_{}.pdf'.format(conference.urlname, slugify(subject)), 'application/pdf', pdf),
+            ],
+        )
+        SponsorAdditionalContract(
+            sponsor=sponsor,
+            subject=subject,
+            contract=contract,
+            sent_to_manager=None,
+            digitalcontract=None,
+        ).save()
+        messages.info(request, "Manual contract sent.")
+        send_conference_sponsor_notification(
+            conference,
+            "Manual contract sent to {} for {}".format(sponsor.name, conference.conferencename),
+            'A manual contract with the subject "{}" was sent to {}'.format(subject, sponsor.name),
+        )
+    else:
+        manager = form.cleaned_data['manager']
+
+        acontract = SponsorAdditionalContract(
+            sponsor=sponsor,
+            subject=subject,
+            contract=contract,
+            sent_to_manager=manager,
+            digitalcontract=None,
+        )
+        acontract.save()
+
+        signer = conference.contractprovider.get_implementation()
+        contractid, error = signer.send_contract(
+            conference.contractsendername,
+            conference.contractsenderemail,
+            "{} {}".format(manager.first_name, manager.last_name),
+            manager.email,
+            pdf,
+            '{}_{}.pdf'.format(conference.urlname, slugify(subject)),
+            subject,
+            form.cleaned_data['message'],
+            {
+                'type': 'sponsoradditional',
+                'additionalid': str(acontract.id),
+            },
+            contract.fieldjson,
+            conference.contractexpires,
+            test=False,
+        )
+        if error:
+            messages.error(request, "Failed to send digital contract.")
+            return HttpResponseRedirect("../")
+
+        acontract.digitalcontract = DigisignDocument(
+            provider=conference.contractprovider,
+            documentid=contractid,
+            handler='confsponsoradditional',
+        )
+        acontract.digitalcontract.save()
+        acontract.save(update_fields=['digitalcontract', ])
+        messages.info(request, "Digital contract sent.")
+
+    return HttpResponseRedirect("../")
+
+
+@login_required
+@transaction.atomic
+def sponsor_admin_markaddcontract(request, confurlname, sponsorid):
+    if request.method != "POST":
+        return HttpResponse("Invalid method.", status=400)
+
+    conference = get_authenticated_conference(request, confurlname)
+    sponsor = get_object_or_404(Sponsor, id=sponsorid, conference=conference)
+
+    if not sponsor.confirmed:  # Cannot-happen
+        raise Http404("Page not valid for unconfirmed sponsors")
+
+    acontract = get_object_or_404(SponsorAdditionalContract, pk=get_int_or_error(request.POST, "id"))
+    if acontract.digitalcontract:
+        raise Http404("Page not valid for digital contracts")
+
+    which = request.POST.get('which', '')
+
+    if which == 'sponsor':
+        if acontract.sponsorsigned:
+            messages.error(request, "Contract already marked as signed by sponsor.")
+        else:
+            acontract.sponsorsigned = timezone.now()
+            acontract.save(update_fields=['sponsorsigned'])
+            send_conference_sponsor_notification(
+                conference,
+                "Manual contract for {} marked as signed by sponsor".format(conference.conferencename),
+                'A manual contract with the subject "{}" sent to {} has been marked as signed by sponsor.'.format(acontract.subject, sponsor.name),
+            )
+            messages.info(request, "Contract marked as signed by sponsor.")
+    elif which == 'org':
+        if acontract.completed:
+            messages.error(request, "Contract already marked as signed by {}.".format(conference.contractsendername))
+        else:
+            acontract.completed = timezone.now()
+            acontract.save(update_fields=['completed'])
+            send_conference_sponsor_notification(
+                conference,
+                "Manual contract for {} marked as signed by {}".format(conference.conferencename, conference.contractsendername),
+                'A manual contract with the subject "{}" sent to {} has been marked as signed by {}.'.format(acontract.subject, sponsor.name, conference.contractsendername),
+            )
+            messages.info(request, "Contract marked as signed by {}.".format(conference.contractsendername))
+    else:
+        raise Http404("Invalid value for which")
+
+    return HttpResponseRedirect("../")
index 0ab9d5c826952a9a34163b083ab1d0b3aa11e1a4..c29bc682f0a63dd609f625d25a1af8a009594cb9 100644 (file)
@@ -230,6 +230,38 @@ This matrix gives a quick overview of the status of the different benefits for e
 <a class="btn btn-primary" href="shipments/new/">Register new shipment</a>
 {%endif%}
 
+{%if additionalcontracts%}
+<h2>Additional contracts</h2>
+<p>
+  Additional contracts are <i>sent</i> (and in the case of manual contracts, marked as signed) on the individual sponsor page.
+</p>
+<table class="table table-striped table-hover">
+  <thead>
+    <tr>
+      <th>Sponsor</th>
+      <th>Subject</th>
+      <th>Contract name</th>
+      <th>Type</th>
+      <th>Sponsor signed</th>
+      <th>{{conference.contractsendername}} signed</th>
+    </tr>
+  </thead>
+  <tbody>
+{% for ac in additionalcontracts%}
+   <tr>
+     <td><a href="{{ac.sponsor.id}}/">{{ac.sponsor.name}}</a></td>
+     <td>{{ac.subject}}</td>
+     <td>{{ac.contract.contractname}}</td>
+     <td>{%if ac.digitalcontract%}Digital{%else%}Manual{%endif%}</td>
+     <td>{{ac.sponsorsigned|default:""}}</td>
+     <td>{{ac.completed|default:""}}</td>
+     </td>
+   </tr>
+{%endfor%}
+  </tbody>
+</table>
+{%endif%}
+
 {%if eu_vat and  user.is_superuser %}
 <h2>VAT testing</h2>
 <p>
index 764983fb642cdd22d3ab04a438eb608de6457459..af59b5d06163e5403609ab6f3134f8521707575d 100644 (file)
@@ -110,6 +110,55 @@ div.panelwrap {
 </div>
 {%endif%}
 
+{%if conference_has_contracts %}
+<h2>Additional contracts</h2>
+<p>
+  You can send additional contracts to this sponsor if one is needed, such as training contracts.
+{%if sponsor.signmethod ==  0%}
+  This sponsor uses digital contracts, so the additional contracts will also be sent digitally. No
+  automatic processing happens for these contracts, just the tracking of the status.
+{%else%}
+  This sponsor uses manual contracts, so the additional contracts will be emailed to all sponsor
+  representatives as a PDF, asking them to return it signed.
+{%endif%}
+</p>
+{%if additionalcontracts%}
+<table class="table table-striped table-hover">
+  <thead>
+    <tr>
+      <th>Subject</th>
+      <th>Contract name</th>
+      <th>Type</th>
+      <th>Sponsor signed</th>
+      <th>{{conference.contractsendername}} signed</th>
+    </tr>
+  </thead>
+  <tbody>
+{% for ac in additionalcontracts%}
+   <tr>
+     <td>{{ac.subject}}</td>
+     <td><a href="../contracts/{{ac.contract.id}}/">{{ac.contract.contractname}}</a></td>
+     <td>{%if ac.digitalcontract%}Digital{%else%}Manual{%endif%}</td>
+     <td>{%if ac.sponsorsigned%}{{ac.sponsorsigned}}{%else%}
+      {%if not ac.digitalcontract%}<form method="post" action="markaddcontract/">{%csrf_token%}<input type="hidden" name="id" value="{{ac.id}}"><input type="hidden" name="which" value="sponsor"><input type="submit" class="btn btn-sm confirm-btn" value="Mark as signed" data-confirm="Are you sure you want to mark this (manual) contract as signed by the sponsor?"></form>{%endif%}
+{%endif%}
+     </td>
+     <td>{%if ac.completed%}{{ac.completed}}{%else%}
+      {%if not ac.digitalcontract%}<form method="post" action="markaddcontract/">{%csrf_token%}<input type="hidden" name="id" value="{{ac.id}}"><input type="hidden" name="which" value="org"><input type="submit" class="btn btn-sm confirm-btn" value="Mark as signed" data-confirm="Are you sure you want to mark this (manual) contract as signed by {{conference.contractsendername}}?"></form>{%endif%}
+      {%endif%}
+     </td>
+   </tr>
+{%endfor%}
+  </tbody>
+</table>
+{%endif%}
+
+<h4>Send new contract</h4>
+<form method="post" action="addcontract/" class="form-horizontal">{%csrf_token%}
+{%include "confreg/admin_backend_form_content.html" with form=addcontractform savebutton="Send contract"%}
+</form>
+{%endif%}
+
 {%else%}{%comment%}Sponsor confirmed{%endcomment%}
 {%if sponsor.invoice%}
 <p>