diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index e3a6a233bffc467ede9a0d40bb30eb534622d123..07bcc73d47add7599cfed87548581aa79b852779 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -18,7 +18,7 @@ User = get_user_model()
 
 
 def link_to(target, request):
-    if target:
+    if target and hasattr(target, 'get_absolute_url'):
         return request.scheme + '://' + request.get_host() + target.get_absolute_url()
 
 
@@ -218,13 +218,15 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.OPENED_SEALED: 'Opened the submission while still sealed',
         MESSAGES.SCREENING: 'Screening status from {old_status} to {source.screening_status}',
         MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {source}',
-        MESSAGES.CREATED_PROJECT: '{user} has created Project',
-        MESSAGES.UPDATE_PROJECT_LEAD: 'Lead changed from {old_lead} to {source.lead} by {user}',
-        MESSAGES.SEND_FOR_APPROVAL: '{user} has requested approval',
-        MESSAGES.APPROVE_PROJECT: '{user} has approved',
-        MESSAGES.REQUEST_PROJECT_CHANGE: '{user} has requested changes to for acceptance: "{comment}"',
-        MESSAGES.UPLOAD_CONTRACT: '{user} has uploaded a contract for {source.title}',
-        MESSAGES.APPROVE_CONTRACT: '{user} has approved contract for {source.title}'
+        MESSAGES.CREATED_PROJECT: 'Created',
+        MESSAGES.UPDATE_PROJECT_LEAD: 'Lead changed from {old_lead} to {source.lead}',
+        MESSAGES.SEND_FOR_APPROVAL: 'Requested approval',
+        MESSAGES.APPROVE_PROJECT: 'Approved',
+        MESSAGES.REQUEST_PROJECT_CHANGE: 'Requested changes for acceptance: "{comment}"',
+        MESSAGES.UPLOAD_CONTRACT: 'Uploaded a contract',
+        MESSAGES.APPROVE_CONTRACT: 'Approved contract',
+        MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'Updated Payment Request status to: {payment_request.status_display}',
+        MESSAGES.REQUEST_PAYMENT: 'Payment Request submitted',
     }
 
     def recipients(self, message_type, **kwargs):
@@ -342,9 +344,10 @@ class ActivityAdapter(AdapterBase):
             except KeyError:
                 pass
 
-        # TODO resolve how related objects work with submission/project
-        has_correct_fields = all(hasattr(related, attr) for attr in ['author', 'submission', 'get_absolute_url'])
-        if has_correct_fields and isinstance(related, models.Model):
+        has_correct_fields = all(hasattr(related, attr) for attr in ['get_absolute_url'])
+        isnt_source = source != related
+        is_model = isinstance(related, models.Model)
+        if has_correct_fields and isnt_source and is_model:
             related_object = related
         else:
             related_object = None
@@ -394,7 +397,7 @@ class SlackAdapter(AdapterBase):
         MESSAGES.UPLOAD_CONTRACT: '{user} has uploaded a contract for <{link}|{source.title}>.',
         MESSAGES.APPROVE_CONTRACT: '{user} has approved contract for <{link}|{source.title}>.',
         MESSAGES.REQUEST_PAYMENT: '{user} has requested payment for <{link}|{source.title}>.',
-        MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: '{user} has updated status for payment request <{link}|{source.title}>.',
+        MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: '{user} has changed the status of <{link_related}|payment request> on <{link}|{source.title}> to {payment_request.status_display}.',
         MESSAGES.DELETE_PAYMENT_REQUEST: '{user} has deleted payment request from <{link}|{source.title}>.',
         MESSAGES.UPDATE_PAYMENT_REQUEST: '{user} has updated payment request for <{link}|{source.title}>.',
     }
@@ -414,13 +417,16 @@ class SlackAdapter(AdapterBase):
         source = kwargs['source']
         sources = kwargs['sources']
         request = kwargs['request']
+        related = kwargs['related']
         link = link_to(source, request)
+        link_related = link_to(related, request)
         links = {
             source.id: link_to(source, request)
             for source in sources
         }
         return {
             'link': link,
+            'link_related': link_related,
             'links': links,
         }
 
diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html
index bdc55edf7ae7fa60bed5332b66916ae77fc3995e..5dce52086caa022b6ce8326272a9cac600468630 100644
--- a/opentech/apply/activity/templates/activity/include/listing_base.html
+++ b/opentech/apply/activity/templates/activity/include/listing_base.html
@@ -1,4 +1,4 @@
-{% load activity_tags bleach_tags markdown_tags submission_tags %}
+{% load activity_tags bleach_tags markdown_tags submission_tags apply_tags %}
 <div class="feed__item feed__item--{{ activity.type }}">
     <div class="feed__pre-content">
         <p class="feed__label feed__label--{{ activity.type }}">{{ activity.type|capfirst }}</p>
@@ -49,7 +49,7 @@
                 {% with url=activity.related_object.get_absolute_url %}
                     {% if url %}
                     <a href="{{ url }}" class="feed__related-item">
-                        {{ activity.related_object }} <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg>
+                        {{ activity.related_object|model_verbose_name }} <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg>
                     </a>
                     {% endif %}
                 {% endwith %}
diff --git a/opentech/apply/activity/tests/test_messaging.py b/opentech/apply/activity/tests/test_messaging.py
index 4341efeb1d8159f493bf14b74e4a7e8fc09a4087..08cb17eebf476543c8960b23ff24331df5a48dff 100644
--- a/opentech/apply/activity/tests/test_messaging.py
+++ b/opentech/apply/activity/tests/test_messaging.py
@@ -607,7 +607,7 @@ class TestAdaptersForProject(AdapterMixin, TestCase):
         )
         self.assertEqual(Activity.objects.count(), 1)
         activity = Activity.objects.first()
-        self.assertEqual(None, activity.related_object)
+        self.assertEqual(project.submission, activity.related_object)
 
     @override_settings(
         SLACK_DESTINATION_URL=target_url,
diff --git a/opentech/apply/activity/views.py b/opentech/apply/activity/views.py
index c0edaf6b7abe782220d0d241b14e1e2eeddfb64d..9b7c4bb433a89fabc4f2680b5f6e4dd15b871c6f 100644
--- a/opentech/apply/activity/views.py
+++ b/opentech/apply/activity/views.py
@@ -43,12 +43,12 @@ class ActivityContextMixin:
             'actions': Activity.actions.filter(**query).select_related(
                 'user',
             ).prefetch_related(
-                'related_object__submission',
+                'related_object',
             ).visible_to(self.request.user),
             'comments': Activity.comments.filter(**query).select_related(
                 'user',
             ).prefetch_related(
-                'related_object__submission',
+                'related_object',
             ).visible_to(self.request.user),
         }
         return super().get_context_data(**extra, **kwargs)
@@ -77,7 +77,8 @@ class CommentFormView(DelegatedViewMixin, CreateView):
     def get_success_url(self):
         return self.object.source.get_absolute_url() + '#communications'
 
-    @classmethod
-    def contribute_form(cls, instance, user):
+    def get_form_kwargs(self):
         # We dont want to pass the submission as the instance
-        return super().contribute_form(instance=None, user=user)
+        kwargs = super().get_form_kwargs()
+        kwargs.pop('instance')
+        return kwargs
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 91911a9858bf01e30272c27b1c3e7cb1791dfcc2..74ac4a435c0207617a0214a27da3bffce3f9532c 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -105,10 +105,6 @@ class BatchUpdateSubmissionLeadForm(forms.Form):
 
     def save(self):
         new_lead = self.cleaned_data['lead']
-        import logging
-        logger = logging.getLogger('opentech')
-        logger.debug(new_lead)
-        logger.debug(new_lead.id)
         submissions = self.cleaned_data['submissions']
 
         for submission in submissions:
diff --git a/opentech/apply/funds/templates/funds/includes/delegated_form_base.html b/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
index f33bc3d89f2f6bf9d59b68bd00febca8a745ea79..18292b3d89d3c371b58917a9063f0baf8a63eae0 100644
--- a/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
+++ b/opentech/apply/funds/templates/funds/includes/delegated_form_base.html
@@ -9,7 +9,10 @@
 
     {{ form.media }}
 
-    {% for field in form %}
+    {% for hidden in form.hidden_fields %}
+        {{ hidden }}
+    {% endfor %}
+    {% for field in form.visible_fields %}
         {% if field.field %}
             {% include "forms/includes/field.html" %}
         {% else %}
diff --git a/opentech/apply/projects/files.py b/opentech/apply/projects/files.py
index 57c4a5eb171e024e046441dab682598e4aa394a3..2195d989e212cfc57b3eb0d79a03a7966558a443 100644
--- a/opentech/apply/projects/files.py
+++ b/opentech/apply/projects/files.py
@@ -25,4 +25,4 @@ def get_files(project):
     file_field_names = project.submission.file_field_ids
     file_fields = (project.submission.data(field) for field in file_field_names)
 
-    yield from flatten(file_fields)
+    return list(flatten(file_fields))
diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py
index f10819b4542cf32066b600f45b99eacfe9279c18..aa792f959902d802774cde363b8b08a37f6aacc5 100644
--- a/opentech/apply/projects/forms.py
+++ b/opentech/apply/projects/forms.py
@@ -12,7 +12,6 @@ from opentech.apply.funds.models import ApplicationSubmission
 from opentech.apply.stream_forms.fields import MultiFileField
 from opentech.apply.users.groups import STAFF_GROUP_NAME
 
-from .files import get_files
 from .models import (
     CHANGES_REQUESTED,
     COMMITTED,
@@ -23,7 +22,6 @@ from .models import (
     UNDER_REVIEW,
     Approval,
     Contract,
-    DocumentCategory,
     PacketFile,
     PaymentReceipt,
     PaymentRequest,
@@ -69,15 +67,13 @@ class ChangePaymentRequestStatusForm(forms.ModelForm):
     def __init__(self, instance, *args, **kwargs):
         super().__init__(instance=instance, *args, **kwargs)
 
-        self.instance = instance
-
         status_field = self.fields['status']
 
-        if instance.status == SUBMITTED:
+        if self.instance.status == SUBMITTED:
             wanted = [CHANGES_REQUESTED, UNDER_REVIEW, DECLINED]
-        elif instance.status == CHANGES_REQUESTED:
+        elif self.instance.status == CHANGES_REQUESTED:
             wanted = [DECLINED]
-        elif instance.status == UNDER_REVIEW:
+        elif self.instance.status == UNDER_REVIEW:
             wanted = [PAID]
         else:
             wanted = []
@@ -89,8 +85,6 @@ class CreateProjectForm(forms.Form):
     submission = forms.ModelChoiceField(
         queryset=ApplicationSubmission.objects.filter(project__isnull=True),
         widget=forms.HiddenInput(),
-        label='',
-        required=False,
     )
 
     def __init__(self, instance=None, user=None, *args, **kwargs):
@@ -108,8 +102,6 @@ class CreateApprovalForm(forms.ModelForm):
     by = forms.ModelChoiceField(
         queryset=User.objects.approvers(),
         widget=forms.HiddenInput(),
-        label='',
-        required=False,
     )
 
     class Meta:
@@ -117,9 +109,14 @@ class CreateApprovalForm(forms.ModelForm):
         fields = ('by',)
 
     def __init__(self, user=None, *args, **kwargs):
-        initial = kwargs.pop('initial', {})
-        initial.update(by=user)
-        super().__init__(*args, initial=initial, **kwargs)
+        self.user = user
+        super().__init__(*args, **kwargs)
+
+    def clean_by(self):
+        by = self.cleaned_data['by']
+        if by != self.user:
+            raise forms.ValidationError('Cannot approve for a different user')
+        return by
 
 
 class EditPaymentRequestForm(forms.ModelForm):
@@ -243,41 +240,34 @@ class RequestPaymentForm(forms.ModelForm):
         return request
 
 
-class SelectDocumentForm(forms.Form):
-    category = forms.ModelChoiceField(queryset=DocumentCategory.objects.all())
-    files = forms.MultipleChoiceField()
+class SelectDocumentForm(forms.ModelForm):
+    document = forms.ChoiceField()
 
-    name = 'select_document_form'
+    class Meta:
+        model = PacketFile
+        fields = ['category', 'document']
 
-    def __init__(self, existing_files, project, *args, **kwargs):
+    def __init__(self, existing_files, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        self.project = project
+        self.files = existing_files
+
+        choices = [(f.url, f.filename) for f in self.files]
 
-        choices = []
-        if existing_files is not None:
-            choices = [(f.url, f.filename) for f in existing_files]
+        self.fields['document'].choices = choices
 
-        self.fields['files'].choices = choices
+    def clean_document(self):
+        file_url = self.cleaned_data['document']
+        for file in self.files:
+            if file.url == file_url:
+                new_file = ContentFile(file.read())
+                new_file.name = file.filename
+                return new_file
+        raise forms.ValidationError("File not found on submission")
 
     @transaction.atomic()
     def save(self, *args, **kwargs):
-        category = self.cleaned_data['category']
-        urls = self.cleaned_data['files']
-
-        files = get_files(self.project)
-        files = (f for f in files if f.url in urls)
-
-        for f in files:
-            new_file = ContentFile(f.read())
-            new_file.name = f.filename
-
-            PacketFile.objects.create(
-                category=category,
-                project=self.project,
-                title=f.filename,
-                document=new_file,
-            )
+        return super().save(*args, **kwargs)
 
 
 class SetPendingForm(forms.ModelForm):
@@ -305,15 +295,14 @@ class SetPendingForm(forms.ModelForm):
 
 class UploadContractForm(forms.ModelForm):
     class Meta:
-        fields = ['file', 'is_signed']
+        fields = ['file']
         model = Contract
 
-    def __init__(self, user=None, instance=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
 
-        if not user.is_staff:
-            self.fields['is_signed'].widget = forms.HiddenInput()
-            self.fields['is_signed'].default = True
+class StaffUploadContractForm(forms.ModelForm):
+    class Meta:
+        fields = ['file', 'is_signed']
+        model = Contract
 
 
 class UploadDocumentForm(forms.ModelForm):
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index d788876bd422738f8b18c4118b5de4c4ac95e19d..9370c0c49e8f91bfedc31b448c0cdcebb37f1352 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -3,12 +3,17 @@ import decimal
 import json
 import logging
 
+
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.fields import JSONField
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
 from django.db import models
+from django.db.models import Sum, Value as V
+from django.db.models.functions import Coalesce
+from django.db.models.signals import post_delete
+from django.dispatch.dispatcher import receiver
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import ugettext as _
@@ -97,6 +102,12 @@ class PacketFile(models.Model):
         return RemoveDocumentForm(instance=self)
 
 
+@receiver(post_delete, sender=PacketFile)
+def delete_packetfile_file(sender, instance, **kwargs):
+    # Remove the file and don't save the base model
+    instance.document.delete(False)
+
+
 class PaymentApproval(models.Model):
     request = models.ForeignKey('PaymentRequest', on_delete=models.CASCADE, related_name="approvals")
     by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="payment_approvals")
@@ -130,6 +141,26 @@ REQUEST_STATUS_CHOICES = [
 ]
 
 
+class PaymentRequestQueryset(models.QuerySet):
+    def in_progress(self):
+        return self.exclude(status__in=[DECLINED, PAID])
+
+    def rejected(self):
+        return self.filter(status=DECLINED)
+
+    def not_rejected(self):
+        return self.exclude(status=DECLINED)
+
+    def total_value(self):
+        return self.aggregate(total=Coalesce(Sum('value'), V(0)))['total']
+
+    def paid_value(self):
+        return self.filter(status=PAID).total_value()
+
+    def unpaid_value(self):
+        return self.filter(status__in=[SUBMITTED, UNDER_REVIEW]).total_value()
+
+
 class PaymentRequest(models.Model):
     project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="payment_requests")
     by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="payment_requests")
@@ -147,18 +178,45 @@ class PaymentRequest(models.Model):
     comment = models.TextField()
     status = models.TextField(choices=REQUEST_STATUS_CHOICES, default=SUBMITTED)
 
+    objects = PaymentRequestQueryset.as_manager()
+
     def __str__(self):
         return f'Payment requested for {self.project}'
 
-    def user_can_delete(self, user):
+    @property
+    def status_display(self):
+        return self.get_status_display()
+
+    def can_user_delete(self, user):
+        if user.is_applicant:
+            if self.status in (SUBMITTED, CHANGES_REQUESTED):
+                return True
+
+        return False
+
+    def can_user_edit(self, user):
+        if user.is_applicant:
+            if self.status in {SUBMITTED, CHANGES_REQUESTED}:
+                return True
+
         if user.is_apply_staff:
-            return False  # Staff can reject
+            if self.status in {SUBMITTED}:
+                return True
+
+        return False
 
-        if self.status not in (SUBMITTED, CHANGES_REQUESTED):
+    def can_user_change_status(self, user):
+        if not user.is_apply_staff:
+            return False  # Users can't change status
+
+        if self.status in {PAID, DECLINED}:
             return False
 
         return True
 
+    def get_absolute_url(self):
+        return reverse('apply:projects:payments:detail', args=[self.project.pk, self.pk])
+
 
 COMMITTED = 'committed'
 CONTRACTING = 'contracting'
@@ -254,6 +312,12 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
             value=submission.form_data.get('value', 0),
         )
 
+    def paid_value(self):
+        return self.payment_requests.paid_value()
+
+    def unpaid_value(self):
+        return self.payment_requests.unpaid_value()
+
     def clean(self):
         if self.proposed_start is None:
             return
diff --git a/opentech/apply/projects/templates/application_projects/includes/funding_block.html b/opentech/apply/projects/templates/application_projects/includes/funding_block.html
index b08db90a3e2b72305054f9b85b0772abd8d69168..c5a1459fe73eb43e5eb1fc3abe8d452cf20c740d 100644
--- a/opentech/apply/projects/templates/application_projects/includes/funding_block.html
+++ b/opentech/apply/projects/templates/application_projects/includes/funding_block.html
@@ -1,4 +1,4 @@
-{% load humanize %}
+{% load humanize payment_request_tools %}
 <ul class="funding-block">
     <li class="funding-block__item">
         <p class="funding-block__title">Fund total</p>
@@ -6,12 +6,12 @@
     </li>
     <li class="funding-block__item">
         <p class="funding-block__title">Total paid</p>
-        <p class="funding-block__standout">${{ payments.totals.paid_absolute|default:0|intcomma }}</p>
-        <p class="funding-block__meta">({{ payments.totals.paid_percentage|default:0 }}%)</p>
+        <p class="funding-block__standout">${{ object.paid_value|intcomma }}</p>
+        <p class="funding-block__meta">({% percentage object.paid_value object.value %}%)</p>
     </li>
     <li class="funding-block__item">
         <p class="funding-block__title">Awaiting payment</p>
-        <p class="funding-block__standout">${{ payments.totals.awaiting_absolute|default:0|intcomma}}</p>
-        <p class="funding-block__meta">({{ payments.totals.awaiting_percentage|default:0 }}%)</p>
+        <p class="funding-block__standout">${{ object.unpaid_value|intcomma}}</p>
+        <p class="funding-block__meta">({% percentage object.unpaid_value object.value %}%)</p>
     </li>
 </ul>
diff --git a/opentech/apply/projects/templates/application_projects/includes/payment_requests.html b/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
index 62c864470dd8885e7e131e13a1344de52f1f2efb..2ca9c43138753ebaf5e70cf958175d78f181066a 100644
--- a/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
+++ b/opentech/apply/projects/templates/application_projects/includes/payment_requests.html
@@ -16,35 +16,25 @@
             <tr>
                 <th class="payment-block__table-amount">Amount</th>
                 <th class="payment-block__table-status">Status</th>
-                <th class="payment-block__table-docs">Documents</th>
+                <th class="payment-block__table-date">From</th>
+                <th class="payment-block__table-date">To</th>
                 <th class="payment-block__table-update"></th>
             </tr>
         </thead>
         <tbody>
-            {% for payment_request in payments.not_rejected %}
+            {% for payment_request in object.payment_requests.not_rejected %}
             <tr>
                 <td><span class="payment-block__mobile-label">Amount: </span>${{ payment_request.value|intcomma }}</td>
                 <td><span class="payment-block__mobile-label">Status: </span>{{ payment_request.get_status_display }}</td>
-                <td><span class="payment-block__mobile-label">Documents: </span><a href="#">Download</a></td>
+                <td><span class="payment-block__mobile-label">From: </span>{{ payment_request.date_from.date }}</td>
+                <td><span class="payment-block__mobile-label">To: </span>{{ payment_request.date_to.date }}</td>
                 <td>
-                    {% can_change_status payment_request user as user_can_change_status %}
-                    {% if user_can_change_status %}
-                    <a
-                        data-fancybox
-                        data-src="#change-payment-status-{{ payment_request.pk }}"
-                        class="payment-block__status-link"
-                        href="#">
-                        Change status
-                    </a>
-                    {% endif %}
-
-                    {% user_can_edit payment_request user as user_can_edit_request %}
+                    <a href="{{ payment_request.get_absolute_url }}">View</a>
+                    {% can_edit payment_request user as user_can_edit_request %}
                     {% if user_can_edit_request %}
                     <a
-                        data-fancybox
-                        data-src="#edit-payment-request-{{ payment_request.pk }}"
                         class="payment-block__status-link"
-                        href="#">
+                        href="{% url "apply:projects:payments:edit" pk=object.pk pr_pk=payment_request.pk %}">
                         Edit
                     </a>
                     {% endif %}
@@ -58,11 +48,15 @@
                     {% endif %}
                 </td>
             </tr>
+            {% empty %}
+            <tr>
+                No active Payment Requests.
+            </tr>
             {% endfor %}
         </tbody>
     </table>
 
-    {% if payments.rejected %}
+    {% if object.payment_requests.rejected %}
         <p class="payment-block__rejected">
             <a class="payment-block__rejected-link js-payment-block-rejected-link" href="#">Show rejected</a>
         </p>
@@ -72,15 +66,15 @@
                 <tr>
                     <th class="payment-block__table-amount">Amount</th>
                     <th class="payment-block__table-status">Status</th>
-                    <th class="payment-block__table-docs">Documents</th>
+                    <th class="payment-block__table-view"></th>
                 </tr>
             </thead>
             <tbody>
-                {% for payment_request in payments.rejected %}
+                {% for payment_request in object.payment_requests.rejected %}
                 <tr>
                     <td><span class="payment-block__mobile-label">Amount: </span>${{ payment_request.value }}</td>
                     <td><span class="payment-block__mobile-label">Status: </span>{{ payment_request.get_status_display }}</td>
-                    <td><span class="payment-block__mobile-label">Documents: </span><a href="#">Download</a></td>
+                    <td><a href="{{ payment_request.get_absolute_url }}">View</a></td>
                 </tr>
                 {% endfor %}
             </tbody>
@@ -99,13 +93,3 @@
     </div>
 </div>
 {% endfor %}
-
-{% for form in edit_payment_request_forms %}
-<div class="modal" id="edit-payment-request-{{ form.instance.pk }}">
-    <h4 class="modal__header-bar">Edit request</h4>
-    <div class="wrapper--outer-space-medium">
-        {% url 'apply:projects:edit-payment-request' pk=object.pk payment_request_id=form.instance.pk as edit_payment_request_action %}
-        {% include 'funds/includes/delegated_form_base.html' with form=form value='Update' action=edit_payment_request_action %}
-    </div>
-</div>
-{% endfor %}
diff --git a/opentech/apply/projects/templates/application_projects/includes/paymentrequest_admin_detail.html b/opentech/apply/projects/templates/application_projects/includes/paymentrequest_admin_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..d5fd9a177e2fca985dad7fb769d4616316fc872a
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/includes/paymentrequest_admin_detail.html
@@ -0,0 +1,28 @@
+{% extends "application-projects/paymentrequest_detail.html" %}
+
+{% block actions %}
+    {{ block.super }}
+    <a
+        data-fancybox
+        data-src="#change-status"
+        class="button button--bottom-space button--primary button--full-width"
+        href="#"
+    >
+        Change Status
+    </a>
+    <div class="modal" id="change-status">
+        <h4 class="modal__header-bar">Change status</h4>
+        <p>Current status: {{ object.get_status_display }}</p>
+        {% include 'funds/includes/delegated_form_base.html' with form=change_payment_status value='Update'%}
+    </div>
+{% endblock %}
+
+{% block extra_css %}
+<link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}">
+{% endblock %}
+
+{% block extra_js %}
+{{ block.super }}
+<script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script>
+<script src="{% static 'js/apply/fancybox-global.js' %}"></script>
+{% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html
index 60646f19cf41ae59c3e73388e9b4f3406dd77efe..1b596b336e36a00d61f6c325d0e115041aeac1cf 100644
--- a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html
+++ b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html
@@ -45,21 +45,24 @@
 
         <li class="docs-block__row">
             <div class="docs-block__row-inner">
-                <svg class="icon docs-block__icon"><use xlink:href="#tick"></use></svg>
+                <svg class="icon docs-block__icon{% if not remaining_document_categories %} is-complete{% endif %}">
+                    <use xlink:href="#tick"></use>
+                </svg>
                 <p class="docs-block__title">Supporting documents</p>
             </div>
             {% if editable %}
             <div class="docs-block__row-inner">
-                <a
-                    data-fancybox
-                    data-src="#copy-supporting-doc"
-                    class="docs-block__link"
-                    style="margin-right:20px"
-                    href="#">
-                    Choose file
-                </a>
-            </div>
-            <div class="docs-block__row-inner">
+                {% if select_document_form %}
+                    <a
+                        data-fancybox
+                        data-src="#copy-supporting-doc"
+                        class="docs-block__link"
+                        href="#">
+                        Choose file
+                    </a>
+                {% else %}
+                    <span class="docs-block__link is-disabled" data-tooltip="No files on submission">Choose file</span>
+                {% endif %}
                 <a data-fancybox data-src="#upload-supporting-doc" class="docs-block__link" href="#">Upload new</a>
             </div>
             {% endif %}
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_admin_detail.html b/opentech/apply/projects/templates/application_projects/paymentrequest_admin_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..edfdd7fc6f4a3e136e0062c7733845b5c52f9fd9
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_admin_detail.html
@@ -0,0 +1,36 @@
+{% extends "application_projects/paymentrequest_detail.html" %}
+{% load static payment_request_tools %}
+
+{% block actions %}
+    {{ block.super }}
+    {% can_change_status object user as user_can_change_status %}
+    <a
+        {% if user_can_change_status %}
+            data-fancybox
+            data-src="#change-status"
+        {% else %}
+            data-tooltip="Cannot change from 'Paid' or 'Declined' state"
+        {% endif %}
+        class="button button--bottom-space button--primary button--full-width{% if not user_can_change_status %} button--tooltip-disabled{% endif %}"
+        href="#"
+    >
+        Change Status
+    </a>
+    {% if user_can_change_status %}
+    <div class="modal" id="change-status">
+        <h4 class="modal__header-bar">Change status</h4>
+        <p>Current status: {{ object.get_status_display }}</p>
+        {% include 'funds/includes/delegated_form_base.html' with form=change_payment_status value='Update'%}
+    </div>
+    {% endif %}
+{% endblock %}
+
+{% block extra_css %}
+<link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}">
+{% endblock %}
+
+{% block extra_js %}
+{{ block.super }}
+<script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script>
+<script src="{% static 'js/apply/fancybox-global.js' %}"></script>
+{% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html b/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..8dc4a84bda83a6491ebc9fa96e63dc47b71f0f76
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_detail.html
@@ -0,0 +1,62 @@
+{% extends "base-apply.html" %}
+{% load humanize payment_request_tools %}
+
+{% block title %}Payment Request: {{ object.project.title }}{% endblock %}
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <a class="simplified__projects-link" href="{{ object.project.get_absolute_url }}">
+            Project
+        </a>
+        <h2 class="heading heading--no-margin">Payment Request</h2>
+        <h5 class="heading heading--no-margin">For: {{ object.project.title }}</h5>
+    </div>
+</div>
+
+<div class="wrapper wrapper--sidebar">
+    <div class="wrapper--sidebar--inner">
+        <h5>Status: {{ object.get_status_display }}</h5>
+        <p>Name of Vendor: {{ object.project.contact_legal_name }}</p>
+        <p>Invoice Number: {{ object.pk }}</p>
+        <p>
+            Period of Performance:
+        </p>
+        <p>
+            From: {{ object.date_from.date }}
+        </p>
+        <p>
+            To: {{ object.date_to.date }}
+        </p>
+        <p>
+            Total: ${{ object.value|intcomma }}
+        </p>
+
+        <h3>Invoice</h3>
+        <a href="{% url "apply:projects:payments:invoice" pk=object.project.pk pr_pk=object.pk %}">Download</a>
+        <h5>Reciepts</h5>
+        {% for reciept in object.receipts.all %}
+            <a href="{% url "apply:projects:payments:receipt" pk=object.project.pk pr_pk=object.pk file_pk=reciept.pk %}">Download</a>
+        {% endfor %}
+    </div>
+    <aside class="sidebar">
+        <div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions">
+            {% block actions %}
+                {% can_edit object user as user_can_edit_request %}
+                <a
+                    {% if not user_can_edit_request %}
+                        data-tooltip="Only editable when 'Submitted' or you have been requested to make changes"
+                    {% endif %}
+                    class="button button--bottom-space button--primary button--full-width{% if not user_can_edit_request %} button--tooltip-disabled{% endif %}"
+                    href={% if user_can_edit_request %}
+                        "{% url "apply:projects:payments:edit" pk=object.project.pk pr_pk=object.pk %}"
+                    {% else %}
+                        "#"
+                    {% endif %}
+                >
+                    Edit
+                </a>
+            {% endblock %}
+        </div>
+    </aside>
+</div>
+{% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/paymentrequest_form.html b/opentech/apply/projects/templates/application_projects/paymentrequest_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..b282e7ca13f3580c06d4119b778496055d38feb6
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/paymentrequest_form.html
@@ -0,0 +1,36 @@
+{% extends "base-apply.html" %}
+{% load static %}
+
+{% block title %}Edit Payment Request: {{ object.project.title }}{% endblock %}
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <h2 class="heading heading--no-margin">Editing Payment Request</h2>
+        <h5 class="heading heading--no-margin">{{ object.project.title }}</h5>
+    </div>
+</div>
+
+{% include "forms/includes/form_errors.html" with form=form %}
+
+<div class="wrapper wrapper--light-grey-bg wrapper--form wrapper--sidebar">
+    <div class="wrapper--sidebar--inner">
+        <form class="form" action="" method="post" enctype="multipart/form-data">
+            {% csrf_token %}
+            {{ form.media }}
+
+            {% for field in form %}
+                {% if field.field %}
+                    {% include "forms/includes/field.html" %}
+                {% else %}
+                    {{ field }}
+                {% endif %}
+            {% endfor %}
+            <input class="button button--submit button--top-space button--primary" type="submit" name="save" value="Save" />
+        </form>
+    </div>
+</div>
+{% endblock %}
+
+{% block extra_js %}
+<script src="{% static 'js/apply/list-input-files.js' %}"></script>
+{% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/project_detail.html b/opentech/apply/projects/templates/application_projects/project_detail.html
index aad2923a0bef311e624c4313891544a39a09ebbf..ce52bf0126fc2e47d9987048da59b89ded61e29e 100644
--- a/opentech/apply/projects/templates/application_projects/project_detail.html
+++ b/opentech/apply/projects/templates/application_projects/project_detail.html
@@ -189,10 +189,10 @@
                 <div class="sidebar__inner">
                     <h5>Supporting Information</h5>
 
-                    <p><a href="{{ object.submission.get_absolute_url }}">Proposal</a></p>
+                    <p><a class="link link--bold" href="{{ object.submission.get_absolute_url }}">Proposal</a></p>
 
                     {% if request.user.is_apply_staff %}
-                    <p><a href="{% url 'apply:projects:simplified' pk=project.pk %}">Approval form</a></p>
+                    <p><a class="link link--bold" href="{% url 'apply:projects:simplified' pk=project.pk %}">Approval form</a></p>
                     {% endif %}
                 </div>
                 {% endif %}
@@ -207,16 +207,19 @@
 
                     {% for contract in contracts %}
                     <p>
-                        <a href="{{ contract.file.url }}">
-                            Uploaded at {{ contract.created_at|date:"j F Y" }}
-                            ({% if contract.is_signed %}Signed{% else %}Unsigned{% endif %})
+                        <a href="{{ contract.file.url }}" class="link link--bold">
+                            {{ contract.created_at|date:"j F Y" }}
                         </a>
+                            {% if contract.approver %}
+                                    - Approved by {{ contract.approver }}
+                            {% else %}
+                                ({% if contract.is_signed %}Signed{% else %}Unsigned{% endif %})
+                            {% endif %}
+
+                        {% if request.user.is_apply_staff and forloop.first %}
+                            {% block approve_contracts %}{% endblock %}
+                        {% endif %}
                     </p>
-
-                    {% if request.user.is_apply_staff and forloop.first %}
-                    {% block approve_contracts %}{% endblock %}
-                    {% endif %}
-
                     {% endfor %}
 
                 </div>
diff --git a/opentech/apply/projects/templatetags/payment_request_tools.py b/opentech/apply/projects/templatetags/payment_request_tools.py
index e0c2b34bf85b370d143d7668981a0a85234e66e8..310e8a0bf4712762b03a5c5b70b449b48e5c9b02 100644
--- a/opentech/apply/projects/templatetags/payment_request_tools.py
+++ b/opentech/apply/projects/templatetags/payment_request_tools.py
@@ -1,34 +1,36 @@
-from django import template
+import decimal
 
-from ..models import CHANGES_REQUESTED, DECLINED, PAID, SUBMITTED
+from django import template
 
 register = template.Library()
 
 
 @register.simple_tag
 def can_change_status(payment_request, user):
-    if not user.is_apply_staff:
-        return False  # Users can't change status
+    return payment_request.can_user_change_status(user)
 
-    if payment_request.status in (PAID, DECLINED):
-        return False
 
-    return True
+@register.simple_tag
+def can_delete(payment_request, user):
+    return payment_request.can_user_delete(user)
 
 
 @register.simple_tag
-def can_delete(payment_request, user):
-    return payment_request.user_can_delete(user)
+def can_edit(payment_request, user):
+    return payment_request.can_user_edit(user)
 
 
 @register.simple_tag
-def user_can_edit(payment_request, user):
-    # staff or applicant can edit when in SUBMITTED
-    if payment_request.status == SUBMITTED:
-        return True
+def percentage(value, total):
+    if not total:
+        return decimal.Decimal(0)
+
+    unrounded_total = (value / total) * 100
 
-    # applicant can edit when in CHANGES_REQUESTED
-    if payment_request.status == CHANGES_REQUESTED and user.is_applicant:
-        return True
+    # round using Decimal since we're dealing with currency
+    rounded_total = unrounded_total.quantize(
+        decimal.Decimal('0.0'),
+        rounding=decimal.ROUND_DOWN,
+    )
 
-    return False
+    return rounded_total
diff --git a/opentech/apply/projects/tests/factories.py b/opentech/apply/projects/tests/factories.py
index 098e85f7053d213326abd6ab25267d17e23863d1..084f7e65d40ec001f98c0439d0a25653f66fbad6 100644
--- a/opentech/apply/projects/tests/factories.py
+++ b/opentech/apply/projects/tests/factories.py
@@ -89,6 +89,11 @@ class ProjectFactory(factory.DjangoModelFactory):
     class Meta:
         model = Project
 
+    class Params:
+        in_approval = factory.Trait(
+            is_locked=True,
+        )
+
 
 class ContractFactory(factory.DjangoModelFactory):
     approver = factory.SubFactory(StaffFactory)
diff --git a/opentech/apply/projects/tests/test_forms.py b/opentech/apply/projects/tests/test_forms.py
index cc917b989abffdfe60402a1726d77c8c3e4ab255..29a1c657e55c763c2904f0686bb4c2d88facbafb 100644
--- a/opentech/apply/projects/tests/test_forms.py
+++ b/opentech/apply/projects/tests/test_forms.py
@@ -1,9 +1,12 @@
 from io import BytesIO
+from unittest import mock
 
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.test import TestCase, override_settings
 
-from opentech.apply.users.tests.factories import UserFactory
+from opentech.apply.users.tests.factories import (
+    UserFactory,
+)
 
 from ..files import get_files
 from ..forms import (
@@ -11,10 +14,18 @@ from ..forms import (
     ProjectApprovalForm,
     RequestPaymentForm,
     SelectDocumentForm,
+    StaffUploadContractForm,
+    UploadContractForm,
     filter_choices,
     filter_request_choices
 )
-from ..models import CHANGES_REQUESTED, DECLINED, PAID, SUBMITTED, UNDER_REVIEW
+from ..models import (
+    CHANGES_REQUESTED,
+    DECLINED,
+    PAID,
+    SUBMITTED,
+    UNDER_REVIEW,
+)
 from .factories import (
     DocumentCategoryFactory,
     PaymentRequestFactory,
@@ -28,7 +39,7 @@ class TestChangePaymentRequestStatusForm(TestCase):
         request = PaymentRequestFactory(status=SUBMITTED)
         form = ChangePaymentRequestStatusForm(instance=request)
 
-        expected = set(filter_request_choices([UNDER_REVIEW, CHANGES_REQUESTED, DECLINED, PAID]))
+        expected = set(filter_request_choices([UNDER_REVIEW, CHANGES_REQUESTED, DECLINED]))
         actual = set(form.fields['status'].choices)
 
         self.assertEqual(expected, actual)
@@ -165,20 +176,48 @@ class TestSelectDocumentForm(TestCase):
         files = list(get_files(project))
         self.assertEqual(len(files), 4)
 
-        urls = [files[0].url, files[2].url]
+        url = files[3].url
 
         form = SelectDocumentForm(
             files,
-            project,
-            data={'category': category.id, 'files': urls},
+            data={'category': category.id, 'document': url},
         )
         self.assertTrue(form.is_valid(), form.errors)
 
+        form.instance.project = project
         form.save()
 
         packet_files = project.packet_files.order_by('id')
-        self.assertEqual(len(packet_files), 2)
+        self.assertEqual(len(packet_files), 1)
+
+        self.assertEqual(packet_files.first().document.read(), files[3].read())
+
+
+class TestStaffContractUploadForm(TestCase):
+    mock_file = mock.MagicMock(spec=SimpleUploadedFile)
+    mock_file.read.return_value = b"fake file contents"
+
+    def test_staff_can_upload_unsigned(self):
+        form = StaffUploadContractForm(data={}, files={'file': self.mock_file})
+        self.assertTrue(form.is_valid(), form.errors)
+        self.assertFalse(form.cleaned_data.get('is_signed'))
 
-        first_file, second_file = packet_files
-        self.assertEqual(first_file.document.read(), files[0].read())
-        self.assertEqual(second_file.document.read(), files[2].read())
+    def test_staff_can_upload_signed(self):
+        form = StaffUploadContractForm(data={'is_signed': True}, files={'file': self.mock_file})
+        self.assertTrue(form.is_valid(), form.errors)
+        self.assertTrue(form.cleaned_data.get('is_signed'))
+
+
+class TestContractUploadForm(TestCase):
+    mock_file = mock.MagicMock(spec=SimpleUploadedFile)
+    mock_file.read.return_value = b"fake file contents"
+
+    def test_applicant_cant_upload_unsigned(self):
+        form = UploadContractForm(data={}, files={'file': self.mock_file})
+        self.assertTrue(form.is_valid(), form.errors)
+        self.assertIsNone(form.cleaned_data.get('is_signed'))
+
+    def test_applicant_can_upload_signed(self):
+        form = UploadContractForm(data={'is_signed': True}, files={'file': self.mock_file})
+        self.assertTrue(form.is_valid(), form.errors)
+        self.assertIsNone(form.cleaned_data.get('is_signed'))
diff --git a/opentech/apply/projects/tests/test_models.py b/opentech/apply/projects/tests/test_models.py
index a03d24b4cd7a29a67b19627f5bd73534232efcad..2ac88c4bac710089b1f9ebba81a0ae31878c4316 100644
--- a/opentech/apply/projects/tests/test_models.py
+++ b/opentech/apply/projects/tests/test_models.py
@@ -9,7 +9,8 @@ from ..models import (
     PAID,
     SUBMITTED,
     UNDER_REVIEW,
-    Project
+    Project,
+    PaymentRequest,
 )
 from .factories import (
     DocumentCategoryFactory,
@@ -76,58 +77,71 @@ class TestProjectModel(TestCase):
         payment_request = PaymentRequestFactory(status=SUBMITTED)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.user_can_delete(staff))
+        self.assertFalse(payment_request.can_user_delete(staff))
 
     def test_staff_cant_delete_from_under_review(self):
         payment_request = PaymentRequestFactory(status=UNDER_REVIEW)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.user_can_delete(staff))
+        self.assertFalse(payment_request.can_user_delete(staff))
 
     def test_staff_cant_delete_from_changes_requested(self):
         payment_request = PaymentRequestFactory(status=CHANGES_REQUESTED)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.user_can_delete(staff))
+        self.assertFalse(payment_request.can_user_delete(staff))
 
     def test_staff_cant_delete_from_paid(self):
         payment_request = PaymentRequestFactory(status=PAID)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.user_can_delete(staff))
+        self.assertFalse(payment_request.can_user_delete(staff))
 
     def test_staff_cant_delete_from_declined(self):
         payment_request = PaymentRequestFactory(status=DECLINED)
         staff = StaffFactory()
 
-        self.assertFalse(payment_request.user_can_delete(staff))
+        self.assertFalse(payment_request.can_user_delete(staff))
 
-    def test_user_can_delete_from_submitted(self):
+    def test_can_user_delete_from_submitted(self):
         payment_request = PaymentRequestFactory(status=SUBMITTED)
         user = ApplicantFactory()
 
-        self.assertTrue(payment_request.user_can_delete(user))
+        self.assertTrue(payment_request.can_user_delete(user))
 
     def test_user_cant_delete_from_under_review(self):
         payment_request = PaymentRequestFactory(status=UNDER_REVIEW)
         user = ApplicantFactory()
 
-        self.assertFalse(payment_request.user_can_delete(user))
+        self.assertFalse(payment_request.can_user_delete(user))
 
     def test_user_can_delete_from_changes_requested(self):
         payment_request = PaymentRequestFactory(status=CHANGES_REQUESTED)
         user = ApplicantFactory()
 
-        self.assertTrue(payment_request.user_can_delete(user))
+        self.assertTrue(payment_request.can_user_delete(user))
 
     def test_user_cant_delete_from_paid(self):
         payment_request = PaymentRequestFactory(status=PAID)
         user = ApplicantFactory()
 
-        self.assertFalse(payment_request.user_can_delete(user))
+        self.assertFalse(payment_request.can_user_delete(user))
 
     def test_user_cant_delete_from_declined(self):
         payment_request = PaymentRequestFactory(status=DECLINED)
         user = ApplicantFactory()
 
-        self.assertFalse(payment_request.user_can_delete(user))
+        self.assertFalse(payment_request.can_user_delete(user))
+
+
+class TestPaymentRequestsQueryset(TestCase):
+    def test_get_totals(self):
+        PaymentRequestFactory(value=20)
+        PaymentRequestFactory(value=10, status=PAID)
+
+        self.assertEqual(PaymentRequest.objects.paid_value(), 10)
+        self.assertEqual(PaymentRequest.objects.unpaid_value(), 20)
+
+    def test_get_totals_no_value(self):
+        self.assertEqual(PaymentRequest.objects.paid_value(), 0)
+        self.assertEqual(PaymentRequest.objects.unpaid_value(), 0)
diff --git a/opentech/apply/projects/tests/test_templatetags.py b/opentech/apply/projects/tests/test_templatetags.py
index 9cfde17da0ecef0ceebe7e30883b754a8dacdaee..a6ce390f9b258aab59776bc06cfe0a1662bcd820 100644
--- a/opentech/apply/projects/tests/test_templatetags.py
+++ b/opentech/apply/projects/tests/test_templatetags.py
@@ -18,7 +18,7 @@ from ..templatetags.contract_tools import user_can_upload_contract
 from ..templatetags.payment_request_tools import (
     can_change_status,
     can_delete,
-    user_can_edit
+    can_edit
 )
 from .factories import ContractFactory, PaymentRequestFactory, ProjectFactory
 
@@ -217,41 +217,41 @@ class TestPaymentRequestTools(TestCase):
         applicant = ApplicantFactory()
         staff = StaffFactory()
 
-        self.assertTrue(user_can_edit(payment_request, applicant))
-        self.assertTrue(user_can_edit(payment_request, staff))
+        self.assertTrue(can_edit(payment_request, applicant))
+        self.assertTrue(can_edit(payment_request, staff))
 
     def test_applicant_can_edit_in_changes_requested(self):
         payment_request = PaymentRequestFactory(status=CHANGES_REQUESTED)
         applicant = ApplicantFactory()
 
-        self.assertTrue(user_can_edit(payment_request, applicant))
+        self.assertTrue(can_edit(payment_request, applicant))
 
     def test_staff_cant_edit_in_changes_requested(self):
         payment_request = PaymentRequestFactory(status=CHANGES_REQUESTED)
         staff = StaffFactory()
 
-        self.assertFalse(user_can_edit(payment_request, staff))
+        self.assertFalse(can_edit(payment_request, staff))
 
     def test_applicant_and_staff_cant_edit_in_under_review(self):
         payment_request = PaymentRequestFactory(status=UNDER_REVIEW)
         applicant = ApplicantFactory()
         staff = StaffFactory()
 
-        self.assertFalse(user_can_edit(payment_request, applicant))
-        self.assertFalse(user_can_edit(payment_request, staff))
+        self.assertFalse(can_edit(payment_request, applicant))
+        self.assertFalse(can_edit(payment_request, staff))
 
     def test_applicant_and_staff_cant_edit_in_paid(self):
         payment_request = PaymentRequestFactory(status=PAID)
         applicant = ApplicantFactory()
         staff = StaffFactory()
 
-        self.assertFalse(user_can_edit(payment_request, applicant))
-        self.assertFalse(user_can_edit(payment_request, staff))
+        self.assertFalse(can_edit(payment_request, applicant))
+        self.assertFalse(can_edit(payment_request, staff))
 
     def test_applicant_and_staff_cant_edit_in_decline(self):
         payment_request = PaymentRequestFactory(status=DECLINED)
         applicant = ApplicantFactory()
         staff = StaffFactory()
 
-        self.assertFalse(user_can_edit(payment_request, applicant))
-        self.assertFalse(user_can_edit(payment_request, staff))
+        self.assertFalse(can_edit(payment_request, applicant))
+        self.assertFalse(can_edit(payment_request, staff))
diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py
index 5384d1100e93f1e06e9ab15bad5407f484f9060d..374d9352b130764ddccfbe9f4cd5cbbd37700a56 100644
--- a/opentech/apply/projects/tests/test_views.py
+++ b/opentech/apply/projects/tests/test_views.py
@@ -18,8 +18,15 @@ from opentech.apply.users.tests.factories import (
 from opentech.apply.utils.testing.tests import BaseViewTestCase
 
 from ..forms import SetPendingForm
-from ..models import CONTRACTING, IN_PROGRESS, PAID
-from ..views import ContractsMixin, PaymentsMixin, ProjectDetailSimplifiedView
+from ..files import get_files
+from ..models import (
+    CHANGES_REQUESTED,
+    COMMITTED,
+    CONTRACTING,
+    IN_PROGRESS,
+    SUBMITTED,
+)
+from ..views import ContractsMixin, ProjectDetailSimplifiedView
 from .factories import (
     ContractFactory,
     DocumentCategoryFactory,
@@ -33,27 +40,38 @@ from .factories import (
 class TestCreateApprovalView(BaseViewTestCase):
     base_view_name = 'detail'
     url_name = 'funds:projects:{}'
-    user_factory = StaffFactory
+    user_factory = ApproverFactory
 
     def get_kwargs(self, instance):
         return {'pk': instance.id}
 
     def test_creating_an_approval_happy_path(self):
-        project = ProjectFactory()
+        project = ProjectFactory(in_approval=True)
         self.assertEqual(project.approvals.count(), 0)
 
         response = self.post_page(project, {'form-submitted-add_approval_form': '', 'by': self.user.id})
         self.assertEqual(response.status_code, 200)
 
         project.refresh_from_db()
-        approval = project.approvals.first()
-
         self.assertEqual(project.approvals.count(), 1)
         self.assertFalse(project.is_locked)
         self.assertEqual(project.status, 'contracting')
 
+        approval = project.approvals.first()
         self.assertEqual(approval.project_id, project.pk)
 
+    def test_creating_an_approval_other_approver(self):
+        project = ProjectFactory(in_approval=True)
+        self.assertEqual(project.approvals.count(), 0)
+
+        other = self.user_factory()
+        response = self.post_page(project, {'form-submitted-add_approval_form': '', 'by': other.id})
+        self.assertEqual(response.status_code, 200)
+
+        project.refresh_from_db()
+        self.assertEqual(project.approvals.count(), 0)
+        self.assertTrue(project.is_locked)
+
 
 class BaseProjectDetailTestCase(BaseViewTestCase):
     url_name = 'funds:projects:{}'
@@ -78,7 +96,7 @@ class TestStaffProjectDetailView(BaseProjectDetailTestCase):
 
 
 class TestUserProjectDetailView(BaseProjectDetailTestCase):
-    user_factory = UserFactory
+    user_factory = ApplicantFactory
 
     def test_doesnt_have_access(self):
         project = ProjectFactory()
@@ -109,6 +127,62 @@ class TestReviewerUserProjectDetailView(BaseProjectDetailTestCase):
         self.assertEqual(response.status_code, 403)
 
 
+class TestStaffProjectRejectView(BaseProjectDetailTestCase):
+    user_factory = StaffFactory
+
+    def test_cant_reject(self):
+        project = ProjectFactory(in_approval=True)
+        response = self.post_page(project, {
+            'form-submitted-rejection_form': '',
+            'comment': 'needs to change',
+        })
+        self.assertEqual(response.status_code, 403)
+        project = self.refresh(project)
+        self.assertEqual(project.status, COMMITTED)
+        self.assertTrue(project.is_locked)
+
+
+class TestApproverProjectRejectView(BaseProjectDetailTestCase):
+    user_factory = ApproverFactory
+
+    def test_can_reject(self):
+        project = ProjectFactory(in_approval=True)
+        response = self.post_page(project, {
+            'form-submitted-rejection_form': '',
+            'comment': 'needs to change',
+        })
+        self.assertEqual(response.status_code, 200)
+        project = self.refresh(project)
+        self.assertEqual(project.status, COMMITTED)
+        self.assertFalse(project.is_locked)
+
+    def test_cant_reject_no_comment(self):
+        project = ProjectFactory(in_approval=True)
+        response = self.post_page(project, {
+            'form-submitted-rejection_form': '',
+            'comment': '',
+        })
+        self.assertEqual(response.status_code, 200)
+        project = self.refresh(project)
+        self.assertEqual(project.status, COMMITTED)
+        self.assertTrue(project.is_locked)
+
+
+class TestUserProjectRejectView(BaseProjectDetailTestCase):
+    user_factory = ApplicantFactory
+
+    def test_cant_reject(self):
+        project = ProjectFactory(in_approval=True, user=self.user)
+        response = self.post_page(project, {
+            'form-submitted-rejection_form': '',
+            'comment': 'needs to change',
+        })
+        self.assertEqual(response.status_code, 200)
+        project = self.refresh(project)
+        self.assertEqual(project.status, COMMITTED)
+        self.assertTrue(project.is_locked)
+
+
 class TestRemoveDocumentView(BaseViewTestCase):
     base_view_name = 'detail'
     url_name = 'funds:projects:{}'
@@ -260,6 +334,58 @@ class TestStaffUploadContractView(BaseViewTestCase):
         self.assertTrue(project.contracts.first().is_signed)
 
 
+class TestStaffSelectDocumentView(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:{}'
+    user_factory = StaffFactory
+
+    def get_kwargs(self, instance):
+        return {'pk': instance.id}
+
+    def test_can_choose(self):
+        category = DocumentCategoryFactory()
+        project = ProjectFactory()
+
+        files = get_files(project)
+
+        response = self.post_page(project, {
+            'form-submitted-select_document_form': '',
+            'category': category.id,
+            'document': files[0].url,
+        })
+        self.assertEqual(response.status_code, 200)
+
+        project.refresh_from_db()
+
+        self.assertEqual(project.packet_files.count(), 1)
+
+
+class TestApplicantSelectDocumentView(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:{}'
+    user_factory = ApplicantFactory
+
+    def get_kwargs(self, instance):
+        return {'pk': instance.id}
+
+    def test_can_choose(self):
+        category = DocumentCategoryFactory()
+        project = ProjectFactory(user=self.user)
+
+        files = get_files(project)
+
+        response = self.post_page(project, {
+            'form-submitted-select_document_form': '',
+            'category': category.id,
+            'document': files[0].url,
+        })
+        self.assertEqual(response.status_code, 200)
+
+        project.refresh_from_db()
+
+        self.assertEqual(project.packet_files.count(), 1)
+
+
 class TestUploadDocumentView(BaseViewTestCase):
     base_view_name = 'detail'
     url_name = 'funds:projects:{}'
@@ -700,31 +826,6 @@ class TestRequestPaymentViewAsStaff(BaseViewTestCase):
         self.assertEqual(project.payment_requests.first().by, self.user)
 
 
-class TestPaymentsMixin(TestCase):
-    def test_get_totals(self):
-        project = ProjectFactory(value=Decimal(100))
-        user = UserFactory()
-
-        PaymentRequestFactory(project=project, by=user, value=20)
-        PaymentRequestFactory(project=project, by=user, value=10, status=PAID)
-
-        values = PaymentsMixin().get_totals(project)
-
-        self.assertEqual(values['awaiting_absolute'], 20)
-        self.assertEqual(values['awaiting_percentage'], 20)
-        self.assertEqual(values['paid_absolute'], 10)
-        self.assertEqual(values['paid_percentage'], 10)
-
-    def test_get_totals_no_value(self):
-        project = ProjectFactory(value=Decimal(0))
-        values = PaymentsMixin().get_totals(project)
-
-        self.assertEqual(values['awaiting_absolute'], 0)
-        self.assertEqual(values['awaiting_percentage'], 0)
-        self.assertEqual(values['paid_absolute'], 0)
-        self.assertEqual(values['paid_percentage'], 0)
-
-
 class TestProjectDetailSimplifiedView(TestCase):
     def test_staff_only(self):
         factory = RequestFactory()
@@ -741,13 +842,58 @@ class TestProjectDetailSimplifiedView(TestCase):
             ProjectDetailSimplifiedView.as_view()(request, pk=project.pk)
 
 
+class TestStaffDetailPaymentRequestStatus(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = StaffFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can(self):
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request)
+        self.assertEqual(response.status_code, 200)
+
+    def test_wrong_project_cant(self):
+        other_project = ProjectFactory()
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request, url_kwargs={'pk': other_project.pk})
+        self.assertEqual(response.status_code, 404)
+
+
+class TestApplicantDetailPaymentRequestStatus(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = ApplicantFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can(self):
+        payment_request = PaymentRequestFactory(project__user=self.user)
+        response = self.get_page(payment_request)
+        self.assertEqual(response.status_code, 200)
+
+    def test_other_cant(self):
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request)
+        self.assertEqual(response.status_code, 403)
+
+
 class TestApplicantEditPaymentRequestView(BaseViewTestCase):
-    base_view_name = 'edit-payment-request'
-    url_name = 'funds:projects:{}'
+    base_view_name = 'edit'
+    url_name = 'funds:projects:payments:{}'
     user_factory = ApplicantFactory
 
     def get_kwargs(self, instance):
-        return {'pk': instance.project.pk, 'payment_request_id': instance.pk}
+        return {'pk': instance.project.pk, 'pr_pk': instance.pk}
 
     def test_editing_payment_request_fires_messaging(self):
         project = ProjectFactory(user=self.user)
@@ -760,7 +906,6 @@ class TestApplicantEditPaymentRequestView(BaseViewTestCase):
         invoice.name = 'invoice.pdf'
 
         response = self.post_page(payment_request, {
-            'form-submitted-edit_request_payment_form': '',
             'value': value + 1,
             'date_from': '2018-08-15',
             'date_to': '2019-08-15',
@@ -780,12 +925,12 @@ class TestApplicantEditPaymentRequestView(BaseViewTestCase):
 
 
 class TestStaffEditPaymentRequestView(BaseViewTestCase):
-    base_view_name = 'edit-payment-request'
-    url_name = 'funds:projects:{}'
+    base_view_name = 'edit'
+    url_name = 'funds:projects:payments:{}'
     user_factory = StaffFactory
 
     def get_kwargs(self, instance):
-        return {'pk': instance.project.pk, 'payment_request_id': instance.pk}
+        return {'pk': instance.project.pk, 'pr_pk': instance.pk}
 
     def test_editing_payment_request_fires_messaging(self):
         project = ProjectFactory()
@@ -798,7 +943,6 @@ class TestStaffEditPaymentRequestView(BaseViewTestCase):
         invoice.name = 'invoice.pdf'
 
         response = self.post_page(payment_request, {
-            'form-submitted-request_payment_form': '',
             'value': value + 1,
             'date_from': '2018-08-15',
             'date_to': '2019-08-15',
@@ -815,3 +959,155 @@ class TestStaffEditPaymentRequestView(BaseViewTestCase):
         self.assertEqual(project.payment_requests.first().pk, payment_request.pk)
 
         self.assertEqual(value + Decimal("1"), payment_request.value)
+
+
+class TestStaffChangePaymentRequestStatus(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = StaffFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can(self):
+        payment_request = PaymentRequestFactory()
+        response = self.post_page(payment_request, {
+            'form-submitted-change_payment_status': '',
+            'status': CHANGES_REQUESTED,
+        })
+        self.assertEqual(response.status_code, 200)
+        payment_request.refresh_from_db()
+        self.assertEqual(payment_request.status, CHANGES_REQUESTED)
+
+
+class TestApplicantChangePaymentRequestStatus(BaseViewTestCase):
+    base_view_name = 'detail'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = ApplicantFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can(self):
+        payment_request = PaymentRequestFactory(project__user=self.user)
+        response = self.post_page(payment_request, {
+            'form-submitted-change_payment_status': '',
+            'status': CHANGES_REQUESTED,
+        })
+        self.assertEqual(response.status_code, 200)
+        payment_request.refresh_from_db()
+        self.assertEqual(payment_request.status, SUBMITTED)
+
+    def test_other_cant(self):
+        payment_request = PaymentRequestFactory()
+        response = self.post_page(payment_request, {
+            'form-submitted-change_payment_status': '',
+            'status': CHANGES_REQUESTED,
+        })
+        self.assertEqual(response.status_code, 403)
+        payment_request.refresh_from_db()
+        self.assertEqual(payment_request.status, SUBMITTED)
+
+
+class TestStaffPaymentRequestInvoicePrivateMedia(BaseViewTestCase):
+    base_view_name = 'invoice'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = StaffFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can_access(self):
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request)
+        self.assertContains(response, payment_request.invoice.read())
+
+    def test_cant_access_if_project_wrong(self):
+        other_project = ProjectFactory()
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request, url_kwargs={'pk': other_project.pk})
+        self.assertEqual(response.status_code, 404)
+
+
+class TestApplicantPaymentRequestInvoicePrivateMedia(BaseViewTestCase):
+    base_view_name = 'invoice'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = ApplicantFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.project.pk,
+            'pr_pk': instance.pk,
+        }
+
+    def test_can_access_own(self):
+        payment_request = PaymentRequestFactory(project__user=self.user)
+        response = self.get_page(payment_request)
+        self.assertContains(response, payment_request.invoice.read())
+
+    def test_cant_access_other(self):
+        payment_request = PaymentRequestFactory()
+        response = self.get_page(payment_request)
+        self.assertEqual(response.status_code, 403)
+
+
+class TestStaffPaymentRequestReceiptPrivateMedia(BaseViewTestCase):
+    base_view_name = 'receipt'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = StaffFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.payment_request.project.pk,
+            'pr_pk': instance.payment_request.pk,
+            'file_pk': instance.pk,
+        }
+
+    def test_can_access(self):
+        payment_receipt = PaymentReceiptFactory()
+        response = self.get_page(payment_receipt)
+        self.assertContains(response, payment_receipt.file.read())
+
+    def test_cant_access_if_project_wrong(self):
+        other_project = ProjectFactory()
+        payment_receipt = PaymentReceiptFactory()
+        response = self.get_page(payment_receipt, url_kwargs={'pk': other_project.pk})
+        self.assertEqual(response.status_code, 404)
+
+    def test_cant_access_if_request_is_wrong(self):
+        other_request = PaymentRequestFactory()
+        payment_receipt = PaymentReceiptFactory()
+        response = self.get_page(payment_receipt, url_kwargs={'pr_pk': other_request.pk})
+        self.assertEqual(response.status_code, 404)
+
+
+class TestApplicantPaymentRequestReceiptPrivateMedia(BaseViewTestCase):
+    base_view_name = 'receipt'
+    url_name = 'funds:projects:payments:{}'
+    user_factory = ApplicantFactory
+
+    def get_kwargs(self, instance):
+        return {
+            'pk': instance.payment_request.project.pk,
+            'pr_pk': instance.payment_request.pk,
+            'file_pk': instance.pk,
+        }
+
+    def test_can_access_own(self):
+        payment_receipt = PaymentReceiptFactory(payment_request__project__user=self.user)
+        response = self.get_page(payment_receipt)
+        self.assertContains(response, payment_receipt.file.read())
+
+    def test_cant_access_other(self):
+        payment_receipt = PaymentReceiptFactory()
+        response = self.get_page(payment_receipt)
+        self.assertEqual(response.status_code, 403)
diff --git a/opentech/apply/projects/urls.py b/opentech/apply/projects/urls.py
index 8ca299d95bc1d388ca767702170389afc13c4e8b..d4f135205cac93b1357952e3a56c0e4b31241d07 100644
--- a/opentech/apply/projects/urls.py
+++ b/opentech/apply/projects/urls.py
@@ -6,6 +6,8 @@ from .views import (
     ChangePaymentRequestStatusView,
     DeletePaymentRequestView,
     EditPaymentRequestView,
+    PaymentRequestView,
+    PaymentRequestPrivateMedia,
     ProjectDetailSimplifiedView,
     ProjectDetailView,
     ProjectEditView,
@@ -38,16 +40,13 @@ if settings.PROJECTS_ENABLED:
                 SelectDocumentView.as_view(),
                 name="copy-documents",
             ),
-            path(
-                'delete-payment-request/<int:payment_request_id>/',
-                DeletePaymentRequestView.as_view(),
-                name='delete-payment-request',
-            ),
-            path(
-                'edit-payment-request/<int:payment_request_id>/',
-                EditPaymentRequestView.as_view(),
-                name='edit-payment-request',
-            ),
             path('simplified/', ProjectDetailSimplifiedView.as_view(), name='simplified'),
+            path('payment-requests/<int:pr_pk>/', include(([
+                path('', PaymentRequestView.as_view(), name='detail'),
+                path('edit/', EditPaymentRequestView.as_view(), name='edit'),
+                path('delete/', DeletePaymentRequestView.as_view(), name='delete'),
+                path('documents/invoice/', PaymentRequestPrivateMedia.as_view(), name="invoice"),
+                path('documents/receipt/<int:file_pk>/', PaymentRequestPrivateMedia.as_view(), name="receipt"),
+            ], 'payments'))),
         ])),
     ]
diff --git a/opentech/apply/projects/views/__init__.py b/opentech/apply/projects/views/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa45c29c176b0b33d5b01a6e2e926dc9f487fbc7
--- /dev/null
+++ b/opentech/apply/projects/views/__init__.py
@@ -0,0 +1,2 @@
+from .payment import *  # NOQA
+from .project import *  # NOQA
diff --git a/opentech/apply/projects/views/payment.py b/opentech/apply/projects/views/payment.py
new file mode 100644
index 0000000000000000000000000000000000000000..952ee30ee3371714a8e1b77eaad7686fd77ee570
--- /dev/null
+++ b/opentech/apply/projects/views/payment.py
@@ -0,0 +1,202 @@
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.core.exceptions import PermissionDenied
+from django.db import transaction
+from django.http import Http404
+from django.shortcuts import get_object_or_404, redirect
+from django.utils.decorators import method_decorator
+from django.views.generic import (
+    CreateView,
+    DeleteView,
+    DetailView,
+    UpdateView
+)
+
+from opentech.apply.activity.messaging import MESSAGES, messenger
+from opentech.apply.users.decorators import staff_required
+from opentech.apply.utils.storage import PrivateMediaView
+from opentech.apply.utils.views import (
+    DelegateableView,
+    DelegatedViewMixin,
+    ViewDispatcher,
+)
+
+from ..forms import (
+    ChangePaymentRequestStatusForm,
+    EditPaymentRequestForm,
+    RequestPaymentForm,
+)
+from ..models import (
+    PaymentRequest,
+    Project
+)
+
+
+@method_decorator(login_required, name='dispatch')
+class PaymentRequestAccessMixin:
+    model = PaymentRequest
+    pk_url_kwarg = 'pr_pk'
+
+    def dispatch(self, request, *args, **kwargs):
+        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+        if self.get_object().project != self.project:
+            raise Http404
+
+        is_admin = request.user.is_apply_staff
+        is_owner = request.user == self.project.user
+        if not (is_owner or is_admin):
+            raise PermissionDenied
+
+        return super().dispatch(request, *args, **kwargs)
+
+
+@method_decorator(staff_required, name='dispatch')
+class ChangePaymentRequestStatusView(DelegatedViewMixin, PaymentRequestAccessMixin, UpdateView):
+    form_class = ChangePaymentRequestStatusForm
+    context_name = 'change_payment_status'
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.pop('user')
+        return kwargs
+
+    def form_valid(self, form):
+        response = super().form_valid(form)
+
+        messenger(
+            MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS,
+            request=self.request,
+            user=self.request.user,
+            source=self.project,
+            related=self.object,
+        )
+
+        return response
+
+
+class DeletePaymentRequestView(DeleteView):
+    model = PaymentRequest
+    pk_url_kwarg = 'payment_request_id'
+
+    def dispatch(self, request, *args, **kwargs):
+        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+
+        self.object = self.get_object()
+        if not self.object.user_can_delete(request.user):
+            raise PermissionDenied
+
+        return super().dispatch(request, *args, **kwargs)
+
+    @transaction.atomic()
+    def delete(self, request, *args, **kwargs):
+        response = super().delete(request, *args, **kwargs)
+
+        messenger(
+            MESSAGES.DELETE_PAYMENT_REQUEST,
+            request=self.request,
+            user=self.request.user,
+            source=self.project,
+            related=self.object,
+        )
+
+        return response
+
+    def get_success_url(self):
+        return self.project.get_absolute_url()
+
+
+class PaymentRequestAdminView(PaymentRequestAccessMixin, DelegateableView, DetailView):
+    form_views = [
+        ChangePaymentRequestStatusView
+    ]
+    template_name_suffix = '_admin_detail'
+
+
+class PaymentRequestApplicantView(PaymentRequestAccessMixin, DelegateableView, DetailView):
+    form_views = []
+
+
+class PaymentRequestView(ViewDispatcher):
+    admin_view = PaymentRequestAdminView
+    applicant_view = PaymentRequestApplicantView
+
+
+class EditPaymentRequestView(PaymentRequestAccessMixin, UpdateView):
+    form_class = EditPaymentRequestForm
+
+    def dispatch(self, request, *args, **kwargs):
+        payment_request = self.get_object()
+        if not payment_request.can_user_edit(request.user):
+            return redirect(payment_request)
+        return super().dispatch(request, *args, **kwargs)
+
+    def form_valid(self, form):
+        response = super().form_valid(form)
+
+        messenger(
+            MESSAGES.UPDATE_PAYMENT_REQUEST,
+            request=self.request,
+            user=self.request.user,
+            source=self.project,
+            related=self.object,
+        )
+
+        return response
+
+    def get_success_url(self):
+        return self.project.get_absolute_url()
+
+
+@method_decorator(login_required, name='dispatch')
+class PaymentRequestPrivateMedia(UserPassesTestMixin, PrivateMediaView):
+    raise_exception = True
+
+    def dispatch(self, *args, **kwargs):
+        project_pk = self.kwargs['pk']
+        payment_pk = self.kwargs['pr_pk']
+        self.project = get_object_or_404(Project, pk=project_pk)
+        self.payment_request = get_object_or_404(PaymentRequest, pk=payment_pk)
+
+        if self.payment_request.project != self.project:
+            raise Http404
+
+        return super().dispatch(*args, **kwargs)
+
+    def get_media(self, *args, **kwargs):
+        file_pk = kwargs.get('file_pk')
+        if not file_pk:
+            return self.payment_request.invoice
+
+        receipt = get_object_or_404(self.payment_request.receipts, pk=file_pk)
+        return receipt.file
+
+    def test_func(self):
+        if self.request.user.is_apply_staff:
+            return True
+
+        if self.request.user == self.project.user:
+            return True
+
+        return False
+
+
+class RequestPaymentView(DelegatedViewMixin, CreateView):
+    context_name = 'request_payment_form'
+    form_class = RequestPaymentForm
+    model = PaymentRequest
+
+    def form_valid(self, form):
+        project = self.kwargs['object']
+
+        form.instance.by = self.request.user
+        form.instance.project = project
+        response = super().form_valid(form)
+
+        messenger(
+            MESSAGES.REQUEST_PAYMENT,
+            request=self.request,
+            user=self.request.user,
+            source=project,
+        )
+
+        return response
diff --git a/opentech/apply/projects/views.py b/opentech/apply/projects/views/project.py
similarity index 62%
rename from opentech/apply/projects/views.py
rename to opentech/apply/projects/views/project.py
index 6b62ab41607fca2fbbac4688a058892b2238307f..871629184ac4d56ec17e0d017ebb134894935f37 100644
--- a/opentech/apply/projects/views.py
+++ b/opentech/apply/projects/views/project.py
@@ -1,4 +1,3 @@
-import decimal
 from copy import copy
 
 from django.contrib import messages
@@ -6,7 +5,6 @@ from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.exceptions import PermissionDenied
 from django.db import transaction
-from django.db.models import Q
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect
 from django.utils.decorators import method_decorator
@@ -14,7 +12,6 @@ from django.utils.functional import cached_property
 from django.utils.translation import ugettext_lazy as _
 from django.views.generic import (
     CreateView,
-    DeleteView,
     DetailView,
     FormView,
     UpdateView
@@ -22,225 +19,62 @@ from django.views.generic import (
 
 from opentech.apply.activity.messaging import MESSAGES, messenger
 from opentech.apply.activity.views import ActivityContextMixin, CommentFormView
-from opentech.apply.users.decorators import staff_required
+from opentech.apply.users.decorators import approver_required, staff_required
 from opentech.apply.utils.storage import PrivateMediaView
 from opentech.apply.utils.views import (
     DelegateableView,
     DelegatedViewMixin,
-    ViewDispatcher
+    ViewDispatcher,
 )
 
-from .files import get_files
-from .forms import (
+from ..files import get_files
+from ..forms import (
     ApproveContractForm,
-    ChangePaymentRequestStatusForm,
     CreateApprovalForm,
-    EditPaymentRequestForm,
     ProjectApprovalForm,
     ProjectEditForm,
     RejectionForm,
     RemoveDocumentForm,
-    RequestPaymentForm,
     SelectDocumentForm,
     SetPendingForm,
+    StaffUploadContractForm,
     UpdateProjectLeadForm,
     UploadContractForm,
     UploadDocumentForm
 )
-from .models import (
-    CHANGES_REQUESTED,
+from ..models import (
     CONTRACTING,
-    DECLINED,
     IN_PROGRESS,
-    PAID,
     PROJECT_STATUS_CHOICES,
-    REQUEST_STATUS_CHOICES,
-    SUBMITTED,
-    UNDER_REVIEW,
     Approval,
     Contract,
     PacketFile,
-    PaymentRequest,
     Project
 )
+from .payment import RequestPaymentView
 
 
-class ContractsMixin:
-    def get_context_data(self, **kwargs):
-        project = self.get_object()
-        contracts = (project.contracts.select_related('approver')
-                                      .order_by('-created_at'))
-
-        latest_contract = self.get_contract_to_approve(contracts)
-
-        contracts = contracts.filter(is_signed=True, approver__isnull=False)
-
-        if latest_contract:
-            contracts = [latest_contract, *contracts]
-
-        context = super().get_context_data(**kwargs)
-        context['latest_contract'] = latest_contract
-        context['contracts'] = contracts
-        return context
-
-    def get_contract_to_approve(self, contracts):
-        """If there's a contract to approve, get that"""
-        latest = contracts.first()
-
-        if not latest:
-            return
-
-        if latest.approver:
-            return
-
-        return latest
-
-
-class PaymentsMixin:
-    def get_context_data(self, **kwargs):
-        project = self.get_object()
-
-        payments = {
-            'availabe_statuses': REQUEST_STATUS_CHOICES,
-            'not_rejected': project.payment_requests.exclude(status=DECLINED),
-            'rejected': project.payment_requests.filter(status=DECLINED),
-            'totals': self.get_totals(project),
-        }
-
-        context = super().get_context_data(**kwargs)
-        context['payments'] = payments
-        context['edit_payment_request_forms'] = list(self.get_edit_payment_request_forms())
-        return context
-
-    def get_edit_payment_request_forms(self):
-        """
-        Get an iterable of EditPaymentRequestForms
-
-        We want to instantiate each EditPaymentRequestForm with a given
-        PaymentRequest.  Each subclass of this mixin defines
-        .get_payment_requests_queryset() so we can change the available forms
-        based on the type of user viewing (applicant or staff).
-        """
-        payment_requests = self.get_payment_requests_queryset().select_related('project')
-        for payment_request in payment_requests:
-            yield EditPaymentRequestForm(instance=payment_request)
-
-    def get_totals(self, project):
-        def percentage(total, value):
-            if not total:
-                return decimal.Decimal(0)
-
-            unrounded_total = (value / total) * 100
-
-            # round using Decimal since we're dealing with currency
-            rounded_total = unrounded_total.quantize(
-                decimal.Decimal('0.0'),
-                rounding=decimal.ROUND_DOWN,
-            )
-
-            return rounded_total
-
-        unpaid_requests = project.payment_requests.filter(Q(status=SUBMITTED) | Q(status=UNDER_REVIEW))
-        awaiting_absolute = sum(unpaid_requests.values_list('value', flat=True))
-        awaiting_percentage = percentage(project.value, awaiting_absolute)
-
-        paid_requests = project.payment_requests.filter(status=PAID)
-        paid_absolute = sum(paid_requests.values_list('value', flat=True))
-        paid_percentage = percentage(project.value, paid_absolute)
-
-        return {
-            'awaiting_absolute': awaiting_absolute,
-            'awaiting_percentage': awaiting_percentage,
-            'paid_absolute': paid_absolute,
-            'paid_percentage': paid_percentage,
-        }
-
-
-class SubmissionFilesMixin:
-    """
-    Mixin to provide an instantiated SelectDocumentForm
-    """
-    def get_context_data(self, **kwargs):
-        project = self.get_object()
-
-        files = get_files(project)
-
-        context = super().get_context_data(**kwargs)
-        context['select_document_form'] = SelectDocumentForm(files, project)
-        return context
-
+# APPROVAL VIEWS
 
 @method_decorator(staff_required, name='dispatch')
-class ApproveContractView(UpdateView):
-    form_class = ApproveContractForm
-    model = Contract
-    pk_url_kwarg = 'contract_pk'
-
-    def dispatch(self, request, *args, **kwargs):
-        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
-        return super().dispatch(request, *args, **kwargs)
-
-    def form_invalid(self, form):
-        for error in form.errors:
-            messages.error(self.request, error)
-
-        return redirect(self.project)
-
-    def form_valid(self, form):
-        with transaction.atomic():
-            form.instance.approver = self.request.user
-            form.instance.project = self.project
-            response = super().form_valid(form)
-
-            messenger(
-                MESSAGES.APPROVE_CONTRACT,
-                request=self.request,
-                user=self.request.user,
-                source=self.project,
-                related=self.object,
-            )
-
-            self.project.status = IN_PROGRESS
-            self.project.save(update_fields=['status'])
-
-        return response
-
-    def get_success_url(self):
-        return self.project.get_absolute_url()
-
-
-@method_decorator(staff_required, name='dispatch')
-class ChangePaymentRequestStatusView(UpdateView):
-    form_class = ChangePaymentRequestStatusForm
-    model = PaymentRequest
-    pk_url_kwarg = 'payment_request_id'
-
-    def dispatch(self, request, *args, **kwargs):
-        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
-        return super().dispatch(request, *args, **kwargs)
-
-    def form_invalid(self, form):
-        for error in form.errors:
-            messages.error(self.request, error)
-
-        return redirect(self.project)
+class SendForApprovalView(DelegatedViewMixin, UpdateView):
+    context_name = 'request_approval_form'
+    form_class = SetPendingForm
+    model = Project
 
     def form_valid(self, form):
+        # lock project
         response = super().form_valid(form)
 
         messenger(
-            MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS,
+            MESSAGES.SEND_FOR_APPROVAL,
             request=self.request,
             user=self.request.user,
-            source=self.project,
-            related=self.object,
+            source=self.object,
         )
 
         return response
 
-    def get_success_url(self):
-        return self.project.get_absolute_url()
-
 
 @method_decorator(staff_required, name='dispatch')
 class CreateApprovalView(DelegatedViewMixin, CreateView):
@@ -248,10 +82,17 @@ class CreateApprovalView(DelegatedViewMixin, CreateView):
     form_class = CreateApprovalForm
     model = Approval
 
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.pop('instance')
+        return kwargs
+
     @transaction.atomic()
     def form_valid(self, form):
         project = self.kwargs['object']
+
         form.instance.project = project
+
         response = super().form_valid(form)
 
         messenger(
@@ -270,93 +111,49 @@ class CreateApprovalView(DelegatedViewMixin, CreateView):
         return response
 
 
-class DeletePaymentRequestView(DeleteView):
-    model = PaymentRequest
-    pk_url_kwarg = 'payment_request_id'
-
-    def dispatch(self, request, *args, **kwargs):
-        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
-
-        self.object = self.get_object()
-        if not self.object.user_can_delete(request.user):
-            raise PermissionDenied
-
-        return super().dispatch(request, *args, **kwargs)
-
-    @transaction.atomic()
-    def delete(self, request, *args, **kwargs):
-        response = super().delete(request, *args, **kwargs)
+@method_decorator(approver_required, name='dispatch')
+class RejectionView(DelegatedViewMixin, UpdateView):
+    context_name = 'rejection_form'
+    form_class = RejectionForm
+    model = Project
 
+    def form_valid(self, form):
         messenger(
-            MESSAGES.DELETE_PAYMENT_REQUEST,
+            MESSAGES.REQUEST_PROJECT_CHANGE,
             request=self.request,
             user=self.request.user,
-            source=self.project,
-            related=self.object,
+            source=self.object,
+            comment=form.cleaned_data['comment'],
         )
 
-        return response
-
-    def get_success_url(self):
-        return self.project.get_absolute_url()
-
-
-class EditPaymentRequestView(UpdateView):
-    form_class = EditPaymentRequestForm
-    model = PaymentRequest
-    pk_url_kwarg = 'payment_request_id'
+        self.object.is_locked = False
+        self.object.save(update_fields=['is_locked'])
 
-    def dispatch(self, request, *args, **kwargs):
-        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+        return redirect(self.object)
 
-        is_admin = request.user.is_apply_staff
-        is_owner = request.user == self.project.user
-        if not (is_owner or is_admin):
-            raise PermissionDenied
 
-        return super().dispatch(request, *args, **kwargs)
+# PROJECT DOCUMENTS
 
-    def form_invalid(self, form):
-        messages.error(self.request, form.errors)
-        return redirect(self.project)
+@method_decorator(staff_required, name='dispatch')
+class UploadDocumentView(DelegatedViewMixin, CreateView):
+    context_name = 'document_form'
+    form_class = UploadDocumentForm
+    model = Project
 
     def form_valid(self, form):
+        project = self.kwargs['object']
+        form.instance.project = project
         response = super().form_valid(form)
 
         messenger(
-            MESSAGES.UPDATE_PAYMENT_REQUEST,
+            MESSAGES.UPLOAD_DOCUMENT,
             request=self.request,
             user=self.request.user,
-            source=self.project,
-            related=self.object,
+            source=project,
         )
 
         return response
 
-    def get_success_url(self):
-        return self.project.get_absolute_url()
-
-
-@method_decorator(staff_required, name='dispatch')
-class RejectionView(DelegatedViewMixin, UpdateView):
-    context_name = 'rejection_form'
-    form_class = RejectionForm
-    model = Project
-
-    def form_valid(self, form):
-        messenger(
-            MESSAGES.REQUEST_PROJECT_CHANGE,
-            request=self.request,
-            user=self.request.user,
-            source=self.object,
-            comment=form.cleaned_data['comment'],
-        )
-
-        self.object.is_locked = False
-        self.object.save(update_fields=['is_locked'])
-
-        return redirect(self.object)
-
 
 @method_decorator(staff_required, name='dispatch')
 class RemoveDocumentView(DelegatedViewMixin, FormView):
@@ -376,33 +173,11 @@ class RemoveDocumentView(DelegatedViewMixin, FormView):
         return redirect(project)
 
 
-class RequestPaymentView(DelegatedViewMixin, CreateView):
-    context_name = 'request_payment_form'
-    form_class = RequestPaymentForm
-    model = PaymentRequest
-
-    def form_valid(self, form):
-        project = self.kwargs['object']
-
-        form.instance.by = self.request.user
-        form.instance.project = project
-        response = super().form_valid(form)
-
-        messenger(
-            MESSAGES.APPROVE_PROJECT,
-            request=self.request,
-            user=self.request.user,
-            source=project,
-        )
-
-        return response
-
-
 @method_decorator(login_required, name='dispatch')
-class SelectDocumentView(SubmissionFilesMixin, FormView):
-    context_name = 'select_document_form'
+class SelectDocumentView(DelegatedViewMixin, CreateView):
     form_class = SelectDocumentForm
-    model = Project
+    context_name = 'select_document_form'
+    model = PacketFile
 
     def dispatch(self, request, *args, **kwargs):
         self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
@@ -415,6 +190,9 @@ class SelectDocumentView(SubmissionFilesMixin, FormView):
         return redirect(self.project)
 
     def form_valid(self, form):
+        form.instance.project = self.project
+        form.instance.name = form.instance.document.name
+
         response = super().form_valid(form)
 
         messenger(
@@ -428,33 +206,13 @@ class SelectDocumentView(SubmissionFilesMixin, FormView):
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
-        kwargs['existing_files'] = get_files(self.project)
-        kwargs['project'] = self.project
+        kwargs.pop('user')
+        kwargs.pop('instance')
+        kwargs['existing_files'] = get_files(self.get_parent_object())
         return kwargs
 
-    def get_success_url(self):
-        return self.project.get_absolute_url()
-
-
-@method_decorator(staff_required, name='dispatch')
-class SendForApprovalView(DelegatedViewMixin, UpdateView):
-    context_name = 'request_approval_form'
-    form_class = SetPendingForm
-    model = Project
-
-    def form_valid(self, form):
-        # lock project
-        response = super().form_valid(form)
-
-        messenger(
-            MESSAGES.SEND_FOR_APPROVAL,
-            request=self.request,
-            user=self.request.user,
-            source=self.object,
-        )
-
-        return response
 
+# GENERAL FORM VIEWS
 
 @method_decorator(staff_required, name='dispatch')
 class UpdateLeadView(DelegatedViewMixin, UpdateView):
@@ -479,10 +237,81 @@ class UpdateLeadView(DelegatedViewMixin, UpdateView):
         return response
 
 
+# CONTRACTS
+
+class ContractsMixin:
+    def get_context_data(self, **kwargs):
+        project = self.get_object()
+        contracts = (project.contracts.select_related('approver')
+                                      .order_by('-created_at'))
+
+        latest_contract = self.get_contract_to_approve(contracts)
+
+        contracts = contracts.filter(is_signed=True, approver__isnull=False)
+
+        if latest_contract:
+            contracts = [latest_contract, *contracts]
+
+        context = super().get_context_data(**kwargs)
+        context['latest_contract'] = latest_contract
+        context['contracts'] = contracts
+        return context
+
+    def get_contract_to_approve(self, contracts):
+        """If there's a contract to approve, get that"""
+        latest = contracts.first()
+
+        if not latest:
+            return
+
+        if latest.approver:
+            return
+
+        return latest
+
+
+@method_decorator(staff_required, name='dispatch')
+class ApproveContractView(UpdateView):
+    form_class = ApproveContractForm
+    model = Contract
+    pk_url_kwarg = 'contract_pk'
+
+    def dispatch(self, request, *args, **kwargs):
+        self.project = get_object_or_404(Project, pk=self.kwargs['pk'])
+        return super().dispatch(request, *args, **kwargs)
+
+    def form_invalid(self, form):
+        for error in form.errors:
+            messages.error(self.request, error)
+
+        return redirect(self.project)
+
+    def form_valid(self, form):
+        with transaction.atomic():
+            form.instance.approver = self.request.user
+            form.instance.project = self.project
+            response = super().form_valid(form)
+
+            messenger(
+                MESSAGES.APPROVE_CONTRACT,
+                request=self.request,
+                user=self.request.user,
+                source=self.project,
+                related=self.object,
+            )
+
+            self.project.status = IN_PROGRESS
+            self.project.save(update_fields=['status'])
+
+        return response
+
+    def get_success_url(self):
+        return self.project.get_absolute_url()
+
+
 @method_decorator(login_required, name='dispatch')
 class UploadContractView(DelegatedViewMixin, CreateView):
     context_name = 'contract_form'
-    form_class = UploadContractForm
     model = Project
 
     def dispatch(self, request, *args, **kwargs):
@@ -495,8 +324,20 @@ class UploadContractView(DelegatedViewMixin, CreateView):
 
         return response
 
+    def get_form_class(self):
+        if self.request.user.is_apply_staff:
+            return StaffUploadContractForm
+        return UploadContractForm
+
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        kwargs.pop('instance')
+        kwargs.pop('user')
+        return kwargs
+
     def form_valid(self, form):
         project = self.kwargs['object']
+
         form.instance.project = project
 
         if self.request.user == project.user:
@@ -514,33 +355,12 @@ class UploadContractView(DelegatedViewMixin, CreateView):
         return response
 
 
-@method_decorator(staff_required, name='dispatch')
-class UploadDocumentView(DelegatedViewMixin, CreateView):
-    context_name = 'document_form'
-    form_class = UploadDocumentForm
-    model = Project
-
-    def form_valid(self, form):
-        project = self.kwargs['object']
-        form.instance.project = project
-        response = super().form_valid(form)
-
-        messenger(
-            MESSAGES.UPLOAD_DOCUMENT,
-            request=self.request,
-            user=self.request.user,
-            source=project,
-        )
-
-        return response
-
+# PROJECT VIEW
 
 class AdminProjectDetailView(
     ActivityContextMixin,
     DelegateableView,
     ContractsMixin,
-    PaymentsMixin,
-    SubmissionFilesMixin,
     DetailView,
 ):
     form_views = [
@@ -549,6 +369,7 @@ class AdminProjectDetailView(
         RejectionView,
         RemoveDocumentView,
         RequestPaymentView,
+        SelectDocumentView,
         SendForApprovalView,
         UpdateLeadView,
         UploadContractView,
@@ -563,29 +384,15 @@ class AdminProjectDetailView(
         context['current_status_index'] = [status for status, _ in PROJECT_STATUS_CHOICES].index(self.object.status)
         context['approvals'] = self.object.approvals.distinct('by')
         context['approve_contract_form'] = ApproveContractForm()
-        context['change_payment_request_status_forms'] = list(self.get_change_payment_request_status_forms())
         context['remaining_document_categories'] = list(self.object.get_missing_document_categories())
         return context
 
-    def get_change_payment_request_status_forms(self):
-        """
-        Get an iterable of ChangePaymentRequestStatusForms
-
-        We want to filter the available options based on the current
-        PaymentRequest object so we need to initialise those forms outside of
-        the template.
-        """
-        for payment_request in self.object.payment_requests.exclude(status=DECLINED).select_related('project'):
-            yield ChangePaymentRequestStatusForm(instance=payment_request)
-
-    def get_payment_requests_queryset(self):
-        return self.object.payment_requests.filter(status=SUBMITTED)
 
-
-class ApplicantProjectDetailView(ActivityContextMixin, DelegateableView, ContractsMixin, PaymentsMixin, DetailView):
+class ApplicantProjectDetailView(ActivityContextMixin, DelegateableView, ContractsMixin, DetailView):
     form_views = [
         CommentFormView,
         RequestPaymentView,
+        SelectDocumentView,
         UploadContractView,
     ]
 
@@ -594,13 +401,14 @@ class ApplicantProjectDetailView(ActivityContextMixin, DelegateableView, Contrac
 
     def dispatch(self, request, *args, **kwargs):
         project = self.get_object()
-        # This view is only for applicants.
         if project.user != request.user:
             raise PermissionDenied
         return super().dispatch(request, *args, **kwargs)
 
-    def get_payment_requests_queryset(self):
-        return self.object.payment_requests.filter(status__in=[SUBMITTED, CHANGES_REQUESTED])
+
+class ProjectDetailView(ViewDispatcher):
+    admin_view = AdminProjectDetailView
+    applicant_view = ApplicantProjectDetailView
 
 
 @method_decorator(login_required, name='dispatch')
@@ -628,10 +436,7 @@ class ProjectPrivateMediaView(UserPassesTestMixin, PrivateMediaView):
         return False
 
 
-class ProjectDetailView(ViewDispatcher):
-    admin_view = AdminProjectDetailView
-    applicant_view = ApplicantProjectDetailView
-
+# PROJECT EDIT
 
 @method_decorator(staff_required, name='dispatch')
 class ProjectDetailSimplifiedView(DetailView):
diff --git a/opentech/apply/users/decorators.py b/opentech/apply/users/decorators.py
index 26d4016f57356e56ce077dfa4c40d012ee2ccef1..b41a4aaee89d268e53e97700d2fb91b8d897377c 100644
--- a/opentech/apply/users/decorators.py
+++ b/opentech/apply/users/decorators.py
@@ -19,8 +19,16 @@ def is_apply_staff(user):
     return True
 
 
+def is_approver(user):
+    if not user.is_approver:
+        raise PermissionDenied
+    return True
+
+
 staff_required = [login_required, user_passes_test(is_apply_staff)]
 
+approver_required = [login_required, user_passes_test(is_approver)]
+
 
 def superuser_decorator(fn):
     check = user_passes_test(lambda user: user.is_superuser)
diff --git a/opentech/apply/users/models.py b/opentech/apply/users/models.py
index 768b583c958865dcd2851c39fa5ffeb686d3845b..1c60d84be3766d124aa41c2a7616f04ba16a8a1a 100644
--- a/opentech/apply/users/models.py
+++ b/opentech/apply/users/models.py
@@ -2,7 +2,6 @@ from django.contrib.auth.hashers import make_password
 from django.contrib.auth.models import AbstractUser, BaseUserManager, Group
 from django.db import models
 from django.db.models import Q
-from django.urls import reverse
 from django.utils.functional import cached_property
 from django.utils.translation import gettext_lazy as _
 
@@ -102,9 +101,6 @@ class User(AbstractUser):
 
     objects = UserManager()
 
-    def get_absolute_url(self):
-        return reverse('wagtailusers_users:edit', args=(self.id,))
-
     def __str__(self):
         return self.get_full_name() if self.get_full_name() else self.get_short_name()
 
diff --git a/opentech/apply/users/wagtail_hooks.py b/opentech/apply/users/wagtail_hooks.py
index dcd5276f5f356dea951f5dfba794fb96dda19954..5e865f25945de71a80959830c5c56a52e16cd998 100644
--- a/opentech/apply/users/wagtail_hooks.py
+++ b/opentech/apply/users/wagtail_hooks.py
@@ -1,4 +1,5 @@
 from django.conf.urls import url
+from django.urls import reverse
 
 from wagtail.core import hooks
 
@@ -20,6 +21,7 @@ def notify_after_create_user(request, user):
         message=f'{request.user} has crated a new account for {user}.',
         request=request,
         related=user,
+        path=reverse('wagtailusers_users:edit', args=(user.id,))
     )
 
 
@@ -32,4 +34,5 @@ def notify_after_edit_user(request, user):
             message=f'{request.user} has edited the account for {user} that now have these roles: {roles}.',
             request=request,
             related=user,
+            path=reverse('wagtailusers_users:edit', args=(user.id,))
         )
diff --git a/opentech/apply/utils/notifications.py b/opentech/apply/utils/notifications.py
index bed3683da90108043926563a59f6c05e7d8f3178..037b23a32bec317028ad8baf077e2ad85e4f7c92 100644
--- a/opentech/apply/utils/notifications.py
+++ b/opentech/apply/utils/notifications.py
@@ -19,16 +19,15 @@ class SlackNotifications():
                 slack_users.append(f'<{user.slack}>')
         return ' '.join(slack_users)
 
-    def slack_link(self, request, related):
-        slack_link = ''
+    def slack_link(self, request, related, path=None, **kwargs):
         try:
-            link = request.scheme + '://' + request.get_host() + related.get_absolute_url()
+            url = path or related.get_absolute_url()
         except AttributeError:
-            pass
-        else:
-            title = str(related)
-            slack_link = f'<{link}|{title}>'
-        return slack_link
+            return ''
+
+        link = request.scheme + '://' + request.get_host() + url
+        title = str(related)
+        return f'<{link}|{title}>'
 
     def send_message(self, message, request, recipients=None, related=None, **kwargs):
         if not self.destination or not self.target_room:
@@ -41,7 +40,7 @@ class SlackNotifications():
 
         slack_users = self.slack_users(recipients) if recipients else ''
 
-        slack_link = self.slack_link(request, related) if related else ''
+        slack_link = self.slack_link(request, related, **kwargs) if related else ''
 
         message = ' '.join([slack_users, message, slack_link]).strip()
 
diff --git a/opentech/apply/utils/templatetags/__init__.py b/opentech/apply/utils/templatetags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/utils/templatetags/apply_tags.py b/opentech/apply/utils/templatetags/apply_tags.py
new file mode 100644
index 0000000000000000000000000000000000000000..c010e4d6cefb172eecdce993a6aff3e292da214b
--- /dev/null
+++ b/opentech/apply/utils/templatetags/apply_tags.py
@@ -0,0 +1,9 @@
+from django import template
+
+register = template.Library()
+
+
+# Get the verbose name of a model instance
+@register.filter
+def model_verbose_name(instance):
+    return instance._meta.verbose_name.title()
diff --git a/opentech/apply/utils/testing/tests.py b/opentech/apply/utils/testing/tests.py
index 2940aceb26b441b25726c68255964da9d9f1db54..b52c1d389828854a3fcb0aff8d0ef2d5891037b6 100644
--- a/opentech/apply/utils/testing/tests.py
+++ b/opentech/apply/utils/testing/tests.py
@@ -36,7 +36,7 @@ class BaseViewTestCase(TestCase):
     def get_kwargs(self, instance):
         return {}
 
-    def url(self, instance, view_name=None, absolute=True, kwargs=dict()):
+    def url(self, instance, view_name=None, absolute=True, url_kwargs=None):
         view = view_name or self.base_view_name
         full_url_name = self.url_name.format(view)
         kwargs_method = f'get_{view}_kwargs'
@@ -44,6 +44,8 @@ class BaseViewTestCase(TestCase):
             kwargs = getattr(self, kwargs_method)(instance)
         else:
             kwargs = self.get_kwargs(instance)
+        if url_kwargs:
+            kwargs.update(url_kwargs)
         return self.url_from_pattern(full_url_name, kwargs, secure=True, absolute=absolute)
 
     def absolute_url(self, location, secure=True):
@@ -57,11 +59,11 @@ class BaseViewTestCase(TestCase):
         request = self.factory.get(url, secure=secure)
         return request.path
 
-    def get_page(self, instance=None, view_name=None):
-        return self.client.get(self.url(instance, view_name), secure=True, follow=True)
+    def get_page(self, instance=None, view_name=None, url_kwargs=None):
+        return self.client.get(self.url(instance, view_name, url_kwargs=url_kwargs), secure=True, follow=True)
 
-    def post_page(self, instance=None, data=dict(), view_name=None):
-        return self.client.post(self.url(instance, view_name), data, secure=True, follow=True)
+    def post_page(self, instance=None, data=dict(), view_name=None, url_kwargs=None):
+        return self.client.post(self.url(instance, view_name, url_kwargs=url_kwargs), data, secure=True, follow=True)
 
     def refresh(self, instance):
         return instance.__class__.objects.get(id=instance.id)
diff --git a/opentech/apply/utils/views.py b/opentech/apply/utils/views.py
index 6ea0fb1673f27e375ac389147740534bf6a3ed73..17f5faa3dfc4bf260bb6df097f6d8e12eaf943c3 100644
--- a/opentech/apply/utils/views.py
+++ b/opentech/apply/utils/views.py
@@ -7,6 +7,7 @@ from django.views.generic import View
 from django.views.generic.base import ContextMixin
 from django.views.generic.detail import SingleObjectTemplateResponseMixin
 from django.views.generic.edit import ModelFormMixin, ProcessFormView
+from django.shortcuts import redirect
 
 
 def page_not_found(request, exception=None, template_name='apply/404.html'):
@@ -60,12 +61,22 @@ class DelegatableBase(ContextMixin):
     """
     form_prefix = 'form-submitted-'
 
+    def __init__(self, *args, **kwargs):
+        self._form_views = {
+            self.form_prefix + form_view.context_name: form_view
+            for form_view in self.form_views
+        }
+
     def get_form_kwargs(self):
         return {}
 
     def get_context_data(self, **kwargs):
-        form_kwargs = self.get_form_kwargs()
-        forms = dict(form_view.contribute_form(**form_kwargs) for form_view in self.form_views)
+        forms = {}
+        for form_view in self._form_views.values():
+            view = form_view()
+            view.setup(self.request, self.args, self.kwargs)
+            context_key, form = view.contribute_form(self)
+            forms[context_key] = form
 
         return super().get_context_data(
             form_prefix=self.form_prefix,
@@ -78,12 +89,12 @@ class DelegatableBase(ContextMixin):
         kwargs['context'] = self.get_context_data()
         kwargs['template_names'] = self.get_template_names()
 
-        for form_view in self.form_views:
-            if self.form_prefix + form_view.context_name in request.POST:
-                return form_view.as_view()(request, *args, **kwargs)
+        for form_key, form_view in self._form_views.items():
+            if form_key in request.POST:
+                return form_view.as_view()(request, *args, parent=self, **kwargs)
 
         # Fall back to get if not form exists as submitted
-        return self.get(request, *args, **kwargs)
+        return redirect(request.path)
 
 
 class DelegateableView(DelegatableBase):
@@ -115,17 +126,46 @@ class DelegateableListView(DelegatableBase):
 class DelegatedViewMixin(View):
     """For use on create views accepting forms from another view"""
 
+    # TODO: REMOVE IN DJANGO 2.2
+    def setup(self, request, *args, **kwargs):
+        """Initialize attributes shared by all view methods."""
+        self.request = request
+        self.args = args
+        self.kwargs = kwargs
+
+    def get_object(self):
+        # We want to make sure we share the same instance between the form
+        # and the view where appropriate
+        parent_object = self.get_parent_kwargs()['instance']
+        if isinstance(parent_object, self.model):
+            return parent_object
+
+        return super().get_object()
+
     def get_template_names(self):
         return self.kwargs['template_names']
 
+    def get_form_name(self):
+        return self.context_name
+
     def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
+        form_kwargs = super().get_form_kwargs()
+        form_kwargs['user'] = self.request.user
+        form_kwargs.update(**self.get_parent_kwargs())
+        return form_kwargs
+
+    def get_parent_kwargs(self):
+        try:
+            return self.parent.get_form_kwargs()
+        except AttributeError:
+            return self.kwargs['parent'].get_form_kwargs()
+
+    def get_parent_object(self):
+        return self.get_parent_kwargs()['instance']
 
     def get_form(self, *args, **kwargs):
         form = super().get_form(*args, **kwargs)
-        form.name = self.context_name
+        form.name = self.get_form_name()
         return form
 
     def get_context_data(self, **kwargs):
@@ -139,11 +179,10 @@ class DelegatedViewMixin(View):
     def is_model_form(cls):
         return issubclass(cls.form_class, ModelForm)
 
-    @classmethod
-    def contribute_form(cls, **kwargs):
-        form = cls.form_class(**kwargs)
-        form.name = cls.context_name
-        return cls.context_name, form
+    def contribute_form(self, parent):
+        self.parent = parent
+        form = self.get_form()
+        return self.context_name, form
 
     def get_success_url(self):
         query = self.request.GET.urlencode()
diff --git a/opentech/static_src/src/sass/apply/components/_docs-block.scss b/opentech/static_src/src/sass/apply/components/_docs-block.scss
index b48aac80abc5d9f22920a2e43ed15de4c2273248..eae2ff83ac07d6460de6f5e709f1ba4fb66c92c7 100644
--- a/opentech/static_src/src/sass/apply/components/_docs-block.scss
+++ b/opentech/static_src/src/sass/apply/components/_docs-block.scss
@@ -95,6 +95,12 @@
         font-weight: $weight--bold;
         margin-right: 1rem;
 
+        &:disabled,
+        &.is-disabled {
+            color: $color--mid-grey;
+            cursor: default;
+        }
+
         &:last-child {
             margin-right: 0;
         }
diff --git a/opentech/static_src/src/sass/apply/components/_payment-block.scss b/opentech/static_src/src/sass/apply/components/_payment-block.scss
index 32dc22e9279fc16079ed394b786c522986aab453..97e4d726c5881a25ef0cfa8974d2821ad240fd24 100644
--- a/opentech/static_src/src/sass/apply/components/_payment-block.scss
+++ b/opentech/static_src/src/sass/apply/components/_payment-block.scss
@@ -139,12 +139,12 @@
 
     &__table-status {
         min-width: 160px;
-        width: 30%;
+        width: 25%;
     }
 
-    &__table-docs {
+    &__table-date{
         min-width: 180px;
-        width: 20%;
+        width: 25%;
     }
 
     &__table-update {