{% trans "Project Information" %}
+{% trans "Proposed start date" %}
+{{ object.proposed_start|date:"DATE_FORMAT"|default:"-" }}
+{% trans "Project Proposed end date" %}
+{{ object.proposed_end|date:"DATE_FORMAT"|default:"-" }}
+{% trans "Legal name" %}
+{{ object.contact_legal_name|default:"-" }}
+{% trans "E-mail" %}
+{{ object.contact_email|default:"-" }}
+{% trans "Address" %}
+{{ object.get_address_display|default:"-"}}
+{% trans "Phone" %}
+{{ object.phone|default:"-" }}
+{% trans "Value" %}
+{{ CURRENCY_SYMBOL }}{{ object.value|default:"-" }}
+{% trans "Sent to Compliance" %}
+{{ object.sent_to_compliance_at|date:"DATE_FORMAT" }}
+{% trans "Phone" %}
-{{ object.phone|default:"-" }}
+{% trans "Approvals" %}
+{% trans "Approver" %}
+ {% with approval=project.approvals.first %} +{{ approval.by }} - {{ approval.created_at|date:"DATE_FORMAT" }}
+ {% endwith %}{% trans "Value" %}
-{{ CURRENCY_SYMBOL }}{{ object.value|default:"-" }}
+{% trans "Review" %}
+{% trans "Submission lead" %}
+{{ project.submission.lead }}
+ +{% trans "Reviews" %}
+{% trans "Staff Reviewers" %}
+ {% for review in project.submission.reviews.by_staff %} +{% trans "External Reviewers" %}
+ {% for review in project.submission.reviews.by_reviewers %} +{% trans "Sent to Compliance" %}
-{{ object.sent_to_compliance_at|date:"DATE_FORMAT" }}
+{% trans "Supporting Documents" %}
+{% trans "Approvals" %}
-{% trans "Approver" %}
- {% with approval=project.approvals.first %} -{{ approval.by }} - {{ approval.created_at|date:"DATE_FORMAT" }}
- {% endwith %} -{% trans "Review" %}
-{% trans "Submission lead" %}
-{{ project.submission.lead }}
- -{% trans "Reviews" %}
-{% trans "Staff Reviewers" %}
- {% for review in project.submission.reviews.by_staff %} -{% trans "External Reviewers" %}
- {% for review in project.submission.reviews.by_reviewers %} -{% trans "Supporting Documents" %}
-{user} request changes the Project and update status to {project_status}.
').format( + user=self.request.user, + project_status=project.status + ) + + Activity.objects.create( + user=self.request.user, + type=ACTION, + source=project, + timestamp=timezone.now(), + message=project_status_message, + visibility=ALL, + ) + + messenger( + MESSAGES.REQUEST_PROJECT_CHANGE, + request=self.request, + user=self.request.user, + source=self.object, + comment=comment, + ) + return response + messenger( - MESSAGES.REQUEST_PROJECT_CHANGE, + MESSAGES.APPROVE_PROJECT, request=self.request, user=self.request.user, - source=self.object, - comment=form.cleaned_data['comment'], + source=project, ) - self.object.is_locked = False - self.object.save(update_fields=['is_locked']) + project.is_locked = True + project.status = CONTRACTING + project.save(update_fields=['is_locked', 'status']) - return redirect(self.object) + project_status_message = _( + '{user} approved the Project and update status to {project_status}.
').format( + user=self.request.user, + project_status=project.status + ) + + Activity.objects.create( + user=self.request.user, + type=ACTION, + source=project, + timestamp=timezone.now(), + message=project_status_message, + visibility=ALL, + ) + + messenger( + MESSAGES.PROJECT_TRANSITION, + request=self.request, + user=self.request.user, + source=project, + related=old_stage, + ) + + return response # PROJECT DOCUMENTS @@ -412,6 +459,63 @@ def form_valid(self, form): # PROJECT VIEW + +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') +class ChangePAFStatusView(DelegatedViewMixin, UpdateView): + form_class = ChangePAFStatusForm + context_name = 'change_paf_status' + model = Project + + def form_valid(self, form): + response = super().form_valid(form) + role = form.cleaned_data.get('role') + paf_status = form.cleaned_data.get('paf_status') + comment = form.cleaned_data.get('comment', '') + + self.object.paf_reviews_meta_data.update({str(role.role): {'status': paf_status, 'comment': comment}}) + self.object.save(update_fields=['paf_reviews_meta_data']) + + paf_status_update_message = _('{role} has updated PAF status to {paf_status}.
').format( + role=role, paf_status=paf_status) + Activity.objects.create( + user=self.request.user, + type=ACTION, + source=self.object, + timestamp=timezone.now(), + message=paf_status_update_message, + visibility=ALL, + ) + + if paf_status == REQUEST_CHANGE: + self.object.status = COMMITTED + self.object.save(update_fields=['status']) + + messenger( + MESSAGES.REQUEST_PROJECT_CHANGE, + request=self.request, + user=self.request.user, + source=self.object, + comment=comment, + ) + + if form.cleaned_data['comment']: + + comment = f"{form.cleaned_data['comment']}.
" + + message = paf_status_update_message + comment + + Activity.objects.create( + user=self.request.user, + type=COMMENT, + source=self.object, + timestamp=timezone.now(), + message=message, + visibility=ALL, + ) + + return response + + class BaseProjectDetailView(ReportingMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -429,8 +533,7 @@ class AdminProjectDetailView( form_views = [ ApproveContractView, CommentFormView, - CreateApprovalView, - RejectionView, + FinalApprovalView, RemoveDocumentView, SelectDocumentView, SendForApprovalView, @@ -438,6 +541,7 @@ class AdminProjectDetailView( UpdateLeadView, UploadContractView, UploadDocumentView, + ChangePAFStatusView, ] model = Project template_name_suffix = '_admin_detail' @@ -481,6 +585,7 @@ def dispatch(self, request, *args, **kwargs): class ProjectDetailView(ViewDispatcher): admin_view = AdminProjectDetailView finance_view = AdminProjectDetailView + contracting_view = AdminProjectDetailView applicant_view = ApplicantProjectDetailView @@ -536,13 +641,16 @@ def test_func(self): # PROJECT EDIT -@method_decorator(staff_or_finance_required, name='dispatch') -class ProjectDetailSimplifiedView(DetailView): +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') +class ProjectDetailSimplifiedView(DelegateableView, DetailView): + form_views = [ + ChangePAFStatusView + ] model = Project template_name_suffix = '_simplified_detail' -@method_decorator(staff_required, name='dispatch') +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') class ProjectDetailPDFView(SingleObjectMixin, View): model = Project @@ -590,7 +698,7 @@ def get(self, request, *args, **kwargs): ) -@method_decorator(staff_required, name='dispatch') +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') class ProjectApprovalEditView(BaseStreamForm, UpdateView): submission_form_class = ProjectApprovalForm model = Project @@ -598,7 +706,6 @@ class ProjectApprovalEditView(BaseStreamForm, UpdateView): def buttons(self): yield ('submit', 'primary', _('Submit')) - # yield ('save', 'white', _('Save draft')) def dispatch(self, request, *args, **kwargs): project = self.get_object() diff --git a/hypha/apply/users/decorators.py b/hypha/apply/users/decorators.py index 1017901b08..3bb0fe4ee3 100644 --- a/hypha/apply/users/decorators.py +++ b/hypha/apply/users/decorators.py @@ -31,12 +31,24 @@ def is_apply_staff_or_finance(user): return True +def is_apply_staff_or_finance_or_contracting(user): + if not (user.is_apply_staff or user.is_finance or user.is_contracting): + raise PermissionDenied + return True + + def is_approver(user): if not user.is_approver: raise PermissionDenied return True +def is_contracting_approver(user): + if not user.is_approver or not user.is_contracting: + raise PermissionDenied + return True + + staff_required = [login_required, user_passes_test(is_apply_staff)] finance_required = [login_required, user_passes_test(is_finance)] @@ -45,6 +57,10 @@ def is_approver(user): approver_required = [login_required, user_passes_test(is_approver)] +staff_or_finance_or_contracting_required = [login_required, user_passes_test(is_apply_staff_or_finance_or_contracting)] + +contracting_approver_required = [login_required, user_passes_test(is_contracting_approver)] + def superuser_decorator(fn): check = user_passes_test(lambda user: user.is_superuser) diff --git a/hypha/apply/users/tests/factories.py b/hypha/apply/users/tests/factories.py index 08f1907ab1..284d91896e 100644 --- a/hypha/apply/users/tests/factories.py +++ b/hypha/apply/users/tests/factories.py @@ -9,6 +9,7 @@ APPLICANT_GROUP_NAME, APPROVER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME, + CONTRACTING_GROUP_NAME, FINANCE_GROUP_NAME, PARTNER_GROUP_NAME, REVIEWER_GROUP_NAME, @@ -130,6 +131,25 @@ def groups(self, create, extracted, **kwargs): ) +class ContractingFactory(UserFactory): + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if create: + self.groups.add( + GroupFactory(name=CONTRACTING_GROUP_NAME), + ) + + +class ContractingApproverFactory(UserFactory): + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if create: + self.groups.add( + GroupFactory(name=CONTRACTING_GROUP_NAME), + GroupFactory(name=APPROVER_GROUP_NAME) + ) + + class SuperUserFactory(StaffFactory): is_superuser = True diff --git a/hypha/apply/utils/views.py b/hypha/apply/utils/views.py index 816d8dc982..351d1b560f 100644 --- a/hypha/apply/utils/views.py +++ b/hypha/apply/utils/views.py @@ -30,6 +30,7 @@ class ViewDispatcher(View): community_view: View = None applicant_view: View = None finance_view: View = None + contracting_view: View = None def admin_check(self, request): return request.user.is_apply_staff @@ -46,6 +47,9 @@ def community_check(self, request): def finance_check(self, request): return request.user.is_finance + def contracting_check(self, request): + return request.user.is_contracting + def dispatch(self, request, *args, **kwargs): view = self.applicant_view @@ -59,6 +63,8 @@ def dispatch(self, request, *args, **kwargs): view = self.community_view elif self.finance_check(request): view = self.finance_view + elif self.contracting_check(request): + view = self.contracting_view if view: return view.as_view()(request, *args, **kwargs)