From ab4f428e91e833306052a7b66c398d327d106cde Mon Sep 17 00:00:00 2001 From: sandeepsajan0 Date: Fri, 15 Jul 2022 17:08:41 +0530 Subject: [PATCH 01/17] Update Dasboards for finance and contracting --- .../dashboard/contracting_dashboard.html | 34 ++++++++++++++++ .../dashboard/finance_dashboard.html | 7 ++++ hypha/apply/dashboard/views.py | 40 +++++++++++++++++++ hypha/apply/utils/views.py | 6 +++ 4 files changed, 87 insertions(+) create mode 100644 hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html diff --git a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html new file mode 100644 index 0000000000..902068d2c6 --- /dev/null +++ b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html @@ -0,0 +1,34 @@ +{% extends "base-apply.html" %} +{% load render_table from django_tables2 %} +{% load i18n static %} + +{% block title %}{% trans "Dashboard" %}{% endblock %} + +{% block content %} +
+
+ {% block page_header %} +

{% trans "Dashboard" %}

+ {% endblock %} + + {% trans "Apply admin" %} + + +
+
+
+ {% if waiting_for_approval.count %} +
+

{% trans "PAF awaiting approval" %}

+ {% render_table waiting_for_approval.table %} +
+ {% endif %} +
+{% endblock %} + +{% block extra_js %} + + + + +{% endblock %} diff --git a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html index 98b17737fd..d3985ae2cc 100644 --- a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html @@ -25,6 +25,13 @@

{% trans "Active Invoices" %}

{% trans "No Active Invoices" %} {% endif %} + + {% if waiting_for_approval.count %} +
+

{% trans "PAF awaiting approval" %}

+ {% render_table waiting_for_approval.table %} +
+ {% endif %} {% endblock %} diff --git a/hypha/apply/dashboard/views.py b/hypha/apply/dashboard/views.py index f08e327d03..bfc9d8150b 100644 --- a/hypha/apply/dashboard/views.py +++ b/hypha/apply/dashboard/views.py @@ -160,6 +160,7 @@ def get_context_data(self, **kwargs): context.update({ 'active_invoices': self.active_invoices(), + 'waiting_for_approval': self.waiting_for_approval(), }) return context @@ -175,6 +176,19 @@ def active_invoices(self): 'table': InvoiceDashboardTable(invoices), } + def waiting_for_approval(self): + if not self.request.user.is_finance: + return { + 'count': None, + 'table': None, + } + + to_paf_approve = Project.objects.waiting_for_approval().for_table() + return { + 'count': to_paf_approve.count(), + 'table': ProjectsDashboardTable(data=to_paf_approve), + } + class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateView): template_name = 'dashboard/reviewer_dashboard.html' @@ -261,6 +275,31 @@ def partner_submissions(self, user, submissions): return partner_submissions, partner_submissions_table +class ContractingDashboardView(MyFlaggedMixin, TemplateView): + template_name = 'dashboard/contracting_dashboard.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'waiting_for_approval': self.waiting_for_approval() + }) + + return context + + def waiting_for_approval(self): + if not self.request.user.is_contracting: + return { + 'count': None, + 'table': None, + } + + to_paf_approve = Project.objects.waiting_for_approval().for_table() + return { + 'count': to_paf_approve.count(), + 'table': ProjectsDashboardTable(data=to_paf_approve), + } + + class CommunityDashboardView(MySubmissionContextMixin, TemplateView): template_name = 'dashboard/community_dashboard.html' @@ -341,3 +380,4 @@ class DashboardView(ViewDispatcher): community_view = CommunityDashboardView applicant_view = ApplicantDashboardView finance_view = FinanceDashboardView + contracting_view = ContractingDashboardView 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) From 1d62afe5f092b01f4519aa08ca8d4c2a4b7a0c95 Mon Sep 17 00:00:00 2001 From: sandeepsajan0 Date: Fri, 15 Jul 2022 19:16:53 +0530 Subject: [PATCH 02/17] Add model for PAF reviewers roles, and manage permissions for contracting and finance --- hypha/apply/projects/forms/__init__.py | 2 + hypha/apply/projects/forms/project.py | 44 ++++ ...f_reviewers_roles__alter_project_status.py | 33 +++ hypha/apply/projects/models/project.py | 37 ++- .../project_admin_detail.html | 17 +- .../project_approval_form.html | 1 - .../application_projects/project_detail.html | 2 +- .../project_simplified_detail.html | 222 +++++++++++------- .../projects/templatetags/approval_tools.py | 19 +- .../projects/templatetags/contract_tools.py | 4 +- hypha/apply/projects/views/project.py | 64 ++++- hypha/apply/users/decorators.py | 8 + 12 files changed, 340 insertions(+), 113 deletions(-) create mode 100644 hypha/apply/projects/migrations/0054_paf_reviewers_roles__alter_project_status.py diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index a5f789bb06..8b8feabeb3 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -6,6 +6,7 @@ ) from .project import ( ApproveContractForm, + ChangePAFStatusForm, CreateApprovalForm, CreateProjectForm, ProjectApprovalForm, @@ -30,6 +31,7 @@ __all__ = [ 'SelectDocumentForm', 'ApproveContractForm', + 'ChangePAFStatusForm', 'CreateProjectForm', 'CreateApprovalForm', 'ProjectApprovalForm', diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index c7e3e2c732..4cea70d483 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -113,6 +113,50 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) +class ChangePAFStatusForm(forms.ModelForm): + # WIP todo: need to update this on the basis if wagtail PAF's reviewers roles + name_prefix = 'change_paf_status_form' + + class Meta: + fields = ['status', ] + model = Project + + def __init__(self, instance, user, *args, **kwargs): + super().__init__(instance=instance, *args, **kwargs) + # self.initial['comment'] = '' + # status_field = self.fields['status'] + # user_choices = invoice_status_user_choices(user) + # possible_status_transitions_lut = { + # SUBMITTED: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED], user_choices), + # RESUBMITTED: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED], user_choices), + # CHANGES_REQUESTED_BY_STAFF: filter_request_choices([DECLINED], user_choices), + # APPROVED_BY_STAFF: filter_request_choices( + # [ + # CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, + # ], + # user_choices + # ), + # CHANGES_REQUESTED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, DECLINED], user_choices), + # CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( + # [ + # CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, + # ], + # user_choices + # ), + # APPROVED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], user_choices), + # APPROVED_BY_FINANCE_2: filter_request_choices([CONVERTED, PAID], user_choices), + # CONVERTED: filter_request_choices([PAID], user_choices), + # } + # status_field.choices = possible_status_transitions_lut.get(instance.status, []) + + # def clean(self): + # cleaned_data = super().clean() + # status = cleaned_data['status'] + # if not self.instance.valid_checks and status == APPROVED_BY_FINANCE_1: + # self.add_error('status', _('Required checks on this invoice need to be compeleted for approval.')) + # return cleaned_data + + class RejectionForm(forms.Form): comment = forms.CharField(widget=forms.Textarea) diff --git a/hypha/apply/projects/migrations/0054_paf_reviewers_roles__alter_project_status.py b/hypha/apply/projects/migrations/0054_paf_reviewers_roles__alter_project_status.py new file mode 100644 index 0000000000..25a8392ee5 --- /dev/null +++ b/hypha/apply/projects/migrations/0054_paf_reviewers_roles__alter_project_status.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.13 on 2022-07-14 12:46 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('application_projects', '0053_projectapprovalform'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='status', + field=models.TextField(choices=[('committed', 'Committed'), ('waiting_for_approval', 'Waiting for Approval'), ('contracting', 'Contracting'), ('in_progress', 'In Progress'), ('closing', 'Closing'), ('complete', 'Complete')], default='committed'), + ), + migrations.CreateModel( + name='PAFReviewersRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('role', models.CharField(max_length=200)), + ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='paf_reviewers_roles', to='application_projects.projectsettings')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + ] diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index c1441746d6..94e04eb5f6 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -16,8 +16,13 @@ from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from modelcluster.fields import ParentalKey +from modelcluster.models import ClusterableModel +from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, StreamFieldPanel from wagtail.admin.panels import FieldPanel from wagtail.contrib.settings.models import BaseSetting, register_setting +from wagtail.core.fields import StreamField +from wagtail.core.models import Orderable from wagtail.fields import StreamField from addressfield.fields import ADDRESS_FIELDS_ORDER @@ -41,12 +46,14 @@ def document_path(instance, filename): COMMITTED = 'committed' +WAITING_FOR_APPROVAL = 'waiting_for_approval' CONTRACTING = 'contracting' IN_PROGRESS = 'in_progress' CLOSING = 'closing' COMPLETE = 'complete' PROJECT_STATUS_CHOICES = [ (COMMITTED, _('Committed')), + (WAITING_FOR_APPROVAL, _('Waiting for Approval')), (CONTRACTING, _('Contracting')), (IN_PROGRESS, _('In Progress')), (CLOSING, _('Closing')), @@ -75,6 +82,11 @@ def in_approval(self): approvals__isnull=True, ) + def waiting_for_approval(self): + return self.filter( + status=WAITING_FOR_APPROVAL, + ) + def by_end_date(self, desc=False): order = getattr(F('proposed_end'), 'desc' if desc else 'asc')(nulls_last=True) @@ -276,11 +288,13 @@ def editable_by(self, user): return True # Approver can edit it when they are approving - return user.is_approver and self.can_make_approval + if self.can_make_approval: + if user.is_finance or user.is_approver or user.is_contracting: + return True @property def editable(self): - if self.status not in (CONTRACTING, COMMITTED): + if self.status not in (CONTRACTING, WAITING_FOR_APPROVAL, COMMITTED): return True # Someone has approved the project - consider it locked while with contracting @@ -297,7 +311,11 @@ def get_absolute_url(self): @property def can_make_approval(self): - return self.is_locked and self.status == COMMITTED + return self.is_locked and self.status == WAITING_FOR_APPROVAL + + @property + def can_update_paf_status(self): + return self.status == WAITING_FOR_APPROVAL def can_request_funding(self): """ @@ -386,11 +404,22 @@ def __str__(self): return self.name +class PAFReviewersRole(Orderable): + role = models.CharField(max_length=200) + page = ParentalKey('ProjectSettings', related_name='paf_reviewers_roles') + + @register_setting -class ProjectSettings(BaseSetting): +class ProjectSettings(BaseSetting, ClusterableModel): compliance_email = models.TextField("Compliance Email") vendor_setup_required = models.BooleanField(default=True) + panels = [ + FieldPanel('compliance_email'), + FieldPanel('vendor_setup_required'), + InlinePanel('paf_reviewers_roles', label=_('PAF Reviewers Roles')), + ] + class Approval(models.Model): project = models.ForeignKey("Project", on_delete=models.CASCADE, related_name="approvals") 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 7cc6eb5951..1a1af19cb1 100644 --- a/hypha/apply/projects/templates/application_projects/project_admin_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_admin_detail.html @@ -69,14 +69,14 @@ {% endif %} data-fancybox data-src="#send-for-approval" - class="button button--bottom-space button--primary button--full-width {% if not object.can_send_for_approval %}button--tooltip-disabled{% endif %}" + class="button button--bottom-space button--primary button--full-width {% if not object.can_send_for_approval or not user.is_apply_staff %}button--tooltip-disabled{% endif %}" href="#"> {% trans "Submit for Approval" %} {% endif %} {% if object.can_make_approval %} - {% user_can_approve_project object user as user_can_approve %} + {% user_can_final_approve_project object user as user_can_approve %} {% trans "Approve Contract" %} {% trans "Approve" %} - - {% trans "Request changes" %} - + {% trans "Update Status" %} + {% endif %} {% endblock %} diff --git a/hypha/apply/projects/templates/application_projects/project_approval_form.html b/hypha/apply/projects/templates/application_projects/project_approval_form.html index 7f41d66c57..1fc5cb3782 100644 --- a/hypha/apply/projects/templates/application_projects/project_approval_form.html +++ b/hypha/apply/projects/templates/application_projects/project_approval_form.html @@ -46,7 +46,6 @@

{% trans "Editing" %}: {{ object.title }} {% block extra_js %} - {% if not show_all_group_fields %} diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index 84499228fd..d7db9dd381 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -120,7 +120,7 @@

{% trans "E-mail" %}
{% trans "Actions to take" %}
- {% if request.user.is_apply_staff %} + {% if request.user.is_apply_staff or request.user.is_contracting or request.user.is_finance %} {% block admin_actions %}{% endblock %} {% endif %} diff --git a/hypha/apply/projects/templates/application_projects/project_simplified_detail.html b/hypha/apply/projects/templates/application_projects/project_simplified_detail.html index 08a8d4be47..71069b5a95 100644 --- a/hypha/apply/projects/templates/application_projects/project_simplified_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_simplified_detail.html @@ -1,8 +1,13 @@ {% extends "base-apply.html" %} -{% load i18n %} +{% load i18n static approval_tools %} {% block title %}{{ object.title }}{% endblock %} +{% block extra_css %} + + {{ reviewer_form.media.css }} +{% endblock %} + {% block body_class %}light-grey-bg{% endblock %} {% block content %} @@ -26,109 +31,144 @@
- -
-

{% 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 "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:"-" }}

+
+ + {% if object.sent_to_compliance_at %} +
+
{% trans "Sent to Compliance" %}
+

{{ object.sent_to_compliance_at|date:"DATE_FORMAT" }}

+
+ {% endif %} + +
+ + {% if object.output_answers %} +
+ {{ object.output_answers }} +
+ {% endif %}
-
-
{% 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 %} +
+ + {{ review.author }} + {% if review.author.role %} + as {{ review.author.role }} + {% endif %} + - {{ review.created_at|date:"DATE_FORMAT" }} + +
+ {% empty %} + {% trans "No reviews" %} + {% endfor %} +
{% trans "External Reviewers" %}
+ {% for review in project.submission.reviews.by_reviewers %} +
+ + {{ review.author }} - {{ review.created_at|date:"DATE_FORMAT" }} + +
+ {% empty %} + {% trans "No reviews" %} + {% endfor %}
- {% if object.sent_to_compliance_at %} -
-
{% trans "Sent to Compliance" %}
-

{{ object.sent_to_compliance_at|date:"DATE_FORMAT" }}

+

{% trans "Supporting Documents" %}

+
+

{% trans "Submission" %}

+ {% for packet_file in object.packet_files.all %} +

{{ packet_file.title }}

+ {% endfor %}
+
+ {% user_can_update_paf_status object user as user_can_take_actions %} + {% if user_can_take_actions %} +
- - {% if object.output_answers %} -
- {{ object.output_answers }} -
- {% endif %} -
- -

{% 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 %} -
- - {{ review.author }} - {% if review.author.role %} - as {{ review.author.role }} + - {% empty %} - {% trans "No reviews" %} - {% endfor %} -
{% trans "External Reviewers" %}
- {% for review in project.submission.reviews.by_reviewers %} -
- - {{ review.author }} - {{ review.created_at|date:"DATE_FORMAT" }} -
- {% empty %} - {% trans "No reviews" %} - {% endfor %} -
-

{% trans "Supporting Documents" %}

-
-

{% trans "Submission" %}

- {% for packet_file in object.packet_files.all %} -

{{ packet_file.title }}

- {% endfor %} + + {% endif %}
{% endblock content %} + +{% block extra_js %} + {{ block.super }} + + + + + +{% endblock %} diff --git a/hypha/apply/projects/templatetags/approval_tools.py b/hypha/apply/projects/templatetags/approval_tools.py index 84dc2891c7..4c36fb9c7c 100644 --- a/hypha/apply/projects/templatetags/approval_tools.py +++ b/hypha/apply/projects/templatetags/approval_tools.py @@ -15,7 +15,24 @@ def can_send_for_approval(project, user): @register.simple_tag def user_can_approve_project(project, user): - return user.is_approver and not user_has_approved(project, user) + if not user_has_approved(project, user): + if user.is_finance or user.is_contracting or user.is_approver: + return True + return False + + +@register.simple_tag +def user_can_update_paf_status(project, user): + if project.can_update_paf_status: + if user.is_finance or user.is_contracting or user.is_approver: + return True + return False + + +@register.simple_tag +def user_can_final_approve_project(project, user): + # todo: we need to decide the user role for final approval + return False @register.simple_tag diff --git a/hypha/apply/projects/templatetags/contract_tools.py b/hypha/apply/projects/templatetags/contract_tools.py index efb2701a38..de991f0d9e 100644 --- a/hypha/apply/projects/templatetags/contract_tools.py +++ b/hypha/apply/projects/templatetags/contract_tools.py @@ -1,6 +1,6 @@ from django import template -from ..models.project import COMMITTED +from ..models.project import COMMITTED, WAITING_FOR_APPROVAL register = template.Library() @@ -8,7 +8,7 @@ @register.simple_tag def user_can_upload_contract(project, user): if user.is_apply_staff: - return project.status != COMMITTED + return project.status not in [COMMITTED, WAITING_FOR_APPROVAL] # Does the Project have any unapproved contracts? latest_contract = project.contracts.order_by('-created_at').first() diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index b436eb3192..fef61e15a9 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -31,6 +31,7 @@ from hypha.apply.stream_forms.models import BaseStreamForm from hypha.apply.users.decorators import ( approver_required, + staff_or_finance_or_contracting_required, staff_or_finance_required, staff_required, ) @@ -47,6 +48,7 @@ from ..filters import InvoiceListFilter, ProjectListFilter, ReportListFilter from ..forms import ( ApproveContractForm, + ChangePAFStatusForm, CreateApprovalForm, ProjectApprovalForm, RejectionForm, @@ -63,6 +65,7 @@ CONTRACTING, IN_PROGRESS, PROJECT_STATUS_CHOICES, + WAITING_FOR_APPROVAL, Approval, Contract, PacketFile, @@ -80,7 +83,9 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): model = Project def form_valid(self, form): - # lock project + project = self.kwargs['object'] + old_stage = project.get_status_display() + response = super().form_valid(form) messenger( @@ -90,6 +95,17 @@ def form_valid(self, form): source=self.object, ) + project.status = WAITING_FOR_APPROVAL + project.save(update_fields=['status']) + + messenger( + MESSAGES.PROJECT_TRANSITION, + request=self.request, + user=self.request.user, + source=project, + related=old_stage, + ) + return response @@ -481,6 +497,7 @@ def dispatch(self, request, *args, **kwargs): class ProjectDetailView(ViewDispatcher): admin_view = AdminProjectDetailView finance_view = AdminProjectDetailView + contracting_view = AdminProjectDetailView applicant_view = ApplicantProjectDetailView @@ -536,13 +553,51 @@ def test_func(self): # PROJECT EDIT -@method_decorator(staff_or_finance_required, name='dispatch') +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') +class ChangePAFStatusView(DelegatedViewMixin, UpdateView): + # WIP todo: needs to create activity and send notifications + form_class = ChangePAFStatusForm + context_name = 'change_paf_status' + + def form_valid(self, form): + response = super().form_valid(form) + if form.cleaned_data['comment']: + invoice_status_change = _('

PAF status updated to: {status}.

').format(status=self.object.status_display) + comment = f'

{self.object.comment}.

' + + message = invoice_status_change + comment + + # Activity.objects.create( + # user=self.request.user, + # type=COMMENT, + # source=self.object.project, + # timestamp=timezone.now(), + # message=message, + # visibility=ALL, + # related_object=self.object, + # ) + + # messenger( + # MESSAGES.UPDATE_INVOICE_STATUS, + # request=self.request, + # user=self.request.user, + # source=self.object.project, + # related=self.object, + # ) + + return response + + +@method_decorator(staff_or_finance_or_contracting_required, name='dispatch') class ProjectDetailSimplifiedView(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 +645,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 +653,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..7abdfe92f4 100644 --- a/hypha/apply/users/decorators.py +++ b/hypha/apply/users/decorators.py @@ -31,6 +31,12 @@ 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 @@ -45,6 +51,8 @@ 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)] + def superuser_decorator(fn): check = user_passes_test(lambda user: user.is_superuser) From d55f9ebf42e12b57ba20726580ae0c4549e18643 Mon Sep 17 00:00:00 2001 From: sandeepsajan0 Date: Tue, 19 Jul 2022 12:13:30 +0530 Subject: [PATCH 03/17] Add PAF status Change form and view with dynamic roles --- hypha/apply/projects/forms/project.py | 52 ++++------- .../0055_project_paf_reviews_meta_data.py | 18 ++++ hypha/apply/projects/models/project.py | 25 ++++++ .../project_admin_detail.html | 6 +- .../project_simplified_detail.html | 9 +- .../projects/templatetags/approval_tools.py | 3 +- hypha/apply/projects/views/project.py | 87 +++++++++++-------- hypha/apply/users/decorators.py | 8 ++ 8 files changed, 128 insertions(+), 80 deletions(-) create mode 100644 hypha/apply/projects/migrations/0055_project_paf_reviews_meta_data.py diff --git a/hypha/apply/projects/forms/project.py b/hypha/apply/projects/forms/project.py index 4cea70d483..6ddd957cbb 100644 --- a/hypha/apply/projects/forms/project.py +++ b/hypha/apply/projects/forms/project.py @@ -7,7 +7,15 @@ from hypha.apply.stream_forms.forms import StreamBaseForm from hypha.apply.users.groups import STAFF_GROUP_NAME -from ..models.project import COMMITTED, Approval, Contract, PacketFile, Project +from ..models.project import ( + COMMITTED, + PAF_STATUS_CHOICES, + Approval, + Contract, + PacketFile, + PAFReviewersRole, + Project, +) User = get_user_model() @@ -114,47 +122,21 @@ def save(self, *args, **kwargs): class ChangePAFStatusForm(forms.ModelForm): - # WIP todo: need to update this on the basis if wagtail PAF's reviewers roles name_prefix = 'change_paf_status_form' + paf_reviewers_roles = PAFReviewersRole.objects.all().only('role') + paf_status = forms.ChoiceField(choices=PAF_STATUS_CHOICES) + role = forms.ModelChoiceField(queryset=paf_reviewers_roles) + comment = forms.CharField(required=False, widget=forms.Textarea) class Meta: - fields = ['status', ] + fields = ['paf_status', 'role', 'comment'] model = Project def __init__(self, instance, user, *args, **kwargs): super().__init__(instance=instance, *args, **kwargs) - # self.initial['comment'] = '' - # status_field = self.fields['status'] - # user_choices = invoice_status_user_choices(user) - # possible_status_transitions_lut = { - # SUBMITTED: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED], user_choices), - # RESUBMITTED: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED], user_choices), - # CHANGES_REQUESTED_BY_STAFF: filter_request_choices([DECLINED], user_choices), - # APPROVED_BY_STAFF: filter_request_choices( - # [ - # CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, - # ], - # user_choices - # ), - # CHANGES_REQUESTED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_STAFF, DECLINED], user_choices), - # CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( - # [ - # CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, - # ], - # user_choices - # ), - # APPROVED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], user_choices), - # APPROVED_BY_FINANCE_2: filter_request_choices([CONVERTED, PAID], user_choices), - # CONVERTED: filter_request_choices([PAID], user_choices), - # } - # status_field.choices = possible_status_transitions_lut.get(instance.status, []) - - # def clean(self): - # cleaned_data = super().clean() - # status = cleaned_data['status'] - # if not self.instance.valid_checks and status == APPROVED_BY_FINANCE_1: - # self.add_error('status', _('Required checks on this invoice need to be compeleted for approval.')) - # return cleaned_data + self.initial['comment'] = '' + status_field = self.fields['paf_status'] + status_field.choices = PAF_STATUS_CHOICES class RejectionForm(forms.Form): diff --git a/hypha/apply/projects/migrations/0055_project_paf_reviews_meta_data.py b/hypha/apply/projects/migrations/0055_project_paf_reviews_meta_data.py new file mode 100644 index 0000000000..a674f2a6a8 --- /dev/null +++ b/hypha/apply/projects/migrations/0055_project_paf_reviews_meta_data.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-18 13:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application_projects', '0054_paf_reviewers_roles__alter_project_status'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='paf_reviews_meta_data', + field=models.JSONField(default=dict, help_text='Reviewers role and their actions/comments'), + ), + ] diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 94e04eb5f6..c21ae0dec8 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -45,6 +45,13 @@ def document_path(instance, filename): return f'projects/{instance.project_id}/supporting_documents/{filename}' +APPROVE = 'approve' +REQUEST_CHANGE = 'request_change' +PAF_STATUS_CHOICES = ( + (APPROVE, 'Approve'), + (REQUEST_CHANGE, 'Request Change') +) + COMMITTED = 'committed' WAITING_FOR_APPROVAL = 'waiting_for_approval' CONTRACTING = 'contracting' @@ -181,6 +188,11 @@ class Project(BaseStreamForm, AccessFormData, models.Model): ) sent_to_compliance_at = models.DateTimeField(null=True) + paf_reviews_meta_data = models.JSONField( + default=dict, + help_text='Reviewers role and their actions/comments' + ) + objects = ProjectQuerySet.as_manager() def __str__(self): @@ -317,6 +329,16 @@ def can_make_approval(self): def can_update_paf_status(self): return self.status == WAITING_FOR_APPROVAL + @property + def can_make_final_approval(self): + paf_reviewers_count = PAFReviewersRole.objects.all().count() + if paf_reviewers_count == len(self.paf_reviews_meta_data): + for paf_review_data in self.paf_reviews_meta_data.values(): + if paf_review_data['status'] == REQUEST_CHANGE: + return False + return True + return False + def can_request_funding(self): """ Should we show this Project's funding block? @@ -408,6 +430,9 @@ class PAFReviewersRole(Orderable): role = models.CharField(max_length=200) page = ParentalKey('ProjectSettings', related_name='paf_reviewers_roles') + def __str__(self): + return str(self.role) + @register_setting class ProjectSettings(BaseSetting, ClusterableModel): 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 1a1af19cb1..3801291be4 100644 --- a/hypha/apply/projects/templates/application_projects/project_admin_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_admin_detail.html @@ -84,10 +84,10 @@ {% trans "Approve" %} - {% trans "Update Status" %} + {% trans "Update PAF Status" %} diff --git a/hypha/apply/projects/templates/application_projects/project_simplified_detail.html b/hypha/apply/projects/templates/application_projects/project_simplified_detail.html index 71069b5a95..65baac2db7 100644 --- a/hypha/apply/projects/templates/application_projects/project_simplified_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_simplified_detail.html @@ -146,11 +146,12 @@

{% trans "Supporting Documents" %}

- - {% if contract_to_approve %}