diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index 9b75edf69d4fed280d27270d65480fb0eb9bc6df..3917813c4c54b8bdcfab2b006eec9db1976190a1 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -17,6 +17,7 @@ from .project import ( RemoveContractDocumentForm, RemoveDocumentForm, SetPendingForm, + SkipPAFApprovalProcessForm, StaffUploadContractForm, SubmitContractDocumentsForm, UpdateProjectLeadForm, @@ -37,6 +38,7 @@ from .vendor import ( __all__ = [ "SelectDocumentForm", "SubmitContractDocumentsForm", + "SkipPAFApprovalProcessForm", "ApproveContractForm", "ApproversForm", "AssignApproversForm", diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index 55551654eeed55b02d10906a1b51d6b9285a4d75..b7923a8b1eae69e8fa3dbf3db19c2d20eeb51541 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -359,6 +359,16 @@ class SubmitContractDocumentsForm(forms.ModelForm): super().__init__(*args, **kwargs) +class SkipPAFApprovalProcessForm(forms.ModelForm): + class Meta: + fields = ["id"] + model = Project + widgets = {"id": forms.HiddenInput()} + + def __init__(self, user=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + class UploadContractForm(FileFormMixin, forms.ModelForm): file = SingleFileField(label=_("Contract"), required=True) diff --git a/hypha/apply/projects/permissions.py b/hypha/apply/projects/permissions.py index dcd32dc04506297a460409328ff2be061dc4620d..7dd2a9e8dfdf410f043aee7bf4400001b3b1320d 100644 --- a/hypha/apply/projects/permissions.py +++ b/hypha/apply/projects/permissions.py @@ -12,6 +12,7 @@ from .models.project import ( INVOICING_AND_REPORTING, ProjectSettings, ) +from .utils import no_pafreviewer_role def has_permission(action, user, object=None, raise_exception=True, **kwargs): @@ -249,14 +250,18 @@ def can_update_paf_status(user, project, **kwargs): def can_update_project_status(user, project, **kwargs): - if project.status not in [COMPLETE, CLOSING, INVOICING_AND_REPORTING]: + if project.status not in [DRAFT, COMPLETE, CLOSING, INVOICING_AND_REPORTING]: return False, "Forbidden Error" if not user.is_authenticated: return False, "Login Required" if user.is_apply_staff or user.is_apply_staff_admin: - return True, "Staff and Staff Admin can update status" + if project.status == DRAFT: + if no_pafreviewer_role(): + return True, "Staff and Staff Admin can skip the PAF approval process" + else: + return True, "Staff and Staff Admin can update status" return False, "Forbidden Error" diff --git a/hypha/apply/projects/templates/application_projects/project_admin_detail.html b/hypha/apply/projects/templates/application_projects/project_admin_detail.html index 15cf645736032738b7be078e13de84192905f7f0..5e3bc0f180d1fa100ebf1dcb61760f1dc80f0589 100644 --- a/hypha/apply/projects/templates/application_projects/project_admin_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_admin_detail.html @@ -4,14 +4,24 @@ {% block admin_assignments %} - {% if object.is_in_progress %} + {% user_can_update_project_status object user as can_update_status %} + {% if can_update_status %} <div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions {% if mobile %}sidebar__inner--mobile{% endif %}"> <h5>{% trans "Actions to take" %}</h5> - {% user_can_update_project_status project user as can_update_status %} + {% user_can_skip_pafapproval_process project user as can_skip_paf %} + {% if can_skip_paf %} + <a data-fancybox data-src="#continue-to-next-phase" class="button button--white button--full-width button--bottom-space" href="#">{% trans "Continue to next status" %}</a> + + <div class="modal" id="continue-to-next-phase"> + <h4 class="modal__project-header-bar">{% trans "Continue to next stage" %}</h4> + <p>{% trans "Please ensure the Project Form is completed and you are ready to proceed to the next stage. This action cannot be reverted." %}</p> + {% trans "Continue" as submit %} + {% include 'funds/includes/delegated_form_base.html' with form=skip_paf_approval_form value=submit %} + </div> + {% else %} <!-- Move the condition below to link if add more than one link to 'More Actions'--> - {% if can_update_status %} <details> <summary class="sidebar__separator sidebar__separator--medium">{% trans "More actions" %}</summary> diff --git a/hypha/apply/projects/templatetags/approval_tools.py b/hypha/apply/projects/templatetags/approval_tools.py index 2038404515ec0e829ed6e3e933e12bb1b98c5774..00db39432012fa111708080e5603117efe38ef81 100644 --- a/hypha/apply/projects/templatetags/approval_tools.py +++ b/hypha/apply/projects/templatetags/approval_tools.py @@ -3,6 +3,7 @@ from datetime import timedelta from django import template from ..permissions import has_permission +from ..utils import no_pafreviewer_role register = template.Library() @@ -19,7 +20,11 @@ def user_has_approved(project, user): @register.simple_tag def user_can_send_for_approval(project, user): - return user.is_apply_staff and project.can_send_for_approval + return ( + user.is_apply_staff + and project.can_send_for_approval + and not (no_pafreviewer_role()) + ) @register.simple_tag diff --git a/hypha/apply/projects/templatetags/project_tags.py b/hypha/apply/projects/templatetags/project_tags.py index 3d91c085e8a57261f668e8b06cdb881ca389d314..85032cbbf31c07ae6fc27b1f8e17525fa5160bac 100644 --- a/hypha/apply/projects/templatetags/project_tags.py +++ b/hypha/apply/projects/templatetags/project_tags.py @@ -13,7 +13,7 @@ from hypha.apply.projects.models.project import ( INVOICING_AND_REPORTING, ) from hypha.apply.projects.permissions import has_permission -from hypha.apply.projects.utils import get_project_public_status +from hypha.apply.projects.utils import get_project_public_status, no_pafreviewer_role register = template.Library() @@ -25,6 +25,13 @@ def project_can_have_report(project): return False +@register.simple_tag +def user_can_skip_pafapproval_process(project, user): + if project.status == DRAFT and (user.is_apply_staff or user.is_apply_staff_admin): + return no_pafreviewer_role() + return False + + @register.simple_tag def user_next_step_on_project(project, user, request=None): from hypha.apply.projects.models.project import PAFReviewersRole, ProjectSettings @@ -36,15 +43,21 @@ def user_next_step_on_project(project, user, request=None): "heading": _("To do"), "text": _("Fill in the Project Form"), } - if project.paf_approvals.exists(): + if no_pafreviewer_role(): return { "heading": _("To do"), - "text": _("Resubmit project documents for approval"), + "text": _("Move project to next stage"), + } + else: + if project.paf_approvals.exists(): + return { + "heading": _("To do"), + "text": _("Resubmit project documents for approval"), + } + return { + "heading": _("To do"), + "text": _("Submit project documents for approval"), } - return { - "heading": _("To do"), - "text": _("Submit project documents for approval"), - } elif user.is_applicant: return { "heading": _("Waiting for"), diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py index 429e71e19ef1e6a09fce14ca9cf836bb14e420ea..c3b885ebb5d9e1a0b341e0c3f199253884008bee 100644 --- a/hypha/apply/projects/tests/test_views.py +++ b/hypha/apply/projects/tests/test_views.py @@ -289,6 +289,16 @@ class TestStaffProjectDetailView(BaseProjectDetailTestCase): class TestFinanceProjectDetailView(BaseProjectDetailTestCase): user_factory = FinanceFactory + def setUp(self): + super().setUp() + apply_site = ApplySiteFactory() + self.project_setting, _ = ProjectSettings.objects.get_or_create( + site_id=apply_site.id + ) + self.project_setting.use_settings = True + self.project_setting.save() + self.role = PAFReviewerRoleFactory(page=self.project_setting) + def test_has_access(self): project = ProjectFactory(status=INTERNAL_APPROVAL) response = self.get_page(project) diff --git a/hypha/apply/projects/utils.py b/hypha/apply/projects/utils.py index ea06cc42766f55fca2c49548428b43295b960aac..a5619f50ca4a931101dfa2cd9b1c809cafa13970 100644 --- a/hypha/apply/projects/utils.py +++ b/hypha/apply/projects/utils.py @@ -28,9 +28,11 @@ from .models.payment import ( SUBMITTED, ) from .models.project import ( + INTERNAL_APPROVAL, PAF_STATUS_CHOICES, PROJECT_PUBLIC_STATUSES, PROJECT_STATUS_CHOICES, + PAFReviewersRole, ) @@ -107,6 +109,26 @@ def fetch_and_save_project_details(project_id, external_projectid): save_project_details(project_id, data) +def no_pafreviewer_role(): + """ + Return True if no PAFReviewerRoles exists + """ + return not (PAFReviewersRole.objects.exists()) + + +def get_project_status_choices(): + """ + Return available Project status choices by removing the disabled ones + """ + if no_pafreviewer_role(): + return [ + (status, label) + for status, label in PROJECT_STATUS_CHOICES + if status != INTERNAL_APPROVAL + ] + return PROJECT_STATUS_CHOICES + + def save_project_details(project_id, data): project = Project.objects.get(id=project_id) project.external_project_information = data diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index 0a5aeaa5b5ed8b525faaba23429889f6bc6b4327..30505e197b9e5bc43312dcd03e7d445baea06ca4 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -81,6 +81,7 @@ from ..forms import ( RemoveDocumentForm, SelectDocumentForm, SetPendingForm, + SkipPAFApprovalProcessForm, SubmitContractDocumentsForm, UpdateProjectLeadForm, UploadContractDocumentForm, @@ -110,7 +111,11 @@ from ..models.project import ( from ..models.report import Report from ..permissions import has_permission from ..tables import InvoiceListTable, ProjectsListTable, ReportListTable -from ..utils import get_paf_status_display, get_placeholder_file +from ..utils import ( + get_paf_status_display, + get_placeholder_file, + get_project_status_choices, +) from ..views.payment import ChangeInvoiceStatusView from .report import ReportFrequencyUpdate, ReportingMixin @@ -578,6 +583,41 @@ class UploadContractView(DelegatedViewMixin, CreateView): return response +class SkipPAFApprovalProcessView(DelegatedViewMixin, UpdateView): + context_name = "skip_paf_approval_form" + model = Project + form_class = SkipPAFApprovalProcessForm + + def form_valid(self, form): + project = self.kwargs["object"] + old_stage = project.status + project.is_locked = True + project.status = CONTRACTING + response = super().form_valid(form) + + messenger( + MESSAGES.PROJECT_TRANSITION, + request=self.request, + user=self.request.user, + source=self.object, + related=old_stage, + ) + # add project waiting contract task to staff/contracting groups + if settings.STAFF_UPLOAD_CONTRACT: + add_task_to_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + else: + add_task_to_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=CONTRACTING_GROUP_NAME), + related_obj=self.object, + ) + return response + + @method_decorator(login_required, name="dispatch") class SubmitContractDocumentsView(DelegatedViewMixin, UpdateView): context_name = "submit_contract_documents_form" @@ -1176,9 +1216,9 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): class BaseProjectDetailView(ReportingMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["statuses"] = PROJECT_STATUS_CHOICES + context["statuses"] = get_project_status_choices() context["current_status_index"] = [ - status for status, _ in PROJECT_STATUS_CHOICES + status for status, _ in get_project_status_choices() ].index(self.object.status) context["supporting_documents_configured"] = ( True if DocumentCategory.objects.count() else False @@ -1210,6 +1250,7 @@ class AdminProjectDetailView( ChangePAFStatusView, ChangeProjectstatusView, ChangeInvoiceStatusView, + SkipPAFApprovalProcessView, ] model = Project template_name_suffix = "_admin_detail" @@ -1223,9 +1264,9 @@ class AdminProjectDetailView( def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["statuses"] = PROJECT_STATUS_CHOICES + context["statuses"] = get_project_status_choices() context["current_status_index"] = [ - status for status, _ in PROJECT_STATUS_CHOICES + status for status, _ in get_project_status_choices() ].index(self.object.status) project_settings = ProjectSettings.for_request(self.request) context["project_settings"] = project_settings diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index a681399fe83ece4d0b1a5552e8711ed16c25ee6e..fb1ebd1d40c369140660a262efdd48679b3038a7 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -14,8 +14,9 @@ from hypha.apply.funds.utils import get_statuses_as_params from ..constants import statuses_and_table_statuses_mapping from ..models.payment import Invoice -from ..models.project import PROJECT_STATUS_CHOICES, Project +from ..models.project import Project from ..permissions import has_permission +from ..utils import get_project_status_choices @login_required @@ -53,7 +54,7 @@ def get_project_status_counts(request): if project_status_url_query and key in project_status_url_query else False, } - for key, display in PROJECT_STATUS_CHOICES + for key, display in get_project_status_choices() } return render(