diff --git a/hypha/apply/activity/messaging.py b/hypha/apply/activity/messaging.py index fbf7eaa843ef59be59a5df9f231bb1c7cc652a4b..2c7e705561bc78ce7eda510adf8c8b1ab7430845 100644 --- a/hypha/apply/activity/messaging.py +++ b/hypha/apply/activity/messaging.py @@ -11,6 +11,18 @@ from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import gettext as _ +from hypha.apply.projects.models.payment import ( + APPROVED_BY_FINANCE_1, + APPROVED_BY_FINANCE_2, + APPROVED_BY_STAFF, + CHANGES_REQUESTED_BY_FINANCE_1, + CHANGES_REQUESTED_BY_FINANCE_2, + CHANGES_REQUESTED_BY_STAFF, + DECLINED, + RESUBMITTED, + SUBMITTED, +) + from .models import ALL, TEAM from .options import MESSAGES from .tasks import send_mail @@ -490,13 +502,31 @@ class SlackAdapter(AdapterBase): ] recipients = [self.slack_id(source.lead)] - # Notify second reviewer when first reviewer is done. if message_type == MESSAGES.NEW_REVIEW and related: submission = source if submission.assigned.with_roles().count() == 2 and related.author.reviewer == submission.assigned.with_roles().first().reviewer: recipients.append(self.slack_id(submission.assigned.with_roles().last().reviewer)) + if message_type == MESSAGES.UPDATE_INVOICE_STATUS: + if related.status in [SUBMITTED, RESUBMITTED, CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_2]: + # Notify project lead/staff + return recipients + if related.status in [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]: + # Notify finance 1 + return [ + self.slack_id(user) + for user in User.objects.finances_level_1() + if self.slack_id(user) + ] + if related.status in [APPROVED_BY_FINANCE_1]: + # Notify finance 2 + return [ + self.slack_id(user) + for user in User.objects.finances_level_2() + if self.slack_id(user) + ] + return [] return recipients def batch_recipients(self, message_type, sources, **kwargs): @@ -891,6 +921,12 @@ class EmailAdapter(AdapterBase): if message_type in {MESSAGES.REVIEW_REMINDER}: return self.reviewers(source) + if message_type == MESSAGES.UPDATE_INVOICE_STATUS: + related = kwargs.get('related', None) + if related: + if related.status in {CHANGES_REQUESTED_BY_STAFF, DECLINED}: + return [source.user.email] + return [] return [source.user.email] def batch_recipients(self, message_type, sources, **kwargs): diff --git a/hypha/apply/api/v1/permissions.py b/hypha/apply/api/v1/permissions.py index 0ead51633492d668e6c10cdd307f0c78750eef75..ac3fa7f5420c29397bc2d760413cdf0d452ce157 100644 --- a/hypha/apply/api/v1/permissions.py +++ b/hypha/apply/api/v1/permissions.py @@ -24,3 +24,23 @@ class IsFinance1User(permissions.BasePermission): def has_object_permission(self, request, view, obj): return request.user.is_finance_level_1 + + +class IsFinance2User(permissions.BasePermission): + def has_permission(self, request, view): + return request.user.is_finance_level_2 + + def has_object_permission(self, request, view, obj): + return request.user.is_finance_level_2 + + +class HasDeliverableEditPermission(permissions.BasePermission): + def has_permission(self, request, view): + invoice = view.get_invoice_object() + return invoice.can_user_edit_deliverables(request.user) + + +class HasRequiredChecksPermission(permissions.BasePermission): + def has_permission(self, request, view): + invoice = view.get_object() + return invoice.can_user_complete_required_checks(request.user) diff --git a/hypha/apply/api/v1/projects/views.py b/hypha/apply/api/v1/projects/views.py index 084260d4e0ce5001f0d59367d692e585094b5737..e2df70ff63352f356402023eeba46834e280c99f 100644 --- a/hypha/apply/api/v1/projects/views.py +++ b/hypha/apply/api/v1/projects/views.py @@ -9,7 +9,13 @@ from hypha.apply.projects.models.payment import Invoice, InvoiceDeliverable from hypha.apply.projects.models.project import Deliverable from ..mixin import InvoiceNestedMixin, ProjectNestedMixin -from ..permissions import IsApplyStaffUser, IsFinance1User +from ..permissions import ( + HasDeliverableEditPermission, + HasRequiredChecksPermission, + IsApplyStaffUser, + IsFinance1User, + IsFinance2User, +) from .serializers import ( DeliverableSerializer, InvoiceDeliverableListSerializer, @@ -25,7 +31,8 @@ class DeliverableViewSet( viewsets.GenericViewSet ): permission_classes = ( - permissions.IsAuthenticated, IsApplyStaffUser | IsFinance1User, + permissions.IsAuthenticated, HasDeliverableEditPermission, + IsApplyStaffUser | IsFinance1User | IsFinance2User ) serializer_class = InvoiceDeliverableListSerializer pagination_class = None @@ -81,7 +88,7 @@ class InvoiceRequiredChecksViewSet( viewsets.GenericViewSet, ): serializer_class = InvoiceRequiredChecksSerializer - permission_classes = [IsFinance1User] + permission_classes = [IsFinance1User, HasRequiredChecksPermission] queryset = Invoice.objects.all() @action(detail=True, methods=['post']) diff --git a/hypha/apply/dashboard/views.py b/hypha/apply/dashboard/views.py index 26a81ef34ba6cc57dc88c6802622b1613dc5d806..ac7a22f69742c0ee3453ac163546ea4002e84a6c 100644 --- a/hypha/apply/dashboard/views.py +++ b/hypha/apply/dashboard/views.py @@ -164,7 +164,7 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView): if self.request.user.is_finance_level_2: invoices = Invoice.objects.approved_by_finance_1() else: - invoices = Invoice.objects.approved_by_staff() + invoices = Invoice.objects.for_finance_1() return { 'count': invoices.count(), diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index ecc01ce231a6b166d110289357754007bbe72394..3f4a46826cb8589919b3c3aca252a7a0d09b0801 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -49,18 +49,18 @@ class ChangeInvoiceStatusForm(forms.ModelForm): 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, DECLINED, + 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, DECLINED + 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, DECLINED], user_choices), + APPROVED_BY_FINANCE_1: filter_request_choices([CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], user_choices), } status_field.choices = possible_status_transitions_lut.get(instance.status, []) diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index c2f5d52b7df6a05d6e8d3ff12df1a72cbc5d6a4c..8cdc4829629c3300373cf61ae0210907ff712329 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -45,8 +45,8 @@ INVOICE_TRANISTION_TO_RESUBMITTED = [ ] INVOICE_STATUS_PM_CHOICES = [CHANGES_REQUESTED_BY_STAFF, APPROVED_BY_STAFF, DECLINED] -INVOICE_STATUS_FINANCE_1_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1, DECLINED] -INVOICE_STATUS_FINANCE_2_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2, PAID, DECLINED] +INVOICE_STATUS_FINANCE_1_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_1, APPROVED_BY_FINANCE_1] +INVOICE_STATUS_FINANCE_2_CHOICES = [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2, PAID] def invoice_status_user_choices(user): @@ -77,6 +77,9 @@ class InvoiceQueryset(models.QuerySet): def approved_by_finance_1(self): return self.filter(status=APPROVED_BY_FINANCE_1) + def for_finance_1(self): + return self.filter(status__in=[APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2]) + def rejected(self): return self.filter(status=DECLINED) @@ -211,11 +214,11 @@ class Invoice(models.Model): return True if user.is_finance_level_1: - if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_1, CHANGES_REQUESTED_BY_FINANCE_2}: + if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2}: return True if user.is_finance_level_2: - if self.status in {CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_1}: + if self.status in {APPROVED_BY_FINANCE_1}: return True return False @@ -231,6 +234,20 @@ class Invoice(models.Model): return True return False + def can_user_edit_deliverables(self, user): + if not (user.is_apply_staff or user.is_finance_level_1 or user.is_finance_level_2): + return False + if user.is_apply_staff: + if self.status in {SUBMITTED, RESUBMITTED, CHANGES_REQUESTED_BY_FINANCE_1}: + return True + if user.is_finance_level_1: + if self.status in {APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE_2}: + return True + if user.is_finance_level_2: + if self.status in {APPROVED_BY_FINANCE_1}: + return True + return False + @property def value(self): return self.paid_value or self.amount diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 2a46509deda05bf5c8b3f0dca05a1bd3cf35b404..96461908df787c4f715fcb78e9b9a2b6846a1ccf 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -21,7 +21,6 @@ from wagtail.contrib.settings.models import BaseSetting, register_setting from wagtail.core.fields import StreamField from addressfield.fields import ADDRESS_FIELDS_ORDER -from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.funds.models.mixins import AccessFormData from hypha.apply.stream_forms.blocks import FormFieldsBlock from hypha.apply.stream_forms.files import StreamFieldDataEncoder @@ -345,18 +344,18 @@ class Project(BaseStreamForm, AccessFormData, models.Model): def has_deliverables(self): return self.deliverables.exists() - def send_to_compliance(self, request): - """Notify Compliance about this Project.""" + # def send_to_compliance(self, request): + # """Notify Compliance about this Project.""" - messenger( - MESSAGES.SENT_TO_COMPLIANCE, - request=request, - user=request.user, - source=self, - ) + # messenger( + # MESSAGES.SENT_TO_COMPLIANCE, + # request=request, + # user=request.user, + # source=self, + # ) - self.sent_to_compliance_at = timezone.now() - self.save(update_fields=['sent_to_compliance_at']) + # self.sent_to_compliance_at = timezone.now() + # self.save(update_fields=['sent_to_compliance_at']) @register_setting diff --git a/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html b/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html index ab39e81b947032a661b30f8c6dad6d233a7d8af3..d1df43682490d05992e3ff85fc6ef7c8715b4ec6 100644 --- a/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html +++ b/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html @@ -1,5 +1,6 @@ -{% load i18n %} +{% load i18n invoice_tools %} <div class="sidebar__inner"> + {% can_edit_deliverables invoice user as user_can_edit_deliverables %} <h5>{% trans "Choose deliverables" %}</h5> <form id="add-deliverables", action="" data-projectid="{{ project.id }}" data-invoiceid="{{ invoice.id }}"> <div class="select-deliverables"> @@ -11,22 +12,25 @@ </select> </div> <br> - <div class="available-to-invoice"> - <b>{% trans "Available to invoice:" %} </b> - </div> - <br> + {% if user_can_edit_deliverables %} + <div class="available-to-invoice"> + <b>{% trans "Available to invoice:" %} </b> + </div> + <br> + {% endif %} + <div class="quantity"> <b><label for="quantity">{% trans "Quantity:" %}</label></b> <input type="number" id="quantity" name="quantity" min="1"> </div> <br> - <input type="submit" value="Add Deliverable"> + <input type="submit" value="Add Deliverable" {% if not user_can_edit_deliverables %}disabled{% endif %}> </form> <br> <div id="list-deliverables"> <div class="deliverables"> {% for deliverable in invoice.deliverables.all %} - <b>{{ deliverable.deliverable.name }} ({{ deliverable.quantity }} {{ CURRENCY_SYMBOL }}{{deliverable.deliverable.unit_price}})</b><a href="{% url "api:v1:remove-deliverables" pk=deliverable.pk invoice_pk=invoice.pk project_pk=project.pk %}"> {% trans "Remove" %}</a> + <b>{{ deliverable.deliverable.name }} ({{ deliverable.quantity }} {{ CURRENCY_SYMBOL }}{{deliverable.deliverable.unit_price}})</b>{% if user_can_edit_deliverables %}<a href="{% url "api:v1:remove-deliverables" pk=deliverable.pk invoice_pk=invoice.pk project_pk=project.pk %}"> {% trans "Remove" %}</a>{% endif %}<br> {% endfor %} </div> {% if invoice.deliverables_total_amount.total %} diff --git a/hypha/apply/projects/templatetags/invoice_tools.py b/hypha/apply/projects/templatetags/invoice_tools.py index e416add7127bf59b242de92e2576f4ae97d92c95..84745c623e180a46b0169513377dc7658a555188 100644 --- a/hypha/apply/projects/templatetags/invoice_tools.py +++ b/hypha/apply/projects/templatetags/invoice_tools.py @@ -44,3 +44,8 @@ def can_complete_required_checks(invoice, user): @register.simple_tag def can_view_required_checks(invoice, user): return invoice.can_user_view_required_checks(user) + + +@register.simple_tag +def can_edit_deliverables(invoice, user): + return invoice.can_user_edit_deliverables(user) diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index 9d9175be59a3ef5bf795e7bbaf8d7015d32acc6a..0bc20a25b5412743d83651d0da37320183d4bf33 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -119,7 +119,7 @@ class CreateApprovalView(DelegatedViewMixin, CreateView): source=project, ) - project.send_to_compliance(self.request) + # project.send_to_compliance(self.request) project.is_locked = False project.status = CONTRACTING diff --git a/hypha/apply/users/forms.py b/hypha/apply/users/forms.py index 02d15e3e722d51655ef560bf122351622174004f..e31c91990db91822aa50eb1d8fd031973b5baca2 100644 --- a/hypha/apply/users/forms.py +++ b/hypha/apply/users/forms.py @@ -49,7 +49,7 @@ class ProfileForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if not self.instance.is_apply_staff: + if not self.instance.is_apply_staff_or_finance: del self.fields['slack'] if not self.instance.has_usable_password(): diff --git a/hypha/apply/users/models.py b/hypha/apply/users/models.py index a559bd4fba7d6d3ffa65756a43e3455cf03f7a4f..f23aeab93173db51c7d1819762e70fb7e106d9fe 100644 --- a/hypha/apply/users/models.py +++ b/hypha/apply/users/models.py @@ -149,6 +149,10 @@ class User(AbstractUser): def is_apply_staff(self): return self.groups.filter(name=STAFF_GROUP_NAME).exists() or self.is_superuser + @cached_property + def is_apply_staff_or_finance(self): + return self.is_apply_staff or self.is_finance + @cached_property def is_apply_staff_admin(self): return self.groups.filter(name=TEAMADMIN_GROUP_NAME).exists() or self.is_superuser