diff --git a/hypha/apply/activity/messaging.py b/hypha/apply/activity/messaging.py
index de1489f154a3fa4699ff5d8fbabd48c074649f24..0593f09feff319e9e50962142d13c34722a7943b 100644
--- a/hypha/apply/activity/messaging.py
+++ b/hypha/apply/activity/messaging.py
@@ -707,6 +707,8 @@ class EmailAdapter(AdapterBase):
         MESSAGES.PARTNERS_UPDATED: 'partners_updated_applicant',
         MESSAGES.PARTNERS_UPDATED_PARTNER: 'partners_updated_partner',
         MESSAGES.UPLOAD_CONTRACT: 'messages/email/contract_uploaded.html',
+        MESSAGES.CREATED_PROJECT: 'handle_project_created',
+        MESSAGES.UPDATED_VENDOR: 'handle_vendor_updated',
         MESSAGES.SENT_TO_COMPLIANCE: 'messages/email/sent_to_compliance.html',
         MESSAGES.UPDATE_PAYMENT_REQUEST: 'messages/email/payment_request_updated.html',
         MESSAGES.UPDATE_PAYMENT_REQUEST_STATUS: 'handle_payment_status_updated',
@@ -767,6 +769,24 @@ class EmailAdapter(AdapterBase):
             **kwargs,
         )
 
+    def handle_project_created(self, source, **kwargs):
+        from hypha.apply.projects.models import ProjectSettings
+        request = kwargs.get('request')
+        project_settings = ProjectSettings.for_request(request)
+        if project_settings.vendor_setup_required:
+            return self.render_message(
+                'messages/email/vendor_setup_needed.html',
+                source=source,
+                **kwargs
+            )
+
+    def handle_vendor_updated(self, source, **kwargs):
+        return self.render_message(
+            'messages/email/vendor_updated.html',
+            source=source,
+            **kwargs,
+        )
+
     def handle_determination(self, determination, source, **kwargs):
         submission = source
         if determination.send_notice:
diff --git a/hypha/apply/activity/migrations/0056_add_updated_vendor.py b/hypha/apply/activity/migrations/0056_add_updated_vendor.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3c4416972b6f1a90e628f02d44b3b9fb6b5741a
--- /dev/null
+++ b/hypha/apply/activity/migrations/0056_add_updated_vendor.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.24 on 2021-07-06 08:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0055_add_batch_delete_submission'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='type',
+            field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('BATCH_UPDATE_LEAD', 'Batch Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('BATCH_TRANSITION', 'Batch Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('BATCH_DETERMINATION_OUTCOME', 'Batch Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('PARTNERS_UPDATED', 'Partners Updated'), ('PARTNERS_UPDATED_PARTNER', 'Partners Updated Partner'), ('READY_FOR_REVIEW', 'Ready For Review'), ('BATCH_READY_FOR_REVIEW', 'Batch Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission'), ('REVIEW_OPINION', 'Review Opinion'), ('DELETE_SUBMISSION', 'Delete Submission'), ('DELETE_REVIEW', 'Delete Review'), ('CREATED_PROJECT', 'Created Project'), ('UPDATED_VENDOR', 'Updated Vendor'), ('UPDATE_PROJECT_LEAD', 'Update Project Lead'), ('EDIT_REVIEW', 'Edit Review'), ('SEND_FOR_APPROVAL', 'Send for Approval'), ('APPROVE_PROJECT', 'Project was Approved'), ('PROJECT_TRANSITION', 'Project was Transitioned'), ('REQUEST_PROJECT_CHANGE', 'Project change requested'), ('UPLOAD_DOCUMENT', 'Document was Uploaded to Project'), ('REMOVE_DOCUMENT', 'Document was Removed from Project'), ('UPLOAD_CONTRACT', 'Contract was Uploaded to Project'), ('APPROVE_CONTRACT', 'Contract was Approved'), ('REQUEST_PAYMENT', 'Payment was requested for Project'), ('UPDATE_PAYMENT_REQUEST_STATUS', 'Updated Payment Request Status'), ('DELETE_PAYMENT_REQUEST', 'Delete Payment Request'), ('SENT_TO_COMPLIANCE', 'Project was sent to Compliance'), ('UPDATE_PAYMENT_REQUEST', 'Updated Payment Request'), ('SUBMIT_REPORT', 'Submit Report'), ('SKIPPED_REPORT', 'Skipped Report'), ('REPORT_FREQUENCY_CHANGED', 'Report Frequency Changed'), ('REPORT_NOTIFY', 'Report Notify'), ('CREATE_REMINDER', 'Reminder Created'), ('DELETE_REMINDER', 'Reminder Deleted'), ('REVIEW_REMINDER', 'Reminde to Review'), ('BATCH_DELETE_SUBMISSION', 'Delete Batch Submissions')], max_length=50),
+        ),
+    ]
diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py
index fb5d7fb04ebc3acd53bde249a1c03ff90fdc2150..75cf974f80b3715fb899ec07df90c4dda7f00b7f 100644
--- a/hypha/apply/activity/options.py
+++ b/hypha/apply/activity/options.py
@@ -27,6 +27,7 @@ class MESSAGES(Enum):
     DELETE_SUBMISSION = 'Delete Submission'
     DELETE_REVIEW = 'Delete Review'
     CREATED_PROJECT = 'Created Project'
+    UPDATED_VENDOR = 'Updated Vendor'
     UPDATE_PROJECT_LEAD = 'Update Project Lead'
     EDIT_REVIEW = 'Edit Review'
     SEND_FOR_APPROVAL = 'Send for Approval'
diff --git a/hypha/apply/activity/templates/messages/email/vendor_setup_needed.html b/hypha/apply/activity/templates/messages/email/vendor_setup_needed.html
new file mode 100644
index 0000000000000000000000000000000000000000..cbfa10606ec0d860d9d85597b9c1f07bbdbf5c45
--- /dev/null
+++ b/hypha/apply/activity/templates/messages/email/vendor_setup_needed.html
@@ -0,0 +1,11 @@
+{% extends "messages/email/applicant_base.html" %}
+
+{% block content %}
+A Project has been created for your submission on {{ ORG_SHORT_NAME }}.
+
+Next step is to complete vendor setup form by visiting project detail page.
+
+Project: {{ source.title }}
+Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
+Submission: {{ request.scheme }}://{{ request.get_host }}{% url 'apply:submissions:simplified' pk=source.submission.pk %}
+{% endblock %}
diff --git a/hypha/apply/activity/templates/messages/email/vendor_updated.html b/hypha/apply/activity/templates/messages/email/vendor_updated.html
new file mode 100644
index 0000000000000000000000000000000000000000..ae3d1cdce50c0fe685d2412b25c4410025dc3735
--- /dev/null
+++ b/hypha/apply/activity/templates/messages/email/vendor_updated.html
@@ -0,0 +1,10 @@
+{% extends "messages/email/base.html" %}
+
+{% block content %}
+
+Vendor Information has been updated on {{ source.title }}.
+
+Project: {{ source.title }}
+Link: {{ request.scheme }}://{{ request.get_host }}{{ source.get_absolute_url }}
+
+{% endblock %}
diff --git a/hypha/apply/categories/blocks.py b/hypha/apply/categories/blocks.py
index 11a126770332d3c91317ba94a74775e0f3b40ba1..9bd9ef2974534fb3eac44a8a361291129b0dc8b4 100644
--- a/hypha/apply/categories/blocks.py
+++ b/hypha/apply/categories/blocks.py
@@ -78,11 +78,12 @@ class CategoryQuestionBlock(OptionalFormFieldBlock):
             return forms.RadioSelect
 
     def prepare_data(self, value, data, serialize):
+        if not data:
+            return data
         if isinstance(data, str):
             data = [data]
         category = value['category']
-        if data:
-            data = category.options.filter(id__in=data).values_list('value', flat=True)
+        data = category.options.filter(id__in=data).values_list('value', flat=True)
         return data
 
     def get_searchable_content(self, value, data):
diff --git a/hypha/apply/projects/forms.py b/hypha/apply/projects/forms.py
deleted file mode 100644
index c73f213c93dd22e76cef040ddadba1d0ee081d69..0000000000000000000000000000000000000000
--- a/hypha/apply/projects/forms.py
+++ /dev/null
@@ -1,510 +0,0 @@
-import functools
-
-from django import forms
-from django.conf import settings
-from django.contrib.auth import get_user_model
-from django.core.exceptions import ValidationError
-from django.core.files.base import ContentFile
-from django.db import transaction
-from django.db.models import Q
-from django.utils import timezone
-from django.utils.translation import gettext_lazy as _
-from django_file_form.forms import FileFormMixin
-
-from addressfield.fields import AddressField
-from hypha.apply.funds.models import ApplicationSubmission
-from hypha.apply.stream_forms.fields import MultiFileField
-from hypha.apply.users.groups import STAFF_GROUP_NAME
-from hypha.apply.utils.fields import RichTextField
-
-from .models.payment import (
-    CHANGES_REQUESTED,
-    DECLINED,
-    PAID,
-    REQUEST_STATUS_CHOICES,
-    SUBMITTED,
-    UNDER_REVIEW,
-    PaymentReceipt,
-    PaymentRequest,
-)
-from .models.project import COMMITTED, Approval, Contract, PacketFile, Project
-from .models.report import Report, ReportConfig, ReportPrivateFiles, ReportVersion
-
-User = get_user_model()
-
-
-def filter_choices(available, choices):
-    return [(k, v) for k, v in available if k in choices]
-
-
-filter_request_choices = functools.partial(filter_choices, REQUEST_STATUS_CHOICES)
-
-
-class ApproveContractForm(forms.Form):
-    id = forms.IntegerField(widget=forms.HiddenInput())
-
-    def __init__(self, instance, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.instance = instance
-        if instance:
-            self.fields['id'].initial = instance.id
-
-    def clean_id(self):
-        if self.has_changed():
-            raise forms.ValidationError(_('Something changed before your approval please re-review'))
-
-    def clean(self):
-        if not self.instance:
-            raise forms.ValidationError(_('The contract you were trying to approve has already been approved'))
-
-        if not self.instance.is_signed:
-            raise forms.ValidationError(_('You can only approve a signed contract'))
-
-        super().clean()
-
-    def save(self, *args, **kwargs):
-        self.instance.save()
-        return self.instance
-
-
-class ChangePaymentRequestStatusForm(forms.ModelForm):
-    name_prefix = 'change_payment_request_status_form'
-
-    class Meta:
-        fields = ['status', 'comment', 'paid_value']
-        model = PaymentRequest
-
-    def __init__(self, instance, *args, **kwargs):
-        super().__init__(instance=instance, *args, **kwargs)
-
-        self.initial['paid_value'] = self.instance.requested_value
-
-        status_field = self.fields['status']
-
-        possible_status_transitions_lut = {
-            CHANGES_REQUESTED: filter_request_choices([DECLINED]),
-            SUBMITTED: filter_request_choices([CHANGES_REQUESTED, UNDER_REVIEW, DECLINED]),
-            UNDER_REVIEW: filter_request_choices([PAID]),
-        }
-        status_field.choices = possible_status_transitions_lut.get(instance.status, [])
-
-        if instance.status != UNDER_REVIEW:
-            del self.fields['paid_value']
-
-    def clean(self):
-        cleaned_data = super().clean()
-        status = cleaned_data['status']
-        paid_value = cleaned_data.get('paid_value')
-
-        if paid_value and status != PAID:
-            self.add_error('paid_value', _('You can only set a value when moving to the Paid status.'))
-        return cleaned_data
-
-
-class CreateProjectForm(forms.Form):
-    submission = forms.ModelChoiceField(
-        queryset=ApplicationSubmission.objects.filter(project__isnull=True),
-        widget=forms.HiddenInput(),
-    )
-
-    def __init__(self, instance=None, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        if instance:
-            self.fields['submission'].initial = instance.id
-
-    def save(self, *args, **kwargs):
-        submission = self.cleaned_data['submission']
-        return Project.create_from_submission(submission)
-
-
-class CreateApprovalForm(forms.ModelForm):
-    by = forms.ModelChoiceField(
-        queryset=User.objects.approvers(),
-        widget=forms.HiddenInput(),
-    )
-
-    class Meta:
-        model = Approval
-        fields = ('by',)
-
-    def __init__(self, user=None, *args, **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 ProjectEditForm(forms.ModelForm):
-    contact_address = AddressField()
-
-    class Meta:
-        fields = [
-            'title',
-            'contact_legal_name',
-            'contact_email',
-            'contact_address',
-            'contact_phone',
-            'value',
-            'proposed_start',
-            'proposed_end',
-        ]
-        model = Project
-        widgets = {
-            'title': forms.TextInput,
-            'contact_legal_name': forms.TextInput,
-            'contact_email': forms.TextInput,
-            'contact_phone': forms.TextInput,
-            'proposed_end': forms.DateInput,
-            'proposed_start': forms.DateInput,
-        }
-
-
-class ProjectApprovalForm(ProjectEditForm):
-    def __init__(self, *args, extra_fields=None, **kwargs):
-        super().__init__(*args, **kwargs)
-        if extra_fields:
-            self.fields = {
-                **self.fields,
-                **extra_fields,
-            }
-
-    def save(self, *args, **kwargs):
-        self.instance.form_data = {
-            field: self.cleaned_data[field]
-            for field in self.instance.question_field_ids
-            if field in self.cleaned_data
-        }
-        self.instance.user_has_updated_details = True
-        return super().save(*args, **kwargs)
-
-
-class RejectionForm(forms.Form):
-    comment = forms.CharField(widget=forms.Textarea)
-
-    def __init__(self, instance=None, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-
-class RemoveDocumentForm(forms.ModelForm):
-    id = forms.IntegerField(widget=forms.HiddenInput())
-
-    class Meta:
-        fields = ['id']
-        model = PacketFile
-
-    def __init__(self, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-
-class PaymentRequestBaseForm(forms.ModelForm):
-    class Meta:
-        fields = ['requested_value', 'invoice', 'date_from', 'date_to']
-        model = PaymentRequest
-        widgets = {
-            'date_from': forms.DateInput,
-            'date_to': forms.DateInput,
-        }
-        labels = {
-            'requested_value': _('Requested Value ({currency})').format(currency=settings.CURRENCY_SYMBOL.strip())
-        }
-
-    def __init__(self, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.fields['requested_value'].widget.attrs['min'] = 0
-
-    def clean(self):
-        cleaned_data = super().clean()
-        date_from = cleaned_data['date_from']
-        date_to = cleaned_data['date_to']
-
-        if date_from > date_to:
-            self.add_error('date_from', _('Date From must be before Date To'))
-
-        return cleaned_data
-
-
-class CreatePaymentRequestForm(FileFormMixin, PaymentRequestBaseForm):
-    receipts = MultiFileField(required=False)
-
-    def save(self, commit=True):
-        request = super().save(commit=commit)
-
-        receipts = self.cleaned_data['receipts'] or []
-
-        PaymentReceipt.objects.bulk_create(
-            PaymentReceipt(payment_request=request, file=receipt)
-            for receipt in receipts
-        )
-
-        return request
-
-
-class EditPaymentRequestForm(FileFormMixin, PaymentRequestBaseForm):
-    receipt_list = forms.ModelMultipleChoiceField(
-        widget=forms.CheckboxSelectMultiple(attrs={'class': 'delete'}),
-        queryset=PaymentReceipt.objects.all(),
-        required=False,
-        label=_('Receipts')
-    )
-    receipts = MultiFileField(label='', required=False)
-
-    def __init__(self, user=None, instance=None, *args, **kwargs):
-        super().__init__(*args, instance=instance, **kwargs)
-
-        self.fields['receipt_list'].queryset = instance.receipts.all()
-
-        self.fields['requested_value'].label = 'Value'
-
-    @transaction.atomic
-    def save(self, commit=True):
-        request = super().save(commit=commit)
-
-        removed_receipts = self.cleaned_data['receipt_list']
-
-        removed_receipts.delete()
-
-        to_add = self.cleaned_data['receipts']
-        if to_add:
-            PaymentReceipt.objects.bulk_create(
-                PaymentReceipt(payment_request=request, file=receipt)
-                for receipt in to_add
-            )
-        return request
-
-
-class SelectDocumentForm(forms.ModelForm):
-    document = forms.ChoiceField(
-        label=_('Document'),
-        widget=forms.Select(attrs={'id': 'from_submission'})
-    )
-
-    class Meta:
-        model = PacketFile
-        fields = ['category', 'document']
-
-    def __init__(self, existing_files, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        self.files = existing_files
-
-        choices = [(f.url, f.filename) for f in self.files]
-
-        self.fields['document'].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):
-        return super().save(*args, **kwargs)
-
-
-class SetPendingForm(forms.ModelForm):
-    class Meta:
-        fields = ['id']
-        model = Project
-        widgets = {'id': forms.HiddenInput()}
-
-    def __init__(self, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-    def clean(self):
-        if self.instance.status != COMMITTED:
-            raise forms.ValidationError(_('A Project can only be sent for Approval when Committed.'))
-
-        if self.instance.is_locked:
-            raise forms.ValidationError(_('A Project can only be sent for Approval once'))
-
-        super().clean()
-
-    def save(self, *args, **kwargs):
-        self.instance.is_locked = True
-        return super().save(*args, **kwargs)
-
-
-class UploadContractForm(forms.ModelForm):
-    class Meta:
-        fields = ['file']
-        model = Contract
-
-
-class StaffUploadContractForm(forms.ModelForm):
-    class Meta:
-        fields = ['file', 'is_signed']
-        model = Contract
-
-
-class UploadDocumentForm(forms.ModelForm):
-    class Meta:
-        fields = ['title', 'category', 'document']
-        model = PacketFile
-        widgets = {'title': forms.TextInput()}
-        labels = {
-            "title": "File Name",
-        }
-
-    def __init__(self, user=None, instance=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-
-class UpdateProjectLeadForm(forms.ModelForm):
-    class Meta:
-        fields = ['lead']
-        model = Project
-
-    def __init__(self, user=None, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        lead_field = self.fields['lead']
-        lead_field.label = _('Update lead from {lead} to').format(lead=self.instance.lead)
-
-        qwargs = Q(groups__name=STAFF_GROUP_NAME) | Q(is_superuser=True)
-        lead_field.queryset = (lead_field.queryset.exclude(pk=self.instance.lead_id)
-                                                  .filter(qwargs)
-                                                  .distinct())
-
-
-class ReportEditForm(FileFormMixin, forms.ModelForm):
-    public_content = RichTextField(
-        help_text=_('This section of the report will be shared with the broader community.')
-    )
-    private_content = RichTextField(
-        help_text=_('This section of the report will be shared with staff only.')
-    )
-    file_list = forms.ModelMultipleChoiceField(
-        widget=forms.CheckboxSelectMultiple(attrs={'class': 'delete'}),
-        queryset=ReportPrivateFiles.objects.all(),
-        required=False,
-        label=_('Files')
-    )
-    files = MultiFileField(required=False, label='')
-
-    class Meta:
-        model = Report
-        fields = (
-            'public_content',
-            'private_content',
-            'file_list',
-            'files',
-        )
-
-    def __init__(self, *args, user=None, initial={}, **kwargs):
-        self.report_files = initial.pop(
-            'file_list',
-            ReportPrivateFiles.objects.none(),
-        )
-        super().__init__(*args, initial=initial, **kwargs)
-        self.fields['file_list'].queryset = self.report_files
-        self.user = user
-
-    def clean(self):
-        cleaned_data = super().clean()
-        public = cleaned_data['public_content']
-        private = cleaned_data['private_content']
-        if not private and not public:
-            missing_content = _('Must include either public or private content when submitting a report.')
-            self.add_error('public_content', missing_content)
-            self.add_error('private_content', missing_content)
-        return cleaned_data
-
-    @transaction.atomic
-    def save(self, commit=True):
-        is_draft = 'save' in self.data
-
-        version = ReportVersion.objects.create(
-            report=self.instance,
-            public_content=self.cleaned_data['public_content'],
-            private_content=self.cleaned_data['private_content'],
-            submitted=timezone.now(),
-            draft=is_draft,
-            author=self.user,
-        )
-
-        if is_draft:
-            self.instance.draft = version
-        else:
-            # If this is the first submission of the report we track that as the
-            # submitted date of the report
-            if not self.instance.submitted:
-                self.instance.submitted = version.submitted
-            self.instance.current = version
-            self.instance.draft = None
-
-        instance = super().save(commit)
-
-        removed_files = self.cleaned_data['file_list']
-        ReportPrivateFiles.objects.bulk_create(
-            ReportPrivateFiles(report=version, document=file.document)
-            for file in self.report_files
-            if file not in removed_files
-        )
-
-        added_files = self.cleaned_data['files']
-        if added_files:
-            ReportPrivateFiles.objects.bulk_create(
-                ReportPrivateFiles(report=version, document=file)
-                for file in added_files
-            )
-
-        return instance
-
-
-class ReportFrequencyForm(forms.ModelForm):
-    start = forms.DateField(label=_('Starting on:'))
-
-    class Meta:
-        model = ReportConfig
-        fields = ('occurrence', 'frequency', 'start')
-        labels = {
-            'occurrence': 'No.',
-        }
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        today = timezone.now().date()
-        last_report = self.instance.last_report()
-
-        self.fields['start'].widget.attrs.update({
-            'min': max(
-                last_report.end_date if last_report else today,
-                today,
-            ),
-            'max': self.instance.project.end_date,
-        })
-
-    def clean_start(self):
-        start_date = self.cleaned_data['start']
-        last_report = self.instance.last_report()
-        if last_report and start_date <= last_report.end_date:
-            raise ValidationError(
-                _("Cannot start a schedule before the current reporting period"),
-                code="bad_start"
-            )
-
-        if start_date < timezone.now().date():
-            raise ValidationError(
-                _("Cannot start a schedule in the past"),
-                code="bad_start"
-            )
-
-        if start_date > self.instance.project.end_date:
-            raise ValidationError(
-                _("Cannot start a schedule beyond the end date"),
-                code="bad_start"
-            )
-        return start_date
-
-    def save(self, *args, **kwargs):
-        self.instance.schedule_start = self.cleaned_data['start']
-        return super().save(*args, **kwargs)
diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..29c59a1b239b0c83a03cddcf47b68c74c8b3b858
--- /dev/null
+++ b/hypha/apply/projects/forms/__init__.py
@@ -0,0 +1,56 @@
+from .payment import (
+    ChangePaymentRequestStatusForm,
+    CreatePaymentRequestForm,
+    EditPaymentRequestForm,
+    SelectDocumentForm,
+)
+from .project import (
+    ApproveContractForm,
+    CreateApprovalForm,
+    CreateProjectForm,
+    ProjectApprovalForm,
+    ProjectEditForm,
+    RejectionForm,
+    RemoveDocumentForm,
+    SetPendingForm,
+    StaffUploadContractForm,
+    UpdateProjectLeadForm,
+    UploadContractForm,
+    UploadDocumentForm,
+)
+from .report import ReportEditForm, ReportFrequencyForm
+from .vendor import (
+    CreateVendorFormStep1,
+    CreateVendorFormStep2,
+    CreateVendorFormStep3,
+    CreateVendorFormStep4,
+    CreateVendorFormStep5,
+    CreateVendorFormStep6,
+)
+
+__all__ = [
+    'ChangePaymentRequestStatusForm',
+    'CreatePaymentRequestForm',
+    'EditPaymentRequestForm',
+    'SelectDocumentForm',
+    'ApproveContractForm',
+    'CreateProjectForm',
+    'CreateApprovalForm',
+    'ProjectEditForm',
+    'ProjectApprovalForm',
+    'RejectionForm',
+    'RemoveDocumentForm',
+    'SetPendingForm',
+    'UploadContractForm',
+    'StaffUploadContractForm',
+    'UploadDocumentForm',
+    'UpdateProjectLeadForm',
+    'ReportEditForm',
+    'ReportFrequencyForm',
+    'CreateVendorFormStep1',
+    'CreateVendorFormStep2',
+    'CreateVendorFormStep3',
+    'CreateVendorFormStep4',
+    'CreateVendorFormStep5',
+    'CreateVendorFormStep6',
+]
diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py
new file mode 100644
index 0000000000000000000000000000000000000000..4af94f103beb80bbf2cc44e0bf3b4ce4fe18a79e
--- /dev/null
+++ b/hypha/apply/projects/forms/payment.py
@@ -0,0 +1,170 @@
+import functools
+
+from django import forms
+from django.core.files.base import ContentFile
+from django.db import transaction
+from django_file_form.forms import FileFormMixin
+
+from hypha.apply.stream_forms.fields import MultiFileField
+
+from ..models.payment import (
+    CHANGES_REQUESTED,
+    DECLINED,
+    PAID,
+    REQUEST_STATUS_CHOICES,
+    SUBMITTED,
+    UNDER_REVIEW,
+    PaymentReceipt,
+    PaymentRequest,
+)
+from ..models.project import PacketFile
+
+
+def filter_choices(available, choices):
+    return [(k, v) for k, v in available if k in choices]
+
+
+filter_request_choices = functools.partial(filter_choices, REQUEST_STATUS_CHOICES)
+
+
+class ChangePaymentRequestStatusForm(forms.ModelForm):
+    name_prefix = 'change_payment_request_status_form'
+
+    class Meta:
+        fields = ['status', 'comment', 'paid_value']
+        model = PaymentRequest
+
+    def __init__(self, instance, *args, **kwargs):
+        super().__init__(instance=instance, *args, **kwargs)
+
+        self.initial['paid_value'] = self.instance.requested_value
+
+        status_field = self.fields['status']
+
+        possible_status_transitions_lut = {
+            CHANGES_REQUESTED: filter_request_choices([DECLINED]),
+            SUBMITTED: filter_request_choices([CHANGES_REQUESTED, UNDER_REVIEW, DECLINED]),
+            UNDER_REVIEW: filter_request_choices([PAID]),
+        }
+        status_field.choices = possible_status_transitions_lut.get(instance.status, [])
+
+        if instance.status != UNDER_REVIEW:
+            del self.fields['paid_value']
+
+    def clean(self):
+        cleaned_data = super().clean()
+        status = cleaned_data['status']
+        paid_value = cleaned_data.get('paid_value')
+
+        if paid_value and status != PAID:
+            self.add_error('paid_value', 'You can only set a value when moving to the Paid status.')
+        return cleaned_data
+
+
+class PaymentRequestBaseForm(forms.ModelForm):
+    class Meta:
+        fields = ['requested_value', 'invoice', 'date_from', 'date_to']
+        model = PaymentRequest
+        widgets = {
+            'date_from': forms.DateInput,
+            'date_to': forms.DateInput,
+        }
+        labels = {
+            'requested_value': 'Requested Value ($)'
+        }
+
+    def __init__(self, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields['requested_value'].widget.attrs['min'] = 0
+
+    def clean(self):
+        cleaned_data = super().clean()
+        date_from = cleaned_data['date_from']
+        date_to = cleaned_data['date_to']
+
+        if date_from > date_to:
+            self.add_error('date_from', 'Date From must be before Date To')
+
+        return cleaned_data
+
+
+class CreatePaymentRequestForm(FileFormMixin, PaymentRequestBaseForm):
+    receipts = MultiFileField(required=False)
+
+    def save(self, commit=True):
+        request = super().save(commit=commit)
+
+        receipts = self.cleaned_data['receipts'] or []
+
+        PaymentReceipt.objects.bulk_create(
+            PaymentReceipt(payment_request=request, file=receipt)
+            for receipt in receipts
+        )
+
+        return request
+
+
+class EditPaymentRequestForm(FileFormMixin, PaymentRequestBaseForm):
+    receipt_list = forms.ModelMultipleChoiceField(
+        widget=forms.CheckboxSelectMultiple(attrs={'class': 'delete'}),
+        queryset=PaymentReceipt.objects.all(),
+        required=False,
+        label='Receipts'
+    )
+    receipts = MultiFileField(label='', required=False)
+
+    def __init__(self, user=None, instance=None, *args, **kwargs):
+        super().__init__(*args, instance=instance, **kwargs)
+
+        self.fields['receipt_list'].queryset = instance.receipts.all()
+
+        self.fields['requested_value'].label = 'Value'
+
+    @transaction.atomic
+    def save(self, commit=True):
+        request = super().save(commit=commit)
+
+        removed_receipts = self.cleaned_data['receipt_list']
+
+        removed_receipts.delete()
+
+        to_add = self.cleaned_data['receipts']
+        if to_add:
+            PaymentReceipt.objects.bulk_create(
+                PaymentReceipt(payment_request=request, file=receipt)
+                for receipt in to_add
+            )
+        return request
+
+
+class SelectDocumentForm(forms.ModelForm):
+    document = forms.ChoiceField(
+        label="Document",
+        widget=forms.Select(attrs={'id': 'from_submission'})
+    )
+
+    class Meta:
+        model = PacketFile
+        fields = ['category', 'document']
+
+    def __init__(self, existing_files, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.files = existing_files
+
+        choices = [(f.url, f.filename) for f in self.files]
+
+        self.fields['document'].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):
+        return super().save(*args, **kwargs)
diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a83b6017ca545aad598816204e298863d474fec
--- /dev/null
+++ b/hypha/apply/projects/forms/project.py
@@ -0,0 +1,193 @@
+from django import forms
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+
+from hypha.apply.funds.models import ApplicationSubmission
+from hypha.apply.users.groups import STAFF_GROUP_NAME
+
+from ..models.project import COMMITTED, Approval, Contract, PacketFile, Project
+
+User = get_user_model()
+
+
+class ApproveContractForm(forms.Form):
+    id = forms.IntegerField(widget=forms.HiddenInput())
+
+    def __init__(self, instance, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.instance = instance
+        if instance:
+            self.fields['id'].initial = instance.id
+
+    def clean_id(self):
+        if self.has_changed():
+            raise forms.ValidationError('Something changed before your approval please re-review')
+
+    def clean(self):
+        if not self.instance:
+            raise forms.ValidationError('The contract you were trying to approve has already been approved')
+
+        if not self.instance.is_signed:
+            raise forms.ValidationError('You can only approve a signed contract')
+
+        super().clean()
+
+    def save(self, *args, **kwargs):
+        self.instance.save()
+        return self.instance
+
+
+class CreateProjectForm(forms.Form):
+    submission = forms.ModelChoiceField(
+        queryset=ApplicationSubmission.objects.filter(project__isnull=True),
+        widget=forms.HiddenInput(),
+    )
+
+    def __init__(self, instance=None, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if instance:
+            self.fields['submission'].initial = instance.id
+
+    def save(self, *args, **kwargs):
+        submission = self.cleaned_data['submission']
+        return Project.create_from_submission(submission)
+
+
+class CreateApprovalForm(forms.ModelForm):
+    by = forms.ModelChoiceField(
+        queryset=User.objects.approvers(),
+        widget=forms.HiddenInput(),
+    )
+
+    class Meta:
+        model = Approval
+        fields = ('by',)
+
+    def __init__(self, user=None, *args, **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 ProjectEditForm(forms.ModelForm):
+    class Meta:
+        fields = [
+            'title',
+            'value',
+            'proposed_start',
+            'proposed_end',
+        ]
+        model = Project
+        widgets = {
+            'title': forms.TextInput,
+            'proposed_end': forms.DateInput,
+            'proposed_start': forms.DateInput,
+        }
+
+
+class ProjectApprovalForm(ProjectEditForm):
+    def __init__(self, *args, extra_fields=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        if extra_fields:
+            self.fields = {
+                **self.fields,
+                **extra_fields,
+            }
+
+    def save(self, *args, **kwargs):
+        self.instance.form_data = {
+            field: self.cleaned_data[field]
+            for field in self.instance.question_field_ids
+            if field in self.cleaned_data
+        }
+        self.instance.user_has_updated_details = True
+        return super().save(*args, **kwargs)
+
+
+class RejectionForm(forms.Form):
+    comment = forms.CharField(widget=forms.Textarea)
+
+    def __init__(self, instance=None, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+
+class RemoveDocumentForm(forms.ModelForm):
+    id = forms.IntegerField(widget=forms.HiddenInput())
+
+    class Meta:
+        fields = ['id']
+        model = PacketFile
+
+    def __init__(self, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+
+class SetPendingForm(forms.ModelForm):
+    class Meta:
+        fields = ['id']
+        model = Project
+        widgets = {'id': forms.HiddenInput()}
+
+    def __init__(self, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+    def clean(self):
+        if self.instance.status != COMMITTED:
+            raise forms.ValidationError('A Project can only be sent for Approval when Committed.')
+
+        if self.instance.is_locked:
+            raise forms.ValidationError('A Project can only be sent for Approval once')
+
+        super().clean()
+
+    def save(self, *args, **kwargs):
+        self.instance.is_locked = True
+        return super().save(*args, **kwargs)
+
+
+class UploadContractForm(forms.ModelForm):
+    class Meta:
+        fields = ['file']
+        model = Contract
+
+
+class StaffUploadContractForm(forms.ModelForm):
+    class Meta:
+        fields = ['file', 'is_signed']
+        model = Contract
+
+
+class UploadDocumentForm(forms.ModelForm):
+    class Meta:
+        fields = ['title', 'category', 'document']
+        model = PacketFile
+        widgets = {'title': forms.TextInput()}
+        labels = {
+            "title": "File Name",
+        }
+
+    def __init__(self, user=None, instance=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+
+class UpdateProjectLeadForm(forms.ModelForm):
+    class Meta:
+        fields = ['lead']
+        model = Project
+
+    def __init__(self, user=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        lead_field = self.fields['lead']
+        lead_field.label = f'Update lead from {self.instance.lead} to'
+
+        qwargs = Q(groups__name=STAFF_GROUP_NAME) | Q(is_superuser=True)
+        lead_field.queryset = (lead_field.queryset.exclude(pk=self.instance.lead_id)
+                                                  .filter(qwargs)
+                                                  .distinct())
diff --git a/hypha/apply/projects/forms/report.py b/hypha/apply/projects/forms/report.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf633c98539e7d33efb74221541fa1a04f701bbe
--- /dev/null
+++ b/hypha/apply/projects/forms/report.py
@@ -0,0 +1,146 @@
+from django import forms
+from django.core.exceptions import ValidationError
+from django.db import transaction
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+from django_file_form.forms import FileFormMixin
+
+from hypha.apply.stream_forms.fields import MultiFileField
+from hypha.apply.utils.fields import RichTextField
+
+from ..models.report import Report, ReportConfig, ReportPrivateFiles, ReportVersion
+
+
+class ReportEditForm(FileFormMixin, forms.ModelForm):
+    public_content = RichTextField(
+        help_text="This section of the report will be shared with the broader community."
+    )
+    private_content = RichTextField(
+        help_text="This section of the report will be shared with staff only."
+    )
+    file_list = forms.ModelMultipleChoiceField(
+        widget=forms.CheckboxSelectMultiple(attrs={'class': 'delete'}),
+        queryset=ReportPrivateFiles.objects.all(),
+        required=False,
+        label='Files'
+    )
+    files = MultiFileField(required=False, label='')
+
+    class Meta:
+        model = Report
+        fields = (
+            'public_content',
+            'private_content',
+            'file_list',
+            'files',
+        )
+
+    def __init__(self, *args, user=None, initial={}, **kwargs):
+        self.report_files = initial.pop(
+            'file_list',
+            ReportPrivateFiles.objects.none(),
+        )
+        super().__init__(*args, initial=initial, **kwargs)
+        self.fields['file_list'].queryset = self.report_files
+        self.user = user
+
+    def clean(self):
+        cleaned_data = super().clean()
+        public = cleaned_data['public_content']
+        private = cleaned_data['private_content']
+        if not private and not public:
+            missing_content = 'Must include either public or private content when submitting a report.'
+            self.add_error('public_content', missing_content)
+            self.add_error('private_content', missing_content)
+        return cleaned_data
+
+    @transaction.atomic
+    def save(self, commit=True):
+        is_draft = 'save' in self.data
+
+        version = ReportVersion.objects.create(
+            report=self.instance,
+            public_content=self.cleaned_data['public_content'],
+            private_content=self.cleaned_data['private_content'],
+            submitted=timezone.now(),
+            draft=is_draft,
+            author=self.user,
+        )
+
+        if is_draft:
+            self.instance.draft = version
+        else:
+            # If this is the first submission of the report we track that as the
+            # submitted date of the report
+            if not self.instance.submitted:
+                self.instance.submitted = version.submitted
+            self.instance.current = version
+            self.instance.draft = None
+
+        instance = super().save(commit)
+
+        removed_files = self.cleaned_data['file_list']
+        ReportPrivateFiles.objects.bulk_create(
+            ReportPrivateFiles(report=version, document=file.document)
+            for file in self.report_files
+            if file not in removed_files
+        )
+
+        added_files = self.cleaned_data['files']
+        if added_files:
+            ReportPrivateFiles.objects.bulk_create(
+                ReportPrivateFiles(report=version, document=file)
+                for file in added_files
+            )
+
+        return instance
+
+
+class ReportFrequencyForm(forms.ModelForm):
+    start = forms.DateField(label='Starting on:')
+
+    class Meta:
+        model = ReportConfig
+        fields = ('occurrence', 'frequency', 'start')
+        labels = {
+            'occurrence': 'No.',
+        }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        today = timezone.now().date()
+        last_report = self.instance.last_report()
+
+        self.fields['start'].widget.attrs.update({
+            'min': max(
+                last_report.end_date if last_report else today,
+                today,
+            ),
+            'max': self.instance.project.end_date,
+        })
+
+    def clean_start(self):
+        start_date = self.cleaned_data['start']
+        last_report = self.instance.last_report()
+        if last_report and start_date <= last_report.end_date:
+            raise ValidationError(
+                _("Cannot start a schedule before the current reporting period"),
+                code="bad_start"
+            )
+
+        if start_date < timezone.now().date():
+            raise ValidationError(
+                _("Cannot start a schedule in the past"),
+                code="bad_start"
+            )
+
+        if start_date > self.instance.project.end_date:
+            raise ValidationError(
+                _("Cannot start a schedule beyond the end date"),
+                code="bad_start"
+            )
+        return start_date
+
+    def save(self, *args, **kwargs):
+        self.instance.schedule_start = self.cleaned_data['start']
+        return super().save(*args, **kwargs)
diff --git a/hypha/apply/projects/forms/vendor.py b/hypha/apply/projects/forms/vendor.py
new file mode 100644
index 0000000000000000000000000000000000000000..961aefb4f97a8bfcb68474a6705d3a4b29e24e5b
--- /dev/null
+++ b/hypha/apply/projects/forms/vendor.py
@@ -0,0 +1,135 @@
+import datetime
+from operator import itemgetter
+
+from babel.core import get_global
+from babel.numbers import get_currency_name, get_territory_currencies
+from django import forms
+from django_file_form.forms import FileFormMixin
+
+from addressfield.fields import AddressField
+from hypha.apply.stream_forms.fields import MultiFileField
+
+from ..models.vendor import VendorFormSettings
+
+
+def get_active_currencies():
+    active_currencies = []
+    territories = get_global('territory_currencies').keys()
+    for territory in territories:
+        currencies = get_territory_currencies(territory, datetime.date.today())
+        if currencies:
+            for currency in currencies:
+                if currency not in active_currencies:
+                    active_currencies.append(currencies[0])
+    return active_currencies
+
+
+class BaseVendorForm:
+    def __init__(self, site=None, *args, **kwargs):
+        if site:
+            self.form_settings = VendorFormSettings.for_site(site)
+        super().__init__(*args, **kwargs)
+
+    def apply_form_settings(self, fields):
+        for field in fields:
+            try:
+                self.fields[field].label = getattr(self.form_settings, f'{field}_label')
+            except AttributeError:
+                pass
+            try:
+                self.fields[field].help_text = getattr(self.form_settings, f'{field}_help_text')
+            except AttributeError:
+                pass
+        return fields
+
+
+class CreateVendorFormStep1(BaseVendorForm, forms.Form):
+    TYPE_CHOICES = [
+        ('organization', 'Yes, the account belongs to the organisation above'),
+        ('personal', 'No, it is a personal bank account'),
+    ]
+
+    name = forms.CharField(required=True)
+    contractor_name = forms.CharField(required=True)
+    type = forms.ChoiceField(choices=TYPE_CHOICES, required=True, widget=forms.RadioSelect)
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep1, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
+
+
+class CreateVendorFormStep2(BaseVendorForm, forms.Form):
+    required_to_pay_taxes = forms.TypedChoiceField(
+        choices=((False, 'No'), (True, 'Yes')),
+        coerce=lambda x: x == 'True',
+        widget=forms.RadioSelect,
+        required=True
+    )
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep2, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
+
+
+class CreateVendorFormStep3(FileFormMixin, BaseVendorForm, forms.Form):
+    due_diligence_documents = MultiFileField(required=True)
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep3, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
+
+
+class CreateVendorFormStep4(BaseVendorForm, forms.Form):
+    CURRENCY_CHOICES = [
+        (currency, f'{get_currency_name(currency)} - {currency}')
+        for currency in get_active_currencies()
+    ]
+
+    account_holder_name = forms.CharField(required=True)
+    account_routing_number = forms.CharField(required=True)
+    account_number = forms.CharField(required=True)
+    account_currency = forms.ChoiceField(
+        choices=sorted(CURRENCY_CHOICES, key=itemgetter(1)),
+        required=True,
+        initial='USD'
+    )
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep4, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
+
+
+class CreateVendorFormStep5(BaseVendorForm, forms.Form):
+    need_extra_info = forms.TypedChoiceField(
+        choices=((False, 'No'), (True, 'Yes')),
+        coerce=lambda x: x == 'True',
+        widget=forms.RadioSelect,
+        required=True
+    )
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep5, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
+
+
+class CreateVendorFormStep6(BaseVendorForm, forms.Form):
+    CURRENCY_CHOICES = [
+        (currency, f'{get_currency_name(currency)} - {currency}')
+        for currency in get_active_currencies()
+    ]
+    branch_address = AddressField()
+    ib_account_routing_number = forms.CharField(required=False)
+    ib_account_number = forms.CharField(required=False)
+    ib_account_currency = forms.ChoiceField(
+        choices=sorted(CURRENCY_CHOICES, key=itemgetter(1)),
+        required=False,
+        initial='USD'
+    )
+    ib_branch_address = AddressField()
+    nid_type = forms.CharField(required=False)
+    nid_number = forms.CharField(required=False)
+    other_info = forms.CharField(required=False, widget=forms.Textarea)
+
+    def __init__(self, *args, **kwargs):
+        super(CreateVendorFormStep6, self).__init__(*args, **kwargs)
+        self.fields = self.apply_form_settings(self.fields)
diff --git a/hypha/apply/projects/migrations/0036_add_vendor.py b/hypha/apply/projects/migrations/0036_add_vendor.py
new file mode 100644
index 0000000000000000000000000000000000000000..5924a565ed784f050942183362e513a691106f35
--- /dev/null
+++ b/hypha/apply/projects/migrations/0036_add_vendor.py
@@ -0,0 +1,130 @@
+# Generated by Django 2.2.24 on 2021-07-26 08:22
+
+from django.conf import settings
+import django.core.files.storage
+from django.db import migrations, models
+import django.db.models.deletion
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('application_projects', '0035_add_heading_block_to_form_fields_block'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='BankInformation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('account_holder_name', models.CharField(max_length=150)),
+                ('account_routing_number', models.CharField(max_length=10)),
+                ('account_number', models.CharField(max_length=20)),
+                ('account_currency', models.CharField(max_length=10)),
+                ('need_extra_info', models.BooleanField(default=False)),
+                ('branch_address', models.TextField(blank=True, verbose_name='Address')),
+                ('nid_type', models.CharField(blank=True, max_length=25, verbose_name='National Identity Document Type')),
+                ('nid_number', models.CharField(blank=True, max_length=20, verbose_name='National Identity Document Number')),
+                ('iba_info', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bank_info', to='application_projects.BankInformation', verbose_name='Intermediary Bank Account Information')),
+            ],
+        ),
+        migrations.RemoveField(
+            model_name='project',
+            name='contact_address',
+        ),
+        migrations.RemoveField(
+            model_name='project',
+            name='contact_email',
+        ),
+        migrations.RemoveField(
+            model_name='project',
+            name='contact_legal_name',
+        ),
+        migrations.RemoveField(
+            model_name='project',
+            name='contact_phone',
+        ),
+        migrations.AddField(
+            model_name='projectsettings',
+            name='vendor_setup_required',
+            field=models.BooleanField(default=True),
+        ),
+        migrations.CreateModel(
+            name='VendorFormSettings',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name_label', models.TextField(default='1. What is the name of the person/organisation on the contract?', verbose_name='label')),
+                ('name_help_text', wagtail.core.fields.RichTextField(blank=True, default='This is the party name in the contract.', verbose_name='help text')),
+                ('contractor_name_label', models.TextField(default="2. What is the individual's name who is signing the contract?", verbose_name='label')),
+                ('contractor_name_help_text', wagtail.core.fields.RichTextField(blank=True, default='This person is is authorised to sign contract on behalf of the person or organization named above.', verbose_name='help text')),
+                ('type_label', models.TextField(default='3. Is the bank account owned by the person or organisation in the Question 1 above?', verbose_name='label')),
+                ('type_help_text', wagtail.core.fields.RichTextField(blank=True, default='The name of the bank account must be the same as on the contract.', verbose_name='help text')),
+                ('required_to_pay_taxes_label', models.TextField(default='Is the organisation required to pay US taxes?', verbose_name='label')),
+                ('required_to_pay_taxes_help_text', wagtail.core.fields.RichTextField(blank=True, default='', verbose_name='help text')),
+                ('due_diligence_documents_label', models.TextField(default='Due Diligence Documents', verbose_name='label')),
+                ('due_diligence_documents_help_text', wagtail.core.fields.RichTextField(blank=True, default='Upload Due Diligence Documents. E.g. w8/w9 forms.', verbose_name='help text')),
+                ('account_holder_name_label', models.TextField(default='Bank Account Holder name', verbose_name='label')),
+                ('account_holder_name_help_text', wagtail.core.fields.RichTextField(blank=True, default='This name must be same as the person or organisation that signed the contract. This person is authorised to sign contracts on behalf of the person or organisation named above.', verbose_name='help text')),
+                ('account_routing_number_label', models.TextField(default='Bank Account Routing number', verbose_name='label')),
+                ('account_routing_number_help_text', wagtail.core.fields.RichTextField(blank=True, default='Depending on your country, this might be called the ACH, SWIFT, BIC or ABA number.', verbose_name='help text')),
+                ('account_number_label', models.TextField(default='Bank Account Number', verbose_name='label')),
+                ('account_number_help_text', wagtail.core.fields.RichTextField(blank=True, default='Depending on your country, this might be called the account number, IBAN, or BBAN number.', verbose_name='help text')),
+                ('account_currency_label', models.TextField(default='Bank Account Currency', verbose_name='label')),
+                ('account_currency_help_text', wagtail.core.fields.RichTextField(blank=True, default='This is the currency of this bank account.', verbose_name='help text')),
+                ('need_extra_info_label', models.TextField(default='Do you need to provide us with extra information?', verbose_name='label')),
+                ('need_extra_info_help_text', wagtail.core.fields.RichTextField(blank=True, default='', verbose_name='help text')),
+                ('branch_address_label', models.TextField(default='Bank Account Branch Address', verbose_name='label')),
+                ('branch_address_help_text', models.TextField(blank=True, default='The address of the bank branch where you have the bank account located(not the bank account holder address)', verbose_name='help text')),
+                ('ib_account_routing_number_label', models.TextField(default='Intermediary Bank Account Routing Number', verbose_name='label')),
+                ('ib_account_routing_number_help_text', wagtail.core.fields.RichTextField(blank=True, default='Depending on your country, this might be called ACH, SWIFT, BIC or ABA number', verbose_name='help text')),
+                ('ib_account_number_label', models.TextField(default='Intermediary Bank Account Number', verbose_name='label')),
+                ('ib_account_number_help_text', wagtail.core.fields.RichTextField(blank=True, default='Depending on your country, this might be called the account number, IBAN, or BBAN number', verbose_name='help text')),
+                ('ib_account_currency_label', models.TextField(default='Intermediary Bank Account Currency', verbose_name='label')),
+                ('ib_account_currency_help_text', wagtail.core.fields.RichTextField(blank=True, default='This is the currency of this bank account', verbose_name='help text')),
+                ('ib_branch_address_label', models.TextField(default='Intermediary Bank Branch Address', verbose_name='label')),
+                ('ib_branch_address_help_text', wagtail.core.fields.RichTextField(blank=True, default='Bank branch address(not the bank account holder address)', verbose_name='help text')),
+                ('nid_type_label', models.TextField(default='Account Holder National Identity Document Type', verbose_name='label')),
+                ('nid_type_help_text', wagtail.core.fields.RichTextField(blank=True, default='This could be a passport, a National Identity number, or other national identity document.', verbose_name='help text')),
+                ('nid_number_label', models.TextField(default='Account Holder National Identity Document Number', verbose_name='label')),
+                ('nid_number_help_text', wagtail.core.fields.RichTextField(blank=True, default='', verbose_name='help text')),
+                ('other_info_label', models.TextField(default='Other Information', verbose_name='label')),
+                ('other_info_help_text', wagtail.core.fields.RichTextField(blank=True, default='If you need to include other information not listed above, provide it here.', verbose_name='help text')),
+                ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='Vendor',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Creation time')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Update time')),
+                ('name', models.CharField(blank=True, max_length=150)),
+                ('contractor_name', models.CharField(blank=True, max_length=150)),
+                ('address', models.TextField(blank=True, verbose_name='Address')),
+                ('type', models.CharField(blank=True, choices=[('organization', 'Yes, the account belongs to the organisation above'), ('personal', 'No, it is a personal bank account')], max_length=15)),
+                ('required_to_pay_taxes', models.BooleanField(default=False)),
+                ('other_info', models.TextField(blank=True)),
+                ('user_has_updated_details', models.BooleanField(default=False)),
+                ('bank_info', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='application_projects.BankInformation')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='vendor', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='DueDiligenceDocument',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('document', models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to='due_diligence_documents')),
+                ('vendor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='due_diligence_documents', to='application_projects.Vendor')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='project',
+            name='vendor',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='application_projects.Vendor'),
+        ),
+    ]
diff --git a/hypha/apply/projects/models/__init__.py b/hypha/apply/projects/models/__init__.py
index dc9e75f778ac499b349d2c3cba8a054fd9f1f1da..760e37b3dde0c4ce821cdb564e19582e05508c66 100644
--- a/hypha/apply/projects/models/__init__.py
+++ b/hypha/apply/projects/models/__init__.py
@@ -9,6 +9,7 @@ from .project import (
     ProjectSettings,
 )
 from .report import Report, ReportConfig, ReportPrivateFiles, ReportVersion
+from .vendor import BankInformation, DueDiligenceDocument, Vendor
 
 __all__ = [
     'Project',
@@ -25,4 +26,7 @@ __all__ = [
     'ReportVersion',
     'ReportPrivateFiles',
     'ReportConfig',
+    'Vendor',
+    'BankInformation',
+    'DueDiligenceDocument',
 ]
diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py
index 785132fb24e2ec1432804dfb1130a51b98347278..5bd291fb25fd98159a73e9a867cc63d4c0b359e0 100644
--- a/hypha/apply/projects/models/project.py
+++ b/hypha/apply/projects/models/project.py
@@ -29,6 +29,8 @@ from hypha.apply.stream_forms.files import StreamFieldDataEncoder
 from hypha.apply.stream_forms.models import BaseStreamForm
 from hypha.apply.utils.storage import PrivateStorage
 
+from .vendor import Vendor
+
 logger = logging.getLogger(__name__)
 
 
@@ -131,11 +133,11 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='owned_projects')
 
     title = models.TextField()
-
-    contact_legal_name = models.TextField(_('Person or Organisation name'), default='')
-    contact_email = models.TextField(_('Email'), default='')
-    contact_address = models.TextField(_('Address'), default='')
-    contact_phone = models.TextField(_('Phone'), default='')
+    vendor = models.ForeignKey(
+        "application_projects.Vendor",
+        on_delete=models.SET_NULL,
+        null=True, blank=True, related_name='projects'
+    )
     value = models.DecimalField(
         default=0,
         max_digits=10,
@@ -177,8 +179,8 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
 
     def get_address_display(self):
         try:
-            address = json.loads(self.contact_address)
-        except json.JSONDecodeError:
+            address = json.loads(self.vendor.address)
+        except (json.JSONDecodeError, AttributeError):
             return ''
         else:
             return ', '.join(
@@ -207,14 +209,17 @@ class Project(BaseStreamForm, AccessFormData, models.Model):
 
         # See if there is a form field named "legal name", if not use user name.
         legal_name = submission.get_answer_from_label('legal name') or submission.user.full_name
-
+        vendor, _ = Vendor.objects.get_or_create(
+            user=submission.user
+        )
+        vendor.name = legal_name
+        vendor.address = submission.form_data.get('address', '')
+        vendor.save()
         return Project.objects.create(
             submission=submission,
-            title=submission.title,
             user=submission.user,
-            contact_email=submission.user.email,
-            contact_legal_name=legal_name,
-            contact_address=submission.form_data.get('address', ''),
+            title=submission.title,
+            vendor=vendor,
             value=submission.form_data.get('value', 0),
         )
 
@@ -367,6 +372,7 @@ class ProjectApprovalForm(BaseStreamForm, models.Model):
 @register_setting
 class ProjectSettings(BaseSetting):
     compliance_email = models.TextField("Compliance Email")
+    vendor_setup_required = models.BooleanField(default=True)
 
 
 class Approval(models.Model):
diff --git a/hypha/apply/projects/models/vendor.py b/hypha/apply/projects/models/vendor.py
new file mode 100644
index 0000000000000000000000000000000000000000..83b0523d7ef6bdd1e14b38535783fd7e38b0e998
--- /dev/null
+++ b/hypha/apply/projects/models/vendor.py
@@ -0,0 +1,324 @@
+from django.conf import settings
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel
+from wagtail.contrib.settings.models import BaseSetting, register_setting
+from wagtail.core.fields import RichTextField
+
+from hypha.apply.utils.storage import PrivateStorage
+
+
+class BankInformation(models.Model):
+    account_holder_name = models.CharField(max_length=150)
+    account_routing_number = models.CharField(max_length=10)
+    account_number = models.CharField(max_length=20)
+    account_currency = models.CharField(
+        max_length=10
+    )
+    need_extra_info = models.BooleanField(default=False)
+    branch_address = models.TextField(_('Address'), blank=True)
+    iba_info = models.OneToOneField(
+        'self',
+        null=True, blank=True,
+        on_delete=models.SET_NULL,
+        related_name='bank_info',
+        verbose_name='Intermediary Bank Account Information'
+    )
+    nid_type = models.CharField(
+        max_length=25,
+        verbose_name='National Identity Document Type',
+        blank=True
+    )
+    nid_number = models.CharField(
+        max_length=20,
+        blank=True,
+        verbose_name='National Identity Document Number'
+    )
+
+    def __str__(self):
+        return self.account_holder_name
+
+
+class Vendor(models.Model):
+
+    TYPE_CHOICES = [
+        ('organization', _('Yes, the account belongs to the organisation above')),
+        ('personal', _('No, it is a personal bank account')),
+    ]
+    created_at = models.DateTimeField(verbose_name=_('Creation time'), auto_now_add=True)
+    updated_at = models.DateTimeField(verbose_name=_('Update time'), auto_now=True)
+    user = models.OneToOneField(
+        settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
+        related_name='vendor'
+    )
+    name = models.CharField(max_length=150, blank=True)
+    contractor_name = models.CharField(max_length=150, blank=True)
+    address = models.TextField(_('Address'), blank=True)
+    type = models.CharField(max_length=15, choices=TYPE_CHOICES, blank=True)
+    required_to_pay_taxes = models.BooleanField(default=False)
+    bank_info = models.OneToOneField(
+        BankInformation,
+        on_delete=models.SET_NULL,
+        null=True, blank=True,
+    )
+    other_info = models.TextField(blank=True)
+    # tracks updates to the Vendor fields via the Vendor Setup Form.
+    user_has_updated_details = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.name
+
+
+class DueDiligenceDocument(models.Model):
+    document = models.FileField(
+        upload_to="due_diligence_documents", storage=PrivateStorage()
+    )
+    vendor = models.ForeignKey(
+        Vendor,
+        on_delete=models.CASCADE,
+        related_name='due_diligence_documents',
+    )
+
+    def __str__(self):
+        return self.vendor.name + ' -> ' + self.document.name
+
+
+@register_setting
+class VendorFormSettings(BaseSetting):
+    name_label = models.TextField(
+        'label',
+        default='1. What is the name of the person/organisation on the contract?'
+    )
+    name_help_text = RichTextField(
+        'help text', blank=True,
+        default='This is the party name in the contract.'
+    )
+    contractor_name_label = models.TextField(
+        'label',
+        default="2. What is the individual's name who is signing the contract?"
+    )
+    contractor_name_help_text = RichTextField(
+        'help text', blank=True,
+        default="This person is is authorised to sign contract on behalf of the person or organization named above."
+    )
+    type_label = models.TextField(
+        'label',
+        default='3. Is the bank account owned by the person or organisation in the Question 1 above?'
+    )
+    type_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='The name of the bank account must be the same as on the contract.'
+    )
+    required_to_pay_taxes_label = models.TextField(
+        'label',
+        default='Is the organisation required to pay US taxes?'
+    )
+    required_to_pay_taxes_help_text = RichTextField(
+        'help text',
+        default='', blank=True,
+    )
+    due_diligence_documents_label = models.TextField(
+        'label',
+        default='Due Diligence Documents'
+    )
+    due_diligence_documents_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Upload Due Diligence Documents. E.g. w8/w9 forms.'
+    )
+    account_holder_name_label = models.TextField(
+        'label',
+        default='Bank Account Holder name'
+    )
+    account_holder_name_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='This name must be same as the person or organisation that signed the contract. '
+        'This person is authorised to sign contracts on behalf of the person or organisation named above.'
+    )
+    account_routing_number_label = models.TextField(
+        'label',
+        default='Bank Account Routing number'
+    )
+    account_routing_number_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Depending on your country, this might be called the ACH, SWIFT, BIC or ABA number.'
+    )
+    account_number_label = models.TextField(
+        'label',
+        default='Bank Account Number'
+    )
+    account_number_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Depending on your country, this might be called the account number, IBAN, or BBAN number.'
+    )
+    account_currency_label = models.TextField(
+        'label',
+        default='Bank Account Currency'
+    )
+    account_currency_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='This is the currency of this bank account.'
+    )
+    need_extra_info_label = models.TextField(
+        'label',
+        default='Do you need to provide us with extra information?'
+    )
+    need_extra_info_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default=''
+    )
+    branch_address_label = models.TextField(
+        'label',
+        default='Bank Account Branch Address'
+    )
+    branch_address_help_text = models.TextField(
+        'help text',
+        blank=True,
+        default='The address of the bank branch where you have the bank account '
+        'located(not the bank account holder address)'
+    )
+    ib_account_routing_number_label = models.TextField(
+        'label',
+        default='Intermediary Bank Account Routing Number'
+    )
+    ib_account_routing_number_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Depending on your country, this might be called ACH, SWIFT, BIC or ABA number'
+    )
+    ib_account_number_label = models.TextField(
+        'label',
+        default='Intermediary Bank Account Number'
+    )
+    ib_account_number_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Depending on your country, this might be called the account number, IBAN, or BBAN number'
+    )
+    ib_account_currency_label = models.TextField(
+        'label',
+        default='Intermediary Bank Account Currency'
+    )
+    ib_account_currency_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='This is the currency of this bank account'
+    )
+    ib_branch_address_label = models.TextField(
+        'label',
+        default='Intermediary Bank Branch Address'
+    )
+    ib_branch_address_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='Bank branch address(not the bank account holder address)'
+    )
+    nid_type_label = models.TextField(
+        'label',
+        default='Account Holder National Identity Document Type'
+    )
+    nid_type_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='This could be a passport, a National Identity number, '
+        'or other national identity document.'
+    )
+    nid_number_label = models.TextField(
+        'label',
+        default='Account Holder National Identity Document Number'
+    )
+    nid_number_help_text = RichTextField(
+        'help text',
+        default='',
+        blank=True,
+    )
+    other_info_label = models.TextField(
+        'label',
+        default='Other Information'
+    )
+    other_info_help_text = RichTextField(
+        'help text',
+        blank=True,
+        default='If you need to include other information not listed above, provide it here.'
+    )
+
+    panels = [
+        MultiFieldPanel([
+            FieldPanel('name_label'),
+            FieldPanel('name_help_text'),
+        ], 'Name'),
+        MultiFieldPanel([
+            FieldPanel('contractor_name_label'),
+            FieldPanel('contractor_name_help_text'),
+        ], 'Contractor Name'),
+        MultiFieldPanel([
+            FieldPanel('type_label'),
+            FieldPanel('type_help_text'),
+        ], 'Type'),
+        MultiFieldPanel([
+            FieldPanel('required_to_pay_taxes_label'),
+            FieldPanel('required_to_pay_taxes_help_text'),
+        ], 'Required to pay taxes'),
+        MultiFieldPanel([
+            FieldPanel('due_diligence_documents_label'),
+            FieldPanel('due_diligence_documents_help_text'),
+        ], 'Due Diligence Documents'),
+        MultiFieldPanel([
+            FieldPanel('account_holder_name_label'),
+            FieldPanel('account_holder_name_help_text'),
+        ], 'Account Holder Name'),
+        MultiFieldPanel([
+            FieldPanel('account_routing_number_label'),
+            FieldPanel('account_routing_number_help_text'),
+        ], 'Account Routing Number'),
+        MultiFieldPanel([
+            FieldPanel('account_number_label'),
+            FieldPanel('account_number_help_text'),
+        ], 'Account Number'),
+        MultiFieldPanel([
+            FieldPanel('account_currency_label'),
+            FieldPanel('account_currency_help_text'),
+        ], 'Account Currency'),
+        MultiFieldPanel([
+            FieldPanel('need_extra_info_label'),
+            FieldPanel('need_extra_info_help_text'),
+        ], 'Need Extra Info'),
+        MultiFieldPanel([
+            FieldPanel('branch_address_label'),
+            FieldPanel('branch_address_help_text'),
+        ], 'Account Branch Address'),
+        MultiFieldPanel([
+            FieldPanel('ib_account_routing_number_label'),
+            FieldPanel('ib_account_routing_number_help_text'),
+        ], 'Intermediary Account Routing Number'),
+        MultiFieldPanel([
+            FieldPanel('ib_account_number_label'),
+            FieldPanel('ib_account_number_help_text'),
+        ], 'Intermediary Account Number'),
+        MultiFieldPanel([
+            FieldPanel('ib_account_currency_label'),
+            FieldPanel('ib_account_currency_help_text'),
+        ], 'Intermediary Account Currency'),
+        MultiFieldPanel([
+            FieldPanel('ib_branch_address_label'),
+            FieldPanel('ib_branch_address_help_text'),
+        ], 'Intermediary Account Branch Address'),
+        MultiFieldPanel([
+            FieldPanel('nid_type_label'),
+            FieldPanel('nid_type_help_text'),
+        ], 'National Identity Document Type'),
+        MultiFieldPanel([
+            FieldPanel('nid_number_label'),
+            FieldPanel('nid_number_help_text'),
+        ], 'National Identity Document Number'),
+        MultiFieldPanel([
+            FieldPanel('other_info_label'),
+            FieldPanel('other_info_help_text'),
+        ], 'Other Information'),
+    ]
diff --git a/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html b/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
index b6cef459217ec8430e1e3135cc31b6052d57c7db..1617d971485a6e5ebde4de37ff3de1c90ac29de6 100644
--- a/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
+++ b/hypha/apply/projects/templates/application_projects/includes/supporting_documents.html
@@ -19,6 +19,28 @@
                 {% endif %}
             </div>
         </li>
+        {% if settings.application_projects.ProjectSettings.vendor_setup_required and project.vendor %}
+            <li class="docs-block__row">
+                <div class="docs-block__row-inner">
+                    <svg class="icon docs-block__icon{% if project.vendor.user_has_updated_details %} is-complete{% endif %}"><use xlink:href="#tick"></use></svg>
+                    <p class="docs-block__title">Contractor Setup Form</p>
+                </div>
+                <div class="docs-block__row-inner">
+                    {% if editable %}
+                        <a class="docs-block__link" href="{% url 'apply:projects:vendor' pk=project.pk %}">
+                            {% if project.vendor.user_has_updated_details %}
+                            Edit
+                            {% else %}
+                            Create
+                            {% endif %}
+                        </a>
+                    {% endif %}
+                    {% if project.vendor.user_has_updated_details %}
+                        <a class="docs-block__link" href="{% url 'apply:projects:vendor-detail' pk=project.pk vendor_pk=project.vendor.pk %}">View</a>
+                    {% endif %}
+                </div>
+            </li>
+        {% endif %}
 
         <li class="docs-block__row">
             <div class="docs-block__row-inner">
diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html
index a08e4361a3e1fda248b3178308f4702e2958104b..fdce23c27a09ff58d3d8cb8b58d35670ca3ddd1a 100644
--- a/hypha/apply/projects/templates/application_projects/project_detail.html
+++ b/hypha/apply/projects/templates/application_projects/project_detail.html
@@ -92,13 +92,13 @@
 
                     <div>
                         <h5>Legal name</h5>
-                        <p>{{ object.contact_legal_name|default:"-" }}</p>
+                        <p>{{ object.vendor.name |default:"-" }}</p>
                     </div>
 
                     <div>
                         <h5>Email</h5>
-                        {% if object.contact_email %}
-                        <a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a>
+                        {% if object.vendor.user.email %}
+                        <a href="mailto:{{ object.vendor.user.email }}">{{ object.vendor.user.email }}</a>
                         {% else %}
                         -
                         {% endif %}
@@ -110,12 +110,12 @@
                 <div class="rich-text--hidden js-rich-text-hidden">
                     <div>
                         <h5>Address</h5>
-                        <p>{{ object.get_address_display|default:"-"}}</p>
+                        <p>{{ object.vendor.get_address_display|default:"-"}}</p>
                     </div>
 
                     <div>
                         <h5>Phone</h5>
-                        <p>{{ object.phone|default:"-" }}</p>
+                        <p>{{ object.vendor.phone|default:"-" }}</p>
                     </div>
 
                     <div>
diff --git a/hypha/apply/projects/templates/application_projects/vendor_detail.html b/hypha/apply/projects/templates/application_projects/vendor_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..da54c5a6dab76f23bcf40d27d2851da7b0f56301
--- /dev/null
+++ b/hypha/apply/projects/templates/application_projects/vendor_detail.html
@@ -0,0 +1,48 @@
+{% extends "base-apply.html" %}
+{% load bleach_tags i18n approval_tools %}
+{% user_can_edit_project object request.user as editable %}
+{% block title %}{% trans "Vendor Information for" %} {{ project.title }} {% endblock %}
+
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <h2 class="heading heading--no-margin">{% trans "Vendor Information for" %} {{ project.title }}</h2>
+    </div>
+</div>
+
+<div class="grid">
+    <div>
+        <h5 class="vendor-info">Last Updated: {{ vendor.updated_at|date:'F d, Y' }}</h5>
+    </div>
+    {% if editable %}
+        <div>
+            <a class="link link--edit-vendor is-active" href="{% url 'apply:projects:vendor' pk=project.pk %}">
+                Edit
+            <svg class="icon icon--pen"><use xlink:href="#pen"></use></svg>
+            </a>
+        </div>
+    {% endif %}
+</div>
+
+<div class="rich-text rich-text--answers">
+    {% for group in vendor_detailed_response.values %}
+        {% if group.title %}
+            <h1>{{ group.title|bleach }}</h4>
+        {% endif %}
+        {% for question, answer in group.questions %}
+            <h5>{{ question }}</h5>
+            {% if question == 'Due Diligence Documents' %}
+                <div class="card card--solid">
+                    <div class="card__inner">
+                        {% for document in due_diligence_documents %}
+                            <p class="card__text"><a href="{% url "apply:projects:vendor-documents" pk=project.pk vendor_pk=project.vendor.pk file_pk=document.pk %}">{{ document.document.name }}</a></p>
+                        {% endfor %}
+                    </div>
+                </div>
+            {% else %}
+                <p>{% if answer == True or answer == False %}{{ answer|yesno:"Yes,No" }}{% else %}{% if answer %}{{ answer|bleach }}{% else %}-{% endif %}{% endif %}</p>
+            {% endif %}
+        {% endfor %}
+    {% endfor %}
+</div>
+{% endblock %}
diff --git a/hypha/apply/projects/templates/application_projects/vendor_form.html b/hypha/apply/projects/templates/application_projects/vendor_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..b10a294de961fec53b2b3176126d61532ae81744
--- /dev/null
+++ b/hypha/apply/projects/templates/application_projects/vendor_form.html
@@ -0,0 +1,40 @@
+{% extends "base-apply.html" %}
+{% load static i18n %}
+
+{% block title %}{% trans "Update Contractor Information" %}{% endblock %}
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <h2 class="heading heading--no-margin">{% trans "Update Contractor Information" %}</h2>
+    </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" id="multi-step-vendor" action="" method="post" enctype="multipart/form-data">
+            {{ wizard.management_form }}
+            {% csrf_token %}
+            {{ form.media }}
+            {% for field in form %}
+                {% if field.field %}
+                    {% include "forms/includes/field.html" %}
+                {% else %}
+                    {{ field }}
+                {% endif %}
+            {% endfor %}
+            <button class="button button--submit button--top-space button--primary" type="submit" name="save">{% if wizard.steps.step1 == 6 %}Submit{% else %}Save and continue{% endif %}</button>
+        </form>
+    </div>
+</div>
+{% endblock %}
+
+{% block extra_css %}
+    {{ form.media.css }}
+{% endblock %}
+
+{% block extra_js %}
+    {{ form.media.js }}
+    <script src="{% static 'js/apply/list-input-files.js' %}"></script>
+{% endblock %}
diff --git a/hypha/apply/projects/templates/application_projects/vendor_success.html b/hypha/apply/projects/templates/application_projects/vendor_success.html
new file mode 100644
index 0000000000000000000000000000000000000000..68c7f1692191f9b468dbdd2074b31f4702fd3c0b
--- /dev/null
+++ b/hypha/apply/projects/templates/application_projects/vendor_success.html
@@ -0,0 +1,24 @@
+{% extends "base-apply.html" %}
+{% load static i18n %}
+
+{% block title %}{% trans "Contractor Setup Completed" %}{% endblock %}
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner">
+        <h2 class="heading heading--no-margin">{% trans "Contractor Setup Completed" %}</h2>
+    </div>
+</div>
+<div class="wrapper wrapper--small">
+    <h3>Thank you for submitting your information.</h3>
+    <p>Your {{ ORG_SHORT_NAME }} Programme Manager will be in touch with you if there are any issues.</p>
+    <h3>What will happen next?</h3>
+    <p>We will use this information to create the contract for your project.</p>
+    <p>Once this has happened, we will send you a message with your contract.</p>
+    <p>We'll ask you to read it, and if everything is OK, you can sign it.</p>
+    <a href="{% url 'apply:projects:detail' pk=project.pk %}">{% trans "Visit Project Detail Page" %}</a>
+</div>
+{% endblock %}
+
+{% block extra_js %}
+<script src="{% static 'js/apply/list-input-files.js' %}"></script>
+{% endblock %}
diff --git a/hypha/apply/projects/tests/factories.py b/hypha/apply/projects/tests/factories.py
index 949cbdca401cf7c7f8e79093c2662475016fee1d..62ccf3fc653119ce1551ada118822b525867dac4 100644
--- a/hypha/apply/projects/tests/factories.py
+++ b/hypha/apply/projects/tests/factories.py
@@ -1,5 +1,4 @@
 import decimal
-import json
 
 import factory
 import pytz
@@ -77,10 +76,6 @@ class ProjectFactory(factory.DjangoModelFactory):
 
     title = factory.Sequence('name {}'.format)
     lead = factory.SubFactory(StaffFactory)
-    contact_legal_name = 'test'
-    contact_email = 'test@example.com'
-    contact_address = json.dumps(ADDRESS)
-    contact_phone = '555 1234'
     value = decimal.Decimal('100')
     proposed_start = factory.LazyFunction(timezone.now)
     proposed_end = factory.LazyFunction(timezone.now)
diff --git a/hypha/apply/projects/tests/test_forms.py b/hypha/apply/projects/tests/test_forms.py
index 8aa146e3d126025a35c28522b503a7e9f16e4138..c871bb74d0a90b74f4603354596bd745bc175a95 100644
--- a/hypha/apply/projects/tests/test_forms.py
+++ b/hypha/apply/projects/tests/test_forms.py
@@ -7,16 +7,18 @@ from django.test import TestCase, override_settings
 from hypha.apply.users.tests.factories import UserFactory
 
 from ..files import get_files
-from ..forms import (
+from ..forms.payment import (
     ChangePaymentRequestStatusForm,
     CreatePaymentRequestForm,
-    ProjectApprovalForm,
     SelectDocumentForm,
-    StaffUploadContractForm,
-    UploadContractForm,
     filter_choices,
     filter_request_choices,
 )
+from ..forms.project import (
+    ProjectApprovalForm,
+    StaffUploadContractForm,
+    UploadContractForm,
+)
 from ..models.payment import CHANGES_REQUESTED, DECLINED, PAID, SUBMITTED, UNDER_REVIEW
 from .factories import (
     DocumentCategoryFactory,
@@ -85,9 +87,6 @@ class TestProjectApprovalForm(TestCase):
 
         data = {
             'title': f'{project.title} test',
-            'contact_legal_name': project.contact_legal_name,
-            'contact_email': project.contact_email,
-            'contact_phone': project.contact_phone,
             'value': project.value,
             'proposed_start': project.proposed_start,
             'proposed_end': project.proposed_end,
diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py
index 02cb0ddd98a9f3e1c270eeb458e23bdfebe815e8..5c29058b0858ccad111d32b99aeec3a98a3fde3f 100644
--- a/hypha/apply/projects/tests/test_views.py
+++ b/hypha/apply/projects/tests/test_views.py
@@ -24,7 +24,7 @@ from ..files import get_files
 from ..forms import SetPendingForm
 from ..models.payment import CHANGES_REQUESTED, SUBMITTED
 from ..models.project import COMMITTED, CONTRACTING, IN_PROGRESS
-from ..views import ContractsMixin, ProjectDetailSimplifiedView
+from ..views.project import ContractsMixin, ProjectDetailSimplifiedView
 from .factories import (
     ContractFactory,
     DocumentCategoryFactory,
diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py
index 5e6784e6da160a5fe9d142f434427a7cfcb23a47..35b4a9eccfe1f9f9f1341faba814667947510b58 100644
--- a/hypha/apply/projects/urls.py
+++ b/hypha/apply/projects/urls.py
@@ -3,6 +3,7 @@ from django.urls import include, path
 from .views import (
     ContractPrivateMediaView,
     CreatePaymentRequestView,
+    CreateVendorView,
     DeletePaymentRequestView,
     EditPaymentRequestView,
     PaymentRequestListView,
@@ -20,6 +21,8 @@ from .views import (
     ReportPrivateMedia,
     ReportSkipView,
     ReportUpdateView,
+    VendorDetailView,
+    VendorPrivateMediaView,
 )
 
 app_name = 'projects'
@@ -35,6 +38,9 @@ urlpatterns = [
         path('download/', ProjectDetailPDFView.as_view(), name='download'),
         path('simplified/', ProjectDetailSimplifiedView.as_view(), name='simplified'),
         path('request/', CreatePaymentRequestView.as_view(), name='request'),
+        path('vendor/', CreateVendorView.as_view(), name='vendor'),
+        path('vendor/<int:vendor_pk>/', VendorDetailView.as_view(), name='vendor-detail'),
+        path('vendor/<int:vendor_pk>/documents/<int:file_pk>/', VendorPrivateMediaView.as_view(), name='vendor-documents'),
     ])),
     path('payment-requests/', include(([
         path('', PaymentRequestListView.as_view(), name='all'),
@@ -55,4 +61,13 @@ urlpatterns = [
             path('documents/<int:file_pk>/', ReportPrivateMedia.as_view(), name="document"),
         ])),
     ], 'reports'))),
+    path('venor/', include(([
+        path('', ReportListView.as_view(), name='all'),
+        path('<int:pk>/', include([
+            path('', ReportDetailView.as_view(), name='detail'),
+            path('skip/', ReportSkipView.as_view(), name='skip'),
+            path('edit/', ReportUpdateView.as_view(), name='edit'),
+            path('documents/<int:file_pk>/', ReportPrivateMedia.as_view(), name="document"),
+        ])),
+    ], 'reports'))),
 ]
diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py
index 940f89395e695b1bb720a89a1430f497e9257bf9..61a184d6d290b93f85e964e2d6820ecffb46a609 100644
--- a/hypha/apply/projects/views/__init__.py
+++ b/hypha/apply/projects/views/__init__.py
@@ -1,3 +1,87 @@
-from .payment import *  # NOQA
-from .project import *  # NOQA
-from .report import *  # NOQA
+from .payment import (
+    ChangePaymentRequestStatusView,
+    CreatePaymentRequestView,
+    DeletePaymentRequestView,
+    EditPaymentRequestView,
+    PaymentRequestAdminView,
+    PaymentRequestApplicantView,
+    PaymentRequestListView,
+    PaymentRequestPrivateMedia,
+    PaymentRequestView,
+)
+from .project import (
+    AdminProjectDetailView,
+    ApplicantProjectDetailView,
+    ApplicantProjectEditView,
+    ApproveContractView,
+    BaseProjectDetailView,
+    ContractPrivateMediaView,
+    CreateApprovalView,
+    ProjectApprovalEditView,
+    ProjectDetailPDFView,
+    ProjectDetailSimplifiedView,
+    ProjectDetailView,
+    ProjectEditView,
+    ProjectListView,
+    ProjectOverviewView,
+    ProjectPrivateMediaView,
+    RejectionView,
+    RemoveDocumentView,
+    SelectDocumentView,
+    SendForApprovalView,
+    UpdateLeadView,
+    UploadContractView,
+    UploadDocumentView,
+)
+from .report import (
+    ReportDetailView,
+    ReportFrequencyUpdate,
+    ReportListView,
+    ReportPrivateMedia,
+    ReportSkipView,
+    ReportUpdateView,
+)
+from .vendor import CreateVendorView, VendorDetailView, VendorPrivateMediaView
+
+__all__ = [
+    'ChangePaymentRequestStatusView',
+    'DeletePaymentRequestView',
+    'PaymentRequestAdminView',
+    'PaymentRequestApplicantView',
+    'PaymentRequestView',
+    'CreatePaymentRequestView',
+    'EditPaymentRequestView',
+    'PaymentRequestPrivateMedia',
+    'PaymentRequestListView',
+    'SendForApprovalView',
+    'CreateApprovalView',
+    'RejectionView',
+    'UploadDocumentView',
+    'RemoveDocumentView',
+    'SelectDocumentView',
+    'UpdateLeadView',
+    'ApproveContractView',
+    'UploadContractView',
+    'BaseProjectDetailView',
+    'AdminProjectDetailView',
+    'ApplicantProjectDetailView',
+    'ProjectDetailView',
+    'ProjectPrivateMediaView',
+    'ContractPrivateMediaView',
+    'ProjectDetailSimplifiedView',
+    'ProjectDetailPDFView',
+    'ProjectApprovalEditView',
+    'ApplicantProjectEditView',
+    'ProjectEditView',
+    'ProjectListView',
+    'ProjectOverviewView',
+    'ReportDetailView',
+    'ReportUpdateView',
+    'ReportPrivateMedia',
+    'ReportSkipView',
+    'ReportFrequencyUpdate',
+    'ReportListView',
+    'CreateVendorView',
+    'VendorDetailView',
+    'VendorPrivateMediaView',
+]
diff --git a/hypha/apply/projects/views/vendor.py b/hypha/apply/projects/views/vendor.py
new file mode 100644
index 0000000000000000000000000000000000000000..6087123d0fcf9ebab14fd6e3dc7d641dbf81e398
--- /dev/null
+++ b/hypha/apply/projects/views/vendor.py
@@ -0,0 +1,331 @@
+import json
+
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.core.exceptions import PermissionDenied
+from django.db.models.fields.files import FieldFile
+from django.http import Http404
+from django.shortcuts import get_object_or_404, render
+from django.utils import timezone
+from django.utils.decorators import method_decorator
+from django.utils.translation import gettext_lazy as _
+from django.views.generic.detail import DetailView
+from formtools.wizard.views import SessionWizardView
+from wagtail.core.models import Site
+
+from addressfield.fields import ADDRESS_FIELDS_ORDER
+from hypha.apply.activity.messaging import MESSAGES, messenger
+from hypha.apply.projects.models.vendor import VendorFormSettings
+from hypha.apply.utils.storage import PrivateMediaView, PrivateStorage
+
+from ..forms import (
+    CreateVendorFormStep1,
+    CreateVendorFormStep2,
+    CreateVendorFormStep3,
+    CreateVendorFormStep4,
+    CreateVendorFormStep5,
+    CreateVendorFormStep6,
+)
+from ..models import (
+    BankInformation,
+    DueDiligenceDocument,
+    Project,
+    ProjectSettings,
+    Vendor,
+)
+
+
+def show_extra_info_form(wizard):
+    # try to get the cleaned data of step where we ask if extra info is needed
+    cleaned_data = wizard.get_cleaned_data_for_step('extra') or {}
+    # check if the field ``extra_info`` box was checked.
+    return cleaned_data.get('need_extra_info', True)
+
+
+class CreateVendorAccessMixin:
+    def dispatch(self, request, *args, **kwargs):
+        project_settings = ProjectSettings.for_request(request)
+        if not project_settings.vendor_setup_required:
+            raise PermissionDenied
+        is_admin = request.user.is_apply_staff
+        project = self.get_project()
+        is_owner = request.user == project.user
+        if not (is_owner or is_admin):
+            raise PermissionDenied
+        if not project.editable_by(request.user):
+            raise PermissionDenied
+        if not project.vendor:
+            raise Http404
+        return super().dispatch(request, *args, **kwargs)
+
+
+class DetailVendorAccessMixin:
+    def dispatch(self, request, *args, **kwargs):
+        project_settings = ProjectSettings.for_request(request)
+        if not project_settings.vendor_setup_required:
+            raise PermissionDenied
+        is_admin = request.user.is_apply_staff
+        project = self.get_project()
+        is_owner = request.user == project.user
+        if not (is_owner or is_admin):
+            raise PermissionDenied
+        if not project.vendor:
+            raise Http404
+        return super().dispatch(request, *args, **kwargs)
+
+
+class CreateVendorView(CreateVendorAccessMixin, SessionWizardView):
+    file_storage = PrivateStorage()
+    form_list = [
+        ('basic', CreateVendorFormStep1),
+        ('taxes', CreateVendorFormStep2),
+        ('documents', CreateVendorFormStep3),
+        ('bank', CreateVendorFormStep4),
+        ('extra', CreateVendorFormStep5),
+        ('other', CreateVendorFormStep6),
+    ]
+    condition_dict = {'other': show_extra_info_form}
+    template_name = 'application_projects/vendor_form.html'
+
+    def get_project(self):
+        return get_object_or_404(Project, pk=self.kwargs['pk'])
+
+    def done(self, form_list, **kwargs):
+        vendor_project = self.get_project()
+        cleaned_data = self.get_all_cleaned_data()
+        vendor = vendor_project.vendor
+        need_extra_info = cleaned_data['need_extra_info']
+        bank_info = vendor.bank_info
+        account_holder_name = cleaned_data['account_holder_name']
+        account_routing_number = cleaned_data['account_routing_number']
+        account_number = cleaned_data['account_number']
+        account_currency = cleaned_data['account_currency']
+        if not bank_info:
+            bank_info = BankInformation.objects.create(
+                account_holder_name=account_holder_name,
+                account_number=account_number,
+                account_routing_number=account_routing_number,
+                account_currency=account_currency,
+                need_extra_info=need_extra_info,
+            )
+        else:
+            bank_info.account_holder_name = account_holder_name
+            bank_info.account_number = account_number
+            bank_info.account_currency = account_currency
+            bank_info.need_extra_info = need_extra_info
+        if need_extra_info:
+            ib_account_routing_number = cleaned_data['ib_account_routing_number']
+            ib_account_number = cleaned_data['ib_account_number']
+            ib_account_currency = cleaned_data['ib_account_currency']
+            ib_branch_address = cleaned_data['ib_branch_address']
+            iba_info = bank_info.iba_info
+            if not iba_info:
+                iba_info = BankInformation.objects.create(
+                    account_routing_number=ib_account_routing_number,
+                    account_number=ib_account_number,
+                    account_currency=ib_account_currency,
+                    branch_address=ib_branch_address
+                )
+            else:
+                iba_info.branch_address = ib_branch_address
+                iba_info.account_routing_number = ib_account_routing_number
+                iba_info.account_number = ib_account_number
+                iba_info.account_currency = ib_account_currency
+            iba_info.save()
+            bank_info.branch_address = cleaned_data['branch_address']
+            bank_info.nid_type = cleaned_data['nid_type']
+            bank_info.nid_number = cleaned_data['nid_number']
+            bank_info.iba_info = iba_info
+            vendor.other_info = cleaned_data['other_info']
+
+        bank_info.save()
+
+        vendor.bank_info = bank_info
+        vendor.name = cleaned_data['name']
+        vendor.contractor_name = cleaned_data['contractor_name']
+        vendor.type = cleaned_data['type']
+        vendor.required_to_pay_taxes = cleaned_data['required_to_pay_taxes']
+        vendor.updated_at = timezone.now()
+        vendor.user_has_updated_details = True
+        vendor.save()
+
+        not_deleted_original_filenames = [
+            file['name'] for file in json.loads(cleaned_data['due_diligence_documents-uploads'])
+        ]
+        for f in vendor.due_diligence_documents.all():
+            if f.document.name not in not_deleted_original_filenames:
+                f.document.delete()
+                f.delete()
+
+        for f in cleaned_data["due_diligence_documents"]:
+            if not isinstance(f, FieldFile):
+                try:
+                    DueDiligenceDocument.objects.create(vendor=vendor, document=f)
+                finally:
+                    f.close()
+        form = self.get_form('documents')
+        form.delete_temporary_files()
+
+        messenger(
+            MESSAGES.UPDATED_VENDOR,
+            request=self.request,
+            user=vendor_project.lead,
+            source=vendor_project
+        )
+
+        return render(self.request, 'application_projects/vendor_success.html', {'project': vendor_project})
+
+    def get_form_initial(self, step):
+        vendor_project = self.get_project()
+        vendor = vendor_project.vendor
+        initial_dict = self.initial_dict.get(step, {})
+        if vendor:
+            initial_dict['basic'] = {
+                'name': vendor.name,
+                'contractor_name': vendor.contractor_name,
+                'type': vendor.type
+            }
+            initial_dict['taxes'] = {
+                'required_to_pay_taxes': vendor.required_to_pay_taxes
+            }
+            initial_dict['documents'] = {
+                'due_diligence_documents': [
+                    f.document for f in vendor.due_diligence_documents.all()
+                ]
+            }
+            bank_info = vendor.bank_info
+            if bank_info:
+                initial_dict['bank'] = {
+                    'account_holder_name': bank_info.account_holder_name,
+                    'account_routing_number': bank_info.account_routing_number,
+                    'account_number': bank_info.account_number,
+                    'account_currency': bank_info.account_currency,
+                }
+                initial_dict['extra'] = {
+                    'need_extra_info': bank_info.need_extra_info
+                }
+                initial_dict['other'] = {
+                    'branch_address': bank_info.branch_address,
+                    'nid_type': bank_info.nid_type,
+                    'nid_number': bank_info.nid_number,
+                    'other_info': vendor.other_info,
+                }
+                iba_info = bank_info.iba_info
+                if iba_info:
+                    initial_dict['other']['ib_account_routing_number'] = iba_info.account_routing_number
+                    initial_dict['other']['ib_account_number'] = iba_info.account_number
+                    initial_dict['other']['ib_account_currency'] = iba_info.account_currency
+                    initial_dict['other']['ib_branch_address'] = iba_info.branch_address
+        return initial_dict.get(step, {})
+
+    def get_form_kwargs(self, step):
+        kwargs = super(CreateVendorView, self).get_form_kwargs(step)
+        kwargs['site'] = Site.find_for_request(self.request)
+        return kwargs
+
+
+class VendorDetailView(DetailVendorAccessMixin, DetailView):
+    model = Vendor
+    template_name = 'application_projects/vendor_detail.html'
+
+    def get_object(self, queryset=None):
+        return get_object_or_404(self.model, id=self.kwargs['vendor_pk'])
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['vendor_detailed_response'] = self.get_detailed_response()
+        context['project'] = self.get_project()
+        vendor = self.get_object()
+        context['due_diligence_documents'] = vendor.due_diligence_documents.all()
+        return context
+
+    def get_project(self):
+        return get_object_or_404(Project, pk=self.kwargs['pk'])
+
+    def get_detailed_response(self):
+        vendor = self.get_object()
+        vendor_form_settings = VendorFormSettings.for_request(self.request)
+        data = {}
+        group = 0
+        data.setdefault(group, {'title': str(_('Vendor Information')), 'questions': list()})
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'name_label'), vendor.name),
+            (getattr(vendor_form_settings, 'contractor_name_label'), vendor.contractor_name),
+            (getattr(vendor_form_settings, 'type_label'), vendor.type),
+            (getattr(vendor_form_settings, 'required_to_pay_taxes_label'), vendor.required_to_pay_taxes),
+            ('Due Diligence Documents', ''),
+        ]
+        group = group + 1
+        data.setdefault(group, {'title': str(_('Bank Account Information')), 'questions': list()})
+        bank_info = vendor.bank_info
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'account_holder_name_label'), bank_info.account_holder_name if bank_info else ''),
+            (getattr(vendor_form_settings, 'account_routing_number_label'), bank_info.account_routing_number if bank_info else ''),
+            (getattr(vendor_form_settings, 'account_number_label'), bank_info.account_number if bank_info else ''),
+            (getattr(vendor_form_settings, 'account_currency_label'), bank_info.account_currency if bank_info else ''),
+        ]
+        group = group + 1
+        data.setdefault(group, {'title': str(_('(Optional) Extra Information for Accepting Payments')), 'questions': list()})
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'branch_address_label'), self.get_address_display(bank_info.branch_address) if bank_info else ''),
+        ]
+        group = group + 1
+        data.setdefault(group, {'title': str(_('Intermediary Bank Account Information')), 'questions': list()})
+        iba_info = bank_info.iba_info if bank_info else None
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'ib_account_routing_number_label'), iba_info.account_routing_number if iba_info else ''),
+            (getattr(vendor_form_settings, 'ib_account_number_label'), iba_info.account_number if iba_info else ''),
+            (getattr(vendor_form_settings, 'ib_account_currency_label'), iba_info.account_currency if iba_info else ''),
+            (getattr(vendor_form_settings, 'ib_branch_address_label'), self.get_address_display(iba_info.branch_address) if iba_info else ''),
+        ]
+        group = group + 1
+        data.setdefault(group, {'title': str(_('Account Holder National Identity Document Information')), 'questions': list()})
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'nid_type_label'), bank_info.nid_type if bank_info else ''),
+            (getattr(vendor_form_settings, 'nid_number_label'), bank_info.nid_number if bank_info else ''),
+        ]
+        group = group + 1
+        data.setdefault(group, {'title': None, 'questions': list()})
+        data[group]['questions'] = [
+            (getattr(vendor_form_settings, 'other_info_label'), vendor.other_info),
+        ]
+        return data
+
+    def get_address_display(self, address):
+        try:
+            address = json.loads(address)
+        except (json.JSONDecodeError, AttributeError):
+            return ''
+        else:
+            return ', '.join(
+                address.get(field)
+                for field in ADDRESS_FIELDS_ORDER
+                if address.get(field)
+            )
+
+
+@method_decorator(login_required, name='dispatch')
+class VendorPrivateMediaView(UserPassesTestMixin, PrivateMediaView):
+    raise_exception = True
+
+    def dispatch(self, *args, **kwargs):
+        pk = self.kwargs['pk']
+        vendor_pk = self.kwargs['vendor_pk']
+        self.vendor = get_object_or_404(Vendor, pk=vendor_pk)
+        self.project = get_object_or_404(Project, pk=pk)
+
+        return super().dispatch(*args, **kwargs)
+
+    def get_media(self, *args, **kwargs):
+        file_pk = kwargs.get('file_pk')
+        document = get_object_or_404(self.vendor.due_diligence_documents, pk=file_pk)
+        return document.document
+
+    def test_func(self):
+        if self.request.user.is_apply_staff:
+            return True
+
+        if self.request.user == self.project.user:
+            return True
+
+        return False
diff --git a/hypha/apply/templates/forms/includes/field.html b/hypha/apply/templates/forms/includes/field.html
index 905df0394ea3b35e176afa9d63795829bc36daf9..4d69a78e767211a5b7b86b9e9c4dfa94c23e5c7f 100644
--- a/hypha/apply/templates/forms/includes/field.html
+++ b/hypha/apply/templates/forms/includes/field.html
@@ -1,5 +1,4 @@
 {% load util_tags %}
-
 {% with widget_type=field|widget_type field_type=field|field_type %}
 
 <div class="form__group {% if widget_type == 'checkbox_input' %} form__group--checkbox{% endif %}{% if widget_type == 'clearable_file_input' or widget_type == 'multi_file_input' or widget_type == 'single_file_field_widget' or widget_type == 'multi_file_field_widget' %} form__group--file{% endif %}{% if field.help_text %} form__group--wrap{% endif %}{% if field.errors %} form__error{% endif %}{% if is_application and field.field.group_number > 1 %} field-group field-group-{{ field.field.group_number }}{% endif %}{% if is_application and field.field.grouper_for %} form-fields-grouper{% endif %}"{% if is_application and field.field.grouper_for %}data-grouper-for="{{ field.field.grouper_for }}" data-toggle-on="{{ field.field.choices.0.0 }}" data-toggle-off="{{ field.field.choices.1.0 }}"{% endif %}{% if is_application and field.field.group_number > 1 %} data-hidden="{% if not show_all_group_fields and not field.field.visible %}true{% else %}false{% endif %}" data-required="{{ field.field.required_when_visible }}"{% endif %}{% if field.field.word_limit %} data-word-limit="{{ field.field.word_limit }}"{% endif %}>
diff --git a/hypha/locale/en/LC_MESSAGES/django.po b/hypha/locale/en/LC_MESSAGES/django.po
index 9eb83c22ac2fba9809fd9e528a893579cf30f73b..0694fc5b73a32170269c6d9c501c5bd5d3e37aee 100644
--- a/hypha/locale/en/LC_MESSAGES/django.po
+++ b/hypha/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-06-15 12:20+0000\n"
+"POT-Creation-Date: 2021-07-08 06:43+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -320,7 +320,7 @@ msgstr ""
 msgid "{user} has batch changed lead to {new_lead} on: {submissions_text}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:532 hypha/apply/activity/messaging.py:909
+#: hypha/apply/activity/messaging.py:532 hypha/apply/activity/messaging.py:947
 #, python-brace-format
 msgid "{user} as {name},"
 msgstr ""
@@ -338,14 +338,16 @@ msgstr ""
 
 #: hypha/apply/activity/messaging.py:565
 #, python-brace-format
-msgid "A determination for <{link}|{submission_title}> was sent by email. "
-"Outcome: {determination_outcome}"
+msgid ""
+"A determination for <{link}|{submission_title}> was sent by email. Outcome: "
+"{determination_outcome}"
 msgstr ""
 
 #: hypha/apply/activity/messaging.py:572
 #, python-brace-format
-msgid "A determination for <{link}|{submission_title}> was saved without sending an email. "
-"Outcome: {determination_outcome}"
+msgid ""
+"A determination for <{link}|{submission_title}> was saved without sending an "
+"email. Outcome: {determination_outcome}"
 msgstr ""
 
 #: hypha/apply/activity/messaging.py:589
@@ -365,69 +367,69 @@ msgid ""
 "reviewers: {reviewers}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:723
+#: hypha/apply/activity/messaging.py:725
 #, python-brace-format
 msgid "Application ready to review: {source.title}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:725
+#: hypha/apply/activity/messaging.py:727
 msgid "Multiple applications are now ready for your review"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:727
+#: hypha/apply/activity/messaging.py:729
 #, python-brace-format
 msgid "Reminder: Application ready to review: {source.title}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:730
+#: hypha/apply/activity/messaging.py:732
 #, python-brace-format
 msgid "Your application to {org_long_name}: {source.title}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:732
+#: hypha/apply/activity/messaging.py:734
 #, python-brace-format
 msgid "Your {org_long_name} Project: {source.title}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:917
+#: hypha/apply/activity/messaging.py:937
 msgid "Successfully uploaded document"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:918
+#: hypha/apply/activity/messaging.py:938
 msgid "Successfully removed document"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:921
+#: hypha/apply/activity/messaging.py:941
 msgid "Reminder created"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:922
+#: hypha/apply/activity/messaging.py:942
 msgid "Reminder deleted"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:933
+#: hypha/apply/activity/messaging.py:953
 #, python-brace-format
 msgid "Batch reviewers added: {reviewers_text} to "
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:938
+#: hypha/apply/activity/messaging.py:958
 #, python-brace-format
 msgid ""
 "Successfully updated reporting frequency. They will now report "
 "{new_schedule} starting on {schedule_start}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:942
+#: hypha/apply/activity/messaging.py:962
 #, python-brace-format
 msgid "Successfully skipped a Report for {start_date} to {end_date}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:944
+#: hypha/apply/activity/messaging.py:964
 #, python-brace-format
 msgid "Successfully unskipped a Report for {start_date} to {end_date}"
 msgstr ""
 
-#: hypha/apply/activity/messaging.py:963
+#: hypha/apply/activity/messaging.py:983
 #, python-brace-format
 msgid "Successfully determined as {outcome}: "
 msgstr ""
@@ -460,11 +462,16 @@ msgid ""
 "inquiries, please email us at %(ORG_EMAIL)s"
 msgstr ""
 
-#: hypha/apply/activity/templates/messages/email/base.html:6
-msgid "Kind Regards,"
+#: hypha/apply/activity/templates/messages/email/base.html:2
+#, python-format
+msgid "Dear %(user)s,"
 msgstr ""
 
 #: hypha/apply/activity/templates/messages/email/base.html:7
+msgid "Kind Regards,"
+msgstr ""
+
+#: hypha/apply/activity/templates/messages/email/base.html:8
 #, python-format
 msgid "The %(ORG_SHORT_NAME)s Team"
 msgstr ""
@@ -889,11 +896,13 @@ msgstr ""
 msgid "Draft"
 msgstr ""
 
-#: hypha/apply/determinations/models.py:107 hypha/apply/review/models.py:163
+#: hypha/apply/determinations/models.py:107
+#: hypha/apply/projects/models/vendor.py:48 hypha/apply/review/models.py:163
 msgid "Creation time"
 msgstr ""
 
-#: hypha/apply/determinations/models.py:108 hypha/apply/review/models.py:164
+#: hypha/apply/determinations/models.py:108
+#: hypha/apply/projects/models/vendor.py:49 hypha/apply/review/models.py:164
 msgid "Update time"
 msgstr ""
 
@@ -1005,7 +1014,8 @@ msgstr ""
 msgid "Requested amount"
 msgstr ""
 
-#: hypha/apply/funds/blocks.py:65 hypha/apply/projects/models/project.py:137
+#: hypha/apply/funds/blocks.py:65 hypha/apply/projects/models/vendor.py:19
+#: hypha/apply/projects/models/vendor.py:56
 msgid "Address"
 msgstr ""
 
@@ -1021,7 +1031,7 @@ msgid "Take action"
 msgstr ""
 
 #: hypha/apply/funds/forms.py:143 hypha/apply/projects/filters.py:28
-#: hypha/apply/projects/filters.py:42 hypha/apply/projects/tables.py:46
+#: hypha/apply/projects/filters.py:42 hypha/apply/projects/tables.py:49
 msgid "Lead"
 msgstr ""
 
@@ -1047,7 +1057,7 @@ msgid "Meta terms are hierarchical in nature."
 msgstr ""
 
 #: hypha/apply/funds/models/__init__.py:17 hypha/apply/funds/tables.py:74
-#: hypha/apply/projects/tables.py:45 hypha/apply/projects/tables.py:76
+#: hypha/apply/projects/tables.py:48 hypha/apply/projects/tables.py:80
 msgid "Fund"
 msgstr ""
 
@@ -1166,12 +1176,12 @@ msgstr ""
 msgid "Confirmation email"
 msgstr ""
 
-#: hypha/apply/funds/tables.py:71 hypha/apply/projects/tables.py:20
+#: hypha/apply/funds/tables.py:71 hypha/apply/projects/tables.py:21
 msgid "Submitted"
 msgstr ""
 
 #: hypha/apply/funds/tables.py:72 hypha/apply/projects/filters.py:27
-#: hypha/apply/projects/filters.py:43 hypha/apply/projects/tables.py:75
+#: hypha/apply/projects/filters.py:43 hypha/apply/projects/tables.py:79
 #: hypha/apply/users/templates/wagtailusers/users/list.html:23
 #: hypha/public/partner/tables.py:51 hypha/public/partner/tables.py:97
 msgid "Status"
@@ -1307,7 +1317,7 @@ msgid "next"
 msgstr ""
 
 #: hypha/apply/funds/views.py:230 hypha/apply/funds/views.py:262
-#: hypha/apply/funds/views.py:283 hypha/apply/projects/views/project.py:315
+#: hypha/apply/funds/views.py:283 hypha/apply/projects/views/project.py:319
 msgid "Sorry something went wrong"
 msgstr ""
 
@@ -1557,140 +1567,110 @@ msgstr ""
 msgid "Reporting Period"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:54
-msgid "Something changed before your approval please re-review"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:58
-msgid "The contract you were trying to approve has already been approved"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:61
-msgid "You can only approve a signed contract"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:100
-msgid "You can only set a value when moving to the Paid status."
+#: hypha/apply/projects/forms/report.py:127
+msgid "Cannot start a schedule before the current reporting period"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:138
-msgid "Cannot approve for a different user"
+#: hypha/apply/projects/forms/report.py:133
+msgid "Cannot start a schedule in the past"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:213
-#, python-brace-format
-msgid "Requested Value ({currency})"
+#: hypha/apply/projects/forms/report.py:139
+msgid "Cannot start a schedule beyond the end date"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:226
-msgid "Date From must be before Date To"
+#: hypha/apply/projects/models/project.py:147
+msgid "Proposed Start Date"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:252
-msgid "Receipts"
+#: hypha/apply/projects/models/project.py:148
+msgid "Proposed End Date"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:282
-msgid "Document"
+#: hypha/apply/projects/models/project.py:258
+msgid "Proposed End Date must be after Proposed Start Date"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:306
-msgid "File not found on submission"
+#: hypha/apply/projects/models/vendor.py:45
+msgid "Yes, the account belongs to the organisation above"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:324
-msgid "A Project can only be sent for Approval when Committed."
+#: hypha/apply/projects/models/vendor.py:46
+msgid "No, it is a personal bank account"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:327
-msgid "A Project can only be sent for Approval once"
+#: hypha/apply/projects/tables.py:16
+msgid "Invoice reference"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:370
+#: hypha/apply/projects/tables.py:22
 #, python-brace-format
-msgid "Update lead from {lead} to"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:380
-msgid "This section of the report will be shared with the broader community."
+msgid "Value ({currency})"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:383
-msgid "This section of the report will be shared with staff only."
-msgstr ""
-
-#: hypha/apply/projects/forms.py:389
-msgid "Files"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:416
-msgid "Must include either public or private content when submitting a report."
-msgstr ""
-
-#: hypha/apply/projects/forms.py:464
-msgid "Starting on:"
-msgstr ""
-
-#: hypha/apply/projects/forms.py:491
-msgid "Cannot start a schedule before the current reporting period"
+#: hypha/apply/projects/tables.py:29
+msgid "Period Start"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:497
-msgid "Cannot start a schedule in the past"
+#: hypha/apply/projects/tables.py:30
+msgid "Period End"
 msgstr ""
 
-#: hypha/apply/projects/forms.py:503
-msgid "Cannot start a schedule beyond the end date"
+#: hypha/apply/projects/tables.py:81
+msgid "Reporting"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:135
-msgid "Person or Organisation name"
+#: hypha/apply/projects/tables.py:83
+msgid "End Date"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:136
-#: hypha/apply/stream_forms/blocks.py:119
-msgid "Email"
+#: hypha/apply/projects/tables.py:84
+#, python-brace-format
+msgid "Fund Allocation ({currency})"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:138
-msgid "Phone"
+#: hypha/apply/projects/templates/application_projects/vendor_detail.html:4
+#: hypha/apply/projects/templates/application_projects/vendor_detail.html:9
+msgid "Vendor Information for"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:145
-msgid "Proposed Start Date"
+#: hypha/apply/projects/templates/application_projects/vendor_form.html:4
+#: hypha/apply/projects/templates/application_projects/vendor_form.html:8
+msgid "Update Contractor Information"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:146
-msgid "Proposed End Date"
+#: hypha/apply/projects/templates/application_projects/vendor_success.html:4
+#: hypha/apply/projects/templates/application_projects/vendor_success.html:8
+msgid "Contractor Setup Completed"
 msgstr ""
 
-#: hypha/apply/projects/models/project.py:253
-msgid "Proposed End Date must be after Proposed Start Date"
+#: hypha/apply/projects/templates/application_projects/vendor_success.html:18
+msgid "Visit Project Detail Page"
 msgstr ""
 
-#: hypha/apply/projects/tables.py:27
-msgid "Period Start"
+#: hypha/apply/projects/views/project.py:589
+#: hypha/apply/projects/views/project.py:636
+msgid "You are not allowed to edit the project at this time"
 msgstr ""
 
-#: hypha/apply/projects/tables.py:28
-msgid "Period End"
+#: hypha/apply/projects/views/vendor.py:250
+msgid "Vendor Information"
 msgstr ""
 
-#: hypha/apply/projects/tables.py:77
-msgid "Reporting"
+#: hypha/apply/projects/views/vendor.py:259
+msgid "Bank Account Information"
 msgstr ""
 
-#: hypha/apply/projects/tables.py:79
-msgid "End Date"
+#: hypha/apply/projects/views/vendor.py:268
+msgid "(Optional) Extra Information for Accepting Payments"
 msgstr ""
 
-#: hypha/apply/projects/tables.py:80
-msgid "Fund Allocation"
+#: hypha/apply/projects/views/vendor.py:273
+msgid "Intermediary Bank Account Information"
 msgstr ""
 
-#: hypha/apply/projects/views/project.py:584
-#: hypha/apply/projects/views/project.py:631
-msgid "You are not allowed to edit the project at this time"
+#: hypha/apply/projects/views/vendor.py:282
+msgid "Account Holder National Identity Document Information"
 msgstr ""
 
 #: hypha/apply/review/blocks.py:34
@@ -1722,6 +1702,10 @@ msgstr ""
 msgid "Required"
 msgstr ""
 
+#: hypha/apply/stream_forms/blocks.py:119
+msgid "Email"
+msgstr ""
+
 #: hypha/apply/stream_forms/blocks.py:120
 msgid "URL"
 msgstr ""
@@ -1843,19 +1827,19 @@ msgstr ""
 msgid "Only includes active, non-superusers"
 msgstr ""
 
-#: hypha/apply/users/models.py:90
+#: hypha/apply/users/models.py:94
 msgid "email address"
 msgstr ""
 
-#: hypha/apply/users/models.py:91
+#: hypha/apply/users/models.py:95
 msgid "Full name"
 msgstr ""
 
-#: hypha/apply/users/models.py:93
+#: hypha/apply/users/models.py:97
 msgid "Slack name"
 msgstr ""
 
-#: hypha/apply/users/models.py:95
+#: hypha/apply/users/models.py:99
 msgid "This is the name we should \"@mention\" when sending notifications"
 msgstr ""
 
@@ -2145,7 +2129,7 @@ msgstr ""
 msgid "Page size of downloadable Project and Submission PDFs"
 msgstr ""
 
-#: hypha/apply/utils/views.py:248
+#: hypha/apply/utils/views.py:254
 #, python-brace-format
 msgid "Page '{0}' can't be deleted because is in use in '{1}'."
 msgstr ""
diff --git a/hypha/settings/base.py b/hypha/settings/base.py
index 39444be49185064755704e969bf1fd27216b57f0..167fa237045879cc29808028285f4ba964cf69b9 100644
--- a/hypha/settings/base.py
+++ b/hypha/settings/base.py
@@ -152,6 +152,7 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.sitemaps',
     'django.forms',
+    'formtools',
 ]
 
 MIDDLEWARE = [
diff --git a/hypha/static_src/src/javascript/apply/file-uploads.js b/hypha/static_src/src/javascript/apply/file-uploads.js
index 0f22571980bf8ddc0ad106ba8ce9fb5ba32ebbee..34750a5293228b752fc611dcc15e62ecf6f3eb1a 100644
--- a/hypha/static_src/src/javascript/apply/file-uploads.js
+++ b/hypha/static_src/src/javascript/apply/file-uploads.js
@@ -9,6 +9,10 @@ jQuery(function ($) {
             init(form);
             form.initUploadFieldsDone = true;
         }
+        if (!form.initUploadFieldsDone && form.querySelector('[name=create_vendor_view-current_step]')) {
+            initWizard(form);
+            form.initUploadFieldsDone = true;
+        }
     });
 
     function init(form) {
@@ -18,4 +22,18 @@ jQuery(function ($) {
         $('input[type=hidden]').closest('.form__group').hide();
     }
 
+    // Initilise multi-step wizard forms
+    function initWizard(form) {
+        const step = form.querySelector('[name=create_vendor_view-current_step]').value;
+        if (step === 'documents') {
+            window.initUploadFields(
+                form,
+                {
+                    prefix: 'documents'
+                }
+            );
+            // Hide wrapper elements for hidden inputs added by django-file-form
+            $('input[type=hidden]').closest('.form__group').hide();
+        }
+    }
 });