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..c6a28ea673eb34cad03abbcc71274c00089e69b5
--- /dev/null
+++ b/hypha/apply/projects/forms/__init__.py
@@ -0,0 +1,46 @@
+from .payment import (
+    ChangePaymentRequestStatusForm,
+    CreatePaymentRequestForm,
+    EditPaymentRequestForm,
+    SelectDocumentForm,
+)
+from .project import (
+    ApproveContractForm,
+    CreateProjectForm,
+    CreateApprovalForm,
+    CreateApprovalForm,
+    ProjectEditForm,
+    ProjectApprovalForm,
+    RejectionForm,
+    RemoveDocumentForm,
+    SetPendingForm,
+    UploadContractForm,
+    StaffUploadContractForm,
+    UploadDocumentForm,
+    UpdateProjectLeadForm,
+)
+from .report import (
+    ReportEditForm,
+    ReportFrequencyForm,
+)
+
+__all__ = [
+    'ChangePaymentRequestStatusForm',
+    'CreatePaymentRequestForm',
+    'EditPaymentRequestForm',
+    'SelectDocumentForm',
+    'ApproveContractForm',
+    'CreateProjectForm',
+    'CreateApprovalForm',
+    'ProjectEditForm',
+    'ProjectApprovalForm',
+    'RejectionForm',
+    'RemoveDocumentForm',
+    'SetPendingForm',
+    'UploadContractForm',
+    'StaffUploadContractForm',
+    'UploadDocumentForm',
+    'UpdateProjectLeadForm',
+    'ReportEditForm',
+    'ReportFrequencyForm',
+]
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..e229aa7eb00ddfe732455adce5f02e5e9e89e035
--- /dev/null
+++ b/hypha/apply/projects/forms/project.py
@@ -0,0 +1,203 @@
+from django import forms
+from django.contrib.auth import get_user_model
+from django.db.models import Q
+
+from addressfield.fields import AddressField
+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):
+    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 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/urls.py b/hypha/apply/projects/urls.py
index 5e6784e6da160a5fe9d142f434427a7cfcb23a47..d6b311773922811161a4a53d8b4e3d4286acb22c 100644
--- a/hypha/apply/projects/urls.py
+++ b/hypha/apply/projects/urls.py
@@ -1,3 +1,4 @@
+from os import name
 from django.urls import include, path
 
 from .views import (
@@ -20,6 +21,7 @@ from .views import (
     ReportPrivateMedia,
     ReportSkipView,
     ReportUpdateView,
+    # CreateVendorView,
 )
 
 app_name = 'projects'
@@ -35,6 +37,7 @@ 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('payment-requests/', include(([
         path('', PaymentRequestListView.as_view(), name='all'),
@@ -55,4 +58,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'))),
 ]