From 11fd75c8ba694d561a03608c07e7d04e5f9f6aa2 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Fri, 9 Aug 2024 18:20:43 -0400 Subject: [PATCH 1/6] Picklist improvements & rought initial HTMX implemention for invoice updating --- hypha/apply/activity/adapters/utils.py | 2 +- hypha/apply/projects/forms/payment.py | 17 ++- .../includes/invoices.html | 132 ++++++------------ .../includes/update_invoice_form.html | 13 ++ .../partials/invoice_status.html | 33 +++++ .../projects/templatetags/invoice_tools.py | 5 + hypha/apply/projects/urls.py | 12 ++ hypha/apply/projects/views/__init__.py | 2 + hypha/apply/projects/views/payment.py | 73 +++++++++- .../apply/projects/views/project_partials.py | 10 ++ 10 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 hypha/apply/projects/templates/application_projects/includes/update_invoice_form.html create mode 100644 hypha/apply/projects/templates/application_projects/partials/invoice_status.html diff --git a/hypha/apply/activity/adapters/utils.py b/hypha/apply/activity/adapters/utils.py index 0e2bd8eb2a..24db3450c6 100644 --- a/hypha/apply/activity/adapters/utils.py +++ b/hypha/apply/activity/adapters/utils.py @@ -136,4 +136,4 @@ def get_users_for_groups(groups, user_queryset=None, exact_match=False): user_queryset = user_queryset.filter(groups__name=groups.pop().name) return get_users_for_groups(groups, user_queryset=user_queryset) else: - return user_queryset + return user_queryset if user_queryset is not None else set() diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index 4896527c6c..c7eb4fd17f 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -5,6 +5,7 @@ from django.core.files.base import ContentFile from django.db import transaction from django.db.models.fields.files import FieldFile +from django.forms.widgets import RadioSelect from django.utils.translation import gettext_lazy as _ from django_file_form.forms import FileFormMixin @@ -87,16 +88,22 @@ class ChangeInvoiceStatusForm(forms.ModelForm): class Meta: fields = ["status", "comment"] model = Invoice + widgets = {"status": RadioSelect} - def __init__(self, instance, user, *args, **kwargs): + def __init__(self, *args, **kwargs): + user = kwargs.pop("user") + instance = kwargs.pop("instance") super().__init__(*args, **kwargs, instance=instance) - self.initial["comment"] = "" - status_field = self.fields["status"] - - status_field.choices = get_invoice_possible_transition_for_user( + invoice_choices = get_invoice_possible_transition_for_user( user, invoice=instance ) + self.initial["comment"] = "" + if len(invoice_choices) > 4: + self.fields["status"] = forms.TypedChoiceField(choices=invoice_choices) + else: + self.fields["status"].choices = invoice_choices + class InvoiceBaseForm(forms.ModelForm): class Meta: diff --git a/hypha/apply/projects/templates/application_projects/includes/invoices.html b/hypha/apply/projects/templates/application_projects/includes/invoices.html index 79f11cbe78..13022b741d 100644 --- a/hypha/apply/projects/templates/application_projects/includes/invoices.html +++ b/hypha/apply/projects/templates/application_projects/includes/invoices.html @@ -24,91 +24,51 @@ {% for invoice in object.invoices.not_rejected %} - {% display_invoice_status_for_user user invoice as invoice_status %} - - {% trans "Date submitted" %}: {{ invoice.requested_at.date }} - {% trans "Invoice number" %}: {{ invoice.invoice_number }} - {% trans "Status" %}: {{ invoice_status }} - - - {% heroicon_micro "eye" aria_hidden=true class="me-1" %} - {% trans "View" %} - - {% can_edit invoice user as user_can_edit_request %} - {% if user_can_edit_request %} - - {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} - {% trans "Edit" %} - - {% endif %} + {% invoice_htmx_triggers invoice as htmx_trig %} + + {% trans "Date submitted" %}: + {% trans "Invoice number" %}: + {% trans "Status" %}: + + + {% endfor %} + + + {% else %} +

{% trans "No active invoices yet." %}

+ {% endif %} - {% can_delete invoice user as user_can_delete_request %} - {% if user.is_applicant and user_can_delete_request %} - - {% heroicon_micro "trash" aria_hidden=true class="me-1" %} - {% trans "Delete" %} - - {% endif %} - {% can_change_status invoice user as can_change_invoice_status %} - {% if can_change_invoice_status %} - - {% trans "Update Status" %} - - - {% endif %} - - - {% endfor %} - - - {% else %} -

{% trans "No active invoices yet." %}

- {% endif %} - - {% if object.invoices.rejected %} -

- {% trans "Show rejected" %} -

+ {% if object.invoices.rejected %} +

+ {% trans "Show rejected" %} +

- - - - - - - - - - - {% for invoice in object.invoices.rejected %} - {% display_invoice_status_for_user user invoice as invoice_status %} - - - - - - - {% endfor %} - - - {% endif %} - - + + + + + + + + + + + {% for invoice in object.invoices.rejected %} + {% display_invoice_status_for_user user invoice as invoice_status %} + + + + + + + {% endfor %} + + + {% endif %} + + diff --git a/hypha/apply/projects/templates/application_projects/includes/update_invoice_form.html b/hypha/apply/projects/templates/application_projects/includes/update_invoice_form.html new file mode 100644 index 0000000000..d2ec4b8e55 --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/includes/update_invoice_form.html @@ -0,0 +1,13 @@ +{% load i18n %} + +{% modal_title %}Update Invoice Status{% endmodal_title %} + +
+

{% trans "Current status" %}: {{ invoice_status }}

+ + {% include 'funds/includes/dialog_form_base.html' with form=form value=value form_id=form_id %} +
\ No newline at end of file diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_status.html b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html new file mode 100644 index 0000000000..1cce3b714a --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html @@ -0,0 +1,33 @@ +{% load i18n invoice_tools heroicons %} + +{% display_invoice_status_for_user user invoice as invoice_status %} +{% trans "Date submitted" %}: {{ invoice.requested_at.date }} +{% trans "Invoice number" %}: {{ invoice.invoice_number }} +{% trans "Status" %}: {{ invoice_status }} + + + {% heroicon_micro "eye" aria_hidden=true class="me-1" %} + {% trans "View" %} + + {% can_edit invoice user as user_can_edit_request %} + {% if user_can_edit_request %} + + {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} + {% trans "Edit" %} + + {% endif %} + + {% can_delete invoice user as user_can_delete_request %} + {% if user.is_applicant and user_can_delete_request %} + + {% heroicon_micro "trash" aria_hidden=true class="me-1" %} + {% trans "Delete" %} + + {% endif %} + {% can_change_status invoice user as can_change_invoice_status %} + {% if can_change_invoice_status %} + + {% endif %} + \ No newline at end of file diff --git a/hypha/apply/projects/templatetags/invoice_tools.py b/hypha/apply/projects/templatetags/invoice_tools.py index 45ffd141be..7e0d2b17de 100644 --- a/hypha/apply/projects/templatetags/invoice_tools.py +++ b/hypha/apply/projects/templatetags/invoice_tools.py @@ -157,3 +157,8 @@ def invoice_status_fg_color(invoice_status): @register.simple_tag def display_invoice_table_status_for_user(status, user): return get_invoice_table_status(status, is_applicant=user.is_applicant) + + +@register.simple_tag +def invoice_htmx_triggers(invoice) -> str: + return f"load, invoiceUpdated-{invoice.id} from:body" diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index d6e109cef7..fbb9c68543 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -3,6 +3,7 @@ from .views import ( CategoryTemplatePrivateMediaView, + ChangeInvoiceStatusView, ContractDocumentPrivateMediaView, ContractPrivateMediaView, CreateInvoiceView, @@ -29,6 +30,7 @@ VendorPrivateMediaView, get_invoices_status_counts, get_project_status_counts, + partial_get_invoice_status, partial_project_activities, ) @@ -110,6 +112,11 @@ path( "edit/", EditInvoiceView.as_view(), name="invoice-edit" ), + path( + "update/", + ChangeInvoiceStatusView.as_view(), + name="invoice-update", + ), path( "delete/", DeleteInvoiceView.as_view(), @@ -125,6 +132,11 @@ InvoicePrivateMedia.as_view(), name="invoice-supporting-document", ), + path( + "partial/invoice-status/", + partial_get_invoice_status, + name="partial-invoice-status", + ), ] ), ), diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py index 0d53a36764..35e1cacbb6 100644 --- a/hypha/apply/projects/views/__init__.py +++ b/hypha/apply/projects/views/__init__.py @@ -33,6 +33,7 @@ from .project_partials import ( get_invoices_status_counts, get_project_status_counts, + partial_get_invoice_status, partial_project_activities, ) from .report import ( @@ -47,6 +48,7 @@ __all__ = [ "partial_project_activities", + "partial_get_invoice_status", "get_invoices_status_counts", "get_project_status_counts", "ChangeInvoiceStatusView", diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 81c147c96e..67c7b9c90c 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -1,3 +1,5 @@ +import json + from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -5,7 +7,8 @@ from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from django.db import transaction -from django.shortcuts import get_object_or_404, redirect +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.utils import timezone from django.utils.decorators import method_decorator @@ -23,6 +26,9 @@ from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.activity.models import APPLICANT, COMMENT, Activity +from hypha.apply.projects.templatetags.invoice_tools import ( + display_invoice_status_for_user, +) from hypha.apply.todo.options import ( INVOICE_REQUIRED_CHANGES, INVOICE_WAITING_APPROVAL, @@ -91,12 +97,35 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView form_class = ChangeInvoiceStatusForm context_name = "change_invoice_status" model = Invoice + template = "application_projects/includes/update_invoice_form.html" + + def dispatch(self, request, *args, **kwargs): + self.object = get_object_or_404(Invoice, id=kwargs.get("invoice_pk")) + return super().dispatch(request, *args, **kwargs) + + def get(self, *args, **kwargs): + form_instance = self.form_class(instance=self.object, user=self.request.user) + form_instance.name = self.context_name + + return render( + self.request, + self.template, + context={ + "form": form_instance, + "form_id": f"{form_instance.name}-{self.object.id}", + "invoice_status": display_invoice_status_for_user( + self.request.user, self.object + ), + "value": _("Update status"), + "object": self.object, + }, + ) + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) def form_valid(self, form): - invoice = get_object_or_404( - Invoice, pk=self.kwargs["invoice_pk"] - ) # to get the old status - old_status = invoice.status + old_status = self.object.status response = super().form_valid(form) if form.cleaned_data["comment"]: invoice_status_change = _( @@ -144,6 +173,40 @@ def form_valid(self, form): return response + def post(self, *args, **kwargs): + form = ChangeInvoiceStatusForm( + self.request.POST, instance=self.object, user=self.request.user + ) + if form.is_valid(): + self.form_valid(form) + return HttpResponse( + status=204, + headers={ + "HX-Trigger": json.dumps( + { + f"invoiceUpdated-{self.object.id}": None, + "showMessage": "Invoice updated.", + } + ) + }, + ) + + # TODO: get_context_data method to not duplicate code + return render( + self.request, + self.template, + context={ + "form": form, + "form_id": f"{form.name}-{self.object.id}", + "invoice_status": display_invoice_status_for_user( + self.request.user, self.object + ), + "value": _("Update status"), + "object": self.object, + }, + status=400, + ) + class DeleteInvoiceView(DeleteView): model = Invoice diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index fb1ebd1d40..517425e478 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -104,3 +104,13 @@ def get_invoices_status_counts(request): "type": _("Invoices"), }, ) + + +@login_required +def partial_get_invoice_status(request, pk, invoice_pk): + invoice = get_object_or_404(Invoice, pk=invoice_pk) + return render( + request, + "application_projects/partials/invoice_status.html", + context={"invoice": invoice, "user": request.user}, + ) From 5afec101b5c2b67bc2e26af3c606d9626b9504a7 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Mon, 12 Aug 2024 17:19:26 -0400 Subject: [PATCH 2/6] Swapped for full table updates & included rejected table --- .../includes/invoices.html | 75 +++++++------------ .../partials/invoice_status.html | 66 ++++++++-------- hypha/apply/projects/urls.py | 17 +++-- hypha/apply/projects/views/payment.py | 70 ++++++++--------- .../apply/projects/views/project_partials.py | 11 ++- 5 files changed, 113 insertions(+), 126 deletions(-) diff --git a/hypha/apply/projects/templates/application_projects/includes/invoices.html b/hypha/apply/projects/templates/application_projects/includes/invoices.html index 13022b741d..57eb54d0af 100644 --- a/hypha/apply/projects/templates/application_projects/includes/invoices.html +++ b/hypha/apply/projects/templates/application_projects/includes/invoices.html @@ -22,53 +22,32 @@ - - {% for invoice in object.invoices.not_rejected %} - {% invoice_htmx_triggers invoice as htmx_trig %} - - {% trans "Date submitted" %}: - {% trans "Invoice number" %}: - {% trans "Status" %}: - - - {% endfor %} - - - {% else %} -

{% trans "No active invoices yet." %}

- {% endif %} + + {% include "application_projects/partials/invoice_status.html" with invoices=object.invoices.not_rejected rejected=False %} + + + {% else %} +

{% trans "No active invoices yet." %}

+ {% endif %} - {% if object.invoices.rejected %} -

- {% trans "Show rejected" %} -

+ {% if object.invoices.rejected %} +

+ {% trans "Show rejected" %} +

- - - - - - - - - - - {% for invoice in object.invoices.rejected %} - {% display_invoice_status_for_user user invoice as invoice_status %} - - - - - - - {% endfor %} - - - {% endif %} - - + + + + + + + + + + + {% include "application_projects/partials/invoice_status.html" with invoices=object.invoices.rejected rejected=True %} + + + {% endif %} + + diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_status.html b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html index 1cce3b714a..69feec1ee2 100644 --- a/hypha/apply/projects/templates/application_projects/partials/invoice_status.html +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html @@ -1,33 +1,39 @@ {% load i18n invoice_tools heroicons %} -{% display_invoice_status_for_user user invoice as invoice_status %} -{% trans "Date submitted" %}: {{ invoice.requested_at.date }} -{% trans "Invoice number" %}: {{ invoice.invoice_number }} -{% trans "Status" %}: {{ invoice_status }} - - - {% heroicon_micro "eye" aria_hidden=true class="me-1" %} - {% trans "View" %} - - {% can_edit invoice user as user_can_edit_request %} - {% if user_can_edit_request %} - - {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} - {% trans "Edit" %} - - {% endif %} +{% for invoice in invoices %} + + {% display_invoice_status_for_user user invoice as invoice_status %} + {% trans "Date submitted" %}: {{ invoice.requested_at.date }} + {% trans "Invoice number" %}: {{ invoice.invoice_number }} + {% trans "Status" %}: {{ invoice_status }} + + + {% heroicon_micro "eye" aria_hidden=true class="me-1" %} + {% trans "View" %} + + {% if not rejected %} + {% can_edit invoice user as user_can_edit_request %} + {% if user_can_edit_request %} + + {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} + {% trans "Edit" %} + + {% endif %} - {% can_delete invoice user as user_can_delete_request %} - {% if user.is_applicant and user_can_delete_request %} - - {% heroicon_micro "trash" aria_hidden=true class="me-1" %} - {% trans "Delete" %} - - {% endif %} - {% can_change_status invoice user as can_change_invoice_status %} - {% if can_change_invoice_status %} - - {% endif %} - \ No newline at end of file + {% can_delete invoice user as user_can_delete_request %} + {% if user.is_applicant and user_can_delete_request %} + + {% heroicon_micro "trash" aria_hidden=true class="me-1" %} + {% trans "Delete" %} + + {% endif %} + {% can_change_status invoice user as can_change_invoice_status %} + {% if can_change_invoice_status %} + + {% endif %} + {% endif %} + + +{% endfor %} \ No newline at end of file diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index fbb9c68543..9001905616 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -104,6 +104,18 @@ VendorPrivateMediaView.as_view(), name="vendor-documents", ), + path( + "partial/invoice-status/", + partial_get_invoice_status, + {"rejected": False}, + name="partial-invoice-status", + ), + path( + "partial/rejected-invoice-status/", + partial_get_invoice_status, + {"rejected": True}, + name="partial-rejected-invoice-status", + ), path( "invoices//", include( @@ -132,11 +144,6 @@ InvoicePrivateMedia.as_view(), name="invoice-supporting-document", ), - path( - "partial/invoice-status/", - partial_get_invoice_status, - name="partial-invoice-status", - ), ] ), ), diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 67c7b9c90c..9d235e6fd1 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -63,6 +63,7 @@ APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE, CHANGES_REQUESTED_BY_STAFF, + DECLINED, INVOICE_TRANISTION_TO_RESUBMITTED, Invoice, ) @@ -100,29 +101,26 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView template = "application_projects/includes/update_invoice_form.html" def dispatch(self, request, *args, **kwargs): - self.object = get_object_or_404(Invoice, id=kwargs.get("invoice_pk")) + self.object: Invoice = get_object_or_404(Invoice, id=kwargs.get("invoice_pk")) return super().dispatch(request, *args, **kwargs) - def get(self, *args, **kwargs): - form_instance = self.form_class(instance=self.object, user=self.request.user) - form_instance.name = self.context_name + def get_context_data(self, **kwargs): + if not (form := kwargs.get("form")): + form = self.form_class(instance=self.object, user=self.request.user) - return render( - self.request, - self.template, - context={ - "form": form_instance, - "form_id": f"{form_instance.name}-{self.object.id}", - "invoice_status": display_invoice_status_for_user( - self.request.user, self.object - ), - "value": _("Update status"), - "object": self.object, - }, - ) + form.name = self.context_name - def get_context_data(self, **kwargs): - return super().get_context_data(**kwargs) + extras = { + "form": form, + "form_id": f"{form.name}-{self.object.id}", + "invoice_status": display_invoice_status_for_user( + self.request.user, self.object + ), + "value": _("Update status"), + "object": self.object, + } + + return {**kwargs, **extras} def form_valid(self, form): old_status = self.object.status @@ -131,7 +129,7 @@ def form_valid(self, form): invoice_status_change = _( "

Invoice status updated to: {status}.

" ).format(status=self.object.get_status_display()) - comment = f"

{self.object.comment}.

" + comment = f"

{self.object.comment}

" message = invoice_status_change + comment @@ -173,37 +171,29 @@ def form_valid(self, form): return response + def get(self, *args, **kwargs): + form_instance = self.form_class(instance=self.object, user=self.request.user) + form_instance.name = self.context_name + + return render(self.request, self.template, self.get_context_data()) + def post(self, *args, **kwargs): - form = ChangeInvoiceStatusForm( + form = self.form_class( self.request.POST, instance=self.object, user=self.request.user ) if form.is_valid(): self.form_valid(form) + htmx_headers = {"invoicesUpdated": None, "showMessage": "Invoice updated."} + if self.object.status == DECLINED: + htmx_headers.update({"rejectedInvoicesUpdated": None}) return HttpResponse( - status=204, - headers={ - "HX-Trigger": json.dumps( - { - f"invoiceUpdated-{self.object.id}": None, - "showMessage": "Invoice updated.", - } - ) - }, + status=204, headers={"HX-Trigger": json.dumps(htmx_headers)} ) - # TODO: get_context_data method to not duplicate code return render( self.request, self.template, - context={ - "form": form, - "form_id": f"{form.name}-{self.object.id}", - "invoice_status": display_invoice_status_for_user( - self.request.user, self.object - ), - "value": _("Update status"), - "object": self.object, - }, + self.get_context_data(form=form), status=400, ) diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index 517425e478..00a34bb62b 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -107,10 +107,15 @@ def get_invoices_status_counts(request): @login_required -def partial_get_invoice_status(request, pk, invoice_pk): - invoice = get_object_or_404(Invoice, pk=invoice_pk) +def partial_get_invoice_status(request, pk: int, rejected: bool): + invoices = get_object_or_404(Project, pk=pk).invoices + return render( request, "application_projects/partials/invoice_status.html", - context={"invoice": invoice, "user": request.user}, + context={ + "invoices": invoices.rejected if rejected else invoices.not_rejected, + "user": request.user, + "rejected": rejected, + }, ) From 62a7614d7a4bf8b4ab62c5398bb1fa9b7e91b2e8 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Mon, 12 Aug 2024 17:20:17 -0400 Subject: [PATCH 3/6] Removed unneeded template tag --- hypha/apply/projects/templatetags/invoice_tools.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hypha/apply/projects/templatetags/invoice_tools.py b/hypha/apply/projects/templatetags/invoice_tools.py index 7e0d2b17de..45ffd141be 100644 --- a/hypha/apply/projects/templatetags/invoice_tools.py +++ b/hypha/apply/projects/templatetags/invoice_tools.py @@ -157,8 +157,3 @@ def invoice_status_fg_color(invoice_status): @register.simple_tag def display_invoice_table_status_for_user(status, user): return get_invoice_table_status(status, is_applicant=user.is_applicant) - - -@register.simple_tag -def invoice_htmx_triggers(invoice) -> str: - return f"load, invoiceUpdated-{invoice.id} from:body" From 7549fcb6d2b3ae28f06efbf07d7f9ad908026675 Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 13 Aug 2024 17:48:24 -0400 Subject: [PATCH 4/6] Added unit testing & small code cleanup --- hypha/apply/projects/tests/test_views.py | 67 +++++++++++++++++-- hypha/apply/projects/urls.py | 1 - .../apply/projects/views/project_partials.py | 17 ++++- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py index 25ce610e07..f22f3bc2a9 100644 --- a/hypha/apply/projects/tests/test_views.py +++ b/hypha/apply/projects/tests/test_views.py @@ -11,6 +11,7 @@ from django.utils import timezone from hypha.apply.funds.tests.factories import LabSubmissionFactory +from hypha.apply.projects.utils import get_invoice_status_display_value from hypha.apply.users.tests.factories import ( ApplicantFactory, ApproverFactory, @@ -27,7 +28,7 @@ from ...funds.models.forms import ApplicationBaseProjectReportForm from ..files import get_files from ..forms import SetPendingForm -from ..models.payment import CHANGES_REQUESTED_BY_STAFF, SUBMITTED +from ..models.payment import CHANGES_REQUESTED_BY_STAFF, DECLINED, SUBMITTED from ..models.project import ( APPROVE, CLOSING, @@ -1069,12 +1070,70 @@ def test_can(self): "comment": "this is a comment", }, ) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 204) + self.assertTrue("invoicesUpdated" in response.headers.get("HX-Trigger", "")) invoice.refresh_from_db() self.assertEqual(invoice.status, CHANGES_REQUESTED_BY_STAFF) + def test_can_view_updated_invoice(self): + project = ProjectFactory() + invoice = InvoiceFactory(project=project) + response = self.post_page( + invoice, + { + "form-submitted-change_invoice_status": "", + "status": CHANGES_REQUESTED_BY_STAFF, + "comment": "this is a comment", + }, + ) + self.assertEqual(response.status_code, 204) + self.assertTrue("invoicesUpdated" in response.headers.get("HX-Trigger", "")) + response = self.client.get( + reverse("apply:projects:partial-invoice-status", kwargs={"pk": project.pk}), + secure=True, + follow=True, + ) + self.assertContains( + response, get_invoice_status_display_value(CHANGES_REQUESTED_BY_STAFF) + ) + + def test_can_view_updated_rejected_invoice(self): + project = ProjectFactory() + invoice = InvoiceFactory(project=project) + response = self.post_page( + invoice, + { + "form-submitted-change_invoice_status": "", + "status": DECLINED, + "comment": "this is a comment", + }, + ) + self.assertEqual(response.status_code, 204) + self.assertTrue("invoicesUpdated" in response.headers.get("HX-Trigger", "")) + self.assertTrue( + "rejectedInvoicesUpdated" in response.headers.get("HX-Trigger", "") + ) + response = self.client.get( + reverse("apply:projects:partial-invoice-status", kwargs={"pk": project.pk}), + secure=True, + follow=True, + ) + self.assertNotContains(response, get_invoice_status_display_value(DECLINED)) + + rejected_response = self.client.get( + reverse( + "apply:projects:partial-rejected-invoice-status", + kwargs={"pk": project.pk}, + ), + secure=True, + follow=True, + ) + self.assertContains( + rejected_response, get_invoice_status_display_value(DECLINED) + ) + -class TestApplicantChangeInoviceStatus(BaseViewTestCase): +class TestApplicantChangeInvoiceStatus(BaseViewTestCase): base_view_name = "invoice-detail" url_name = "funds:projects:{}" user_factory = ApplicantFactory @@ -1109,7 +1168,7 @@ def test_other_cant(self): self.assertEqual(invoice.status, SUBMITTED) -class TestStaffInoviceDocumentPrivateMedia(BaseViewTestCase): +class TestStaffInvoiceDocumentPrivateMedia(BaseViewTestCase): base_view_name = "invoice-document" url_name = "funds:projects:{}" user_factory = StaffFactory diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index 9001905616..e5aec877da 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -107,7 +107,6 @@ path( "partial/invoice-status/", partial_get_invoice_status, - {"rejected": False}, name="partial-invoice-status", ), path( diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index 00a34bb62b..db7ad63087 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -1,7 +1,9 @@ +from typing import Optional from urllib.parse import parse_qs, urlparse from django.contrib.auth.decorators import login_required from django.db.models import Count +from django.http import HttpRequest from django.shortcuts import get_object_or_404, render from django.urls import reverse_lazy from django.utils.translation import gettext as _ @@ -107,7 +109,20 @@ def get_invoices_status_counts(request): @login_required -def partial_get_invoice_status(request, pk: int, rejected: bool): +def partial_get_invoice_status( + request: HttpRequest, pk: int, rejected: Optional[bool] = False +): + """ + Partial to get the invoice status table + + Args: + request: request used to retrieve partial + pk: PK of the project to retrieve invoices for + rejected: retrieve rejected invoices, by default only retrieves not rejected invoices + + Returns: + HttpResponse containing the table of requested invoices + """ invoices = get_object_or_404(Project, pk=pk).invoices return render( From ebb7e0d8310abd9906e9bfa7760de69d2c98abca Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Wed, 14 Aug 2024 14:01:19 -0400 Subject: [PATCH 5/6] Implemented HTMX for invoice detail view --- .../includes/invoices.html | 8 +- .../invoice_admin_detail.html | 30 -------- .../application_projects/invoice_detail.html | 74 ++---------------- .../partials/invoice_detail_actions.html | 46 +++++++++++ .../partials/invoice_status.html | 76 ++++++++++--------- .../partials/invoice_status_table.html | 39 ++++++++++ hypha/apply/projects/tests/test_views.py | 10 ++- hypha/apply/projects/urls.py | 20 ++++- hypha/apply/projects/views/__init__.py | 4 + hypha/apply/projects/views/payment.py | 32 ++------ .../apply/projects/views/project_partials.py | 59 +++++++++++++- hypha/static_src/sass/components/_button.scss | 5 +- 12 files changed, 230 insertions(+), 173 deletions(-) create mode 100644 hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html create mode 100644 hypha/apply/projects/templates/application_projects/partials/invoice_status_table.html diff --git a/hypha/apply/projects/templates/application_projects/includes/invoices.html b/hypha/apply/projects/templates/application_projects/includes/invoices.html index 57eb54d0af..9a2ae604ba 100644 --- a/hypha/apply/projects/templates/application_projects/includes/invoices.html +++ b/hypha/apply/projects/templates/application_projects/includes/invoices.html @@ -22,8 +22,8 @@ - - {% include "application_projects/partials/invoice_status.html" with invoices=object.invoices.not_rejected rejected=False %} + + {% include "application_projects/partials/invoice_status_table.html" with invoices=object.invoices.not_rejected rejected=False %} {% else %} @@ -44,8 +44,8 @@ - - {% include "application_projects/partials/invoice_status.html" with invoices=object.invoices.rejected rejected=True %} + + {% include "application_projects/partials/invoice_status_table.html" with invoices=object.invoices.rejected rejected=True %} {% endif %} diff --git a/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html b/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html index 35df7c6a30..521956e33b 100644 --- a/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html +++ b/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html @@ -7,37 +7,7 @@ {% endif %} {% endblock %} -{% block actions %} - {{ block.super }} - {% can_change_status object user as user_can_change_status %} - - {% trans "Update Invoice Status" %} - - {% if user_can_change_status %} - - {% endif %} -{% endblock %} - -{% block extra_css %} - -{% endblock %} - {% block extra_js %} {{ block.super }} - - {% endblock %} diff --git a/hypha/apply/projects/templates/application_projects/invoice_detail.html b/hypha/apply/projects/templates/application_projects/invoice_detail.html index 93738728ea..de4282d84f 100644 --- a/hypha/apply/projects/templates/application_projects/invoice_detail.html +++ b/hypha/apply/projects/templates/application_projects/invoice_detail.html @@ -28,47 +28,9 @@ {% endif %}

{% trans "Fund" %}: {{ object.project.submission.page }}

-
-
-

{% trans "Status" %}:

-
-
- {% extract_status latest_activity user as latest_activity_status %} - {% get_comment_for_invoice_action object latest_activity as latest_activity_comment %} -

{{ latest_activity_status }} {% if user.is_applicant and latest_activity.user != user %} ({{ ORG_SHORT_NAME }}){% else %}({{ latest_activity.user }}){% endif %} - {{ latest_activity.timestamp }} - {% if latest_activity_comment %} - - {% trans "View comment" %} - {% endif %} -

- {% for activity in activities %} - {% extract_status activity user as activity_status %} - {% get_comment_for_invoice_action object activity as activity_comment %} -

{{ activity_status }} {% if user.is_applicant and activity.user != user %} ({{ ORG_SHORT_NAME }}){% else %}({{ activity.user }}){% endif %} - {{ activity.timestamp }} - {% if activity_comment %} - - {% trans "View comment" %} - {% endif %} -

- {% endfor %} -
- +
+
+
@@ -90,34 +52,8 @@
{% trans "Supporting Documents" %}
diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html b/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html new file mode 100644 index 0000000000..a86e0957d7 --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html @@ -0,0 +1,46 @@ +{% load i18n invoice_tools %} + +
{% trans "Actions to take" %}
+{% can_edit object user as user_can_edit_request %} +{% if user.is_apply_staff or user.is_applicant %} + + {% trans "Edit Invoice" %} + +{% endif %} +{% can_delete object user as user_can_delete_request %} +{% if user_can_delete_request %} + + {% trans "Delete Invoice" %} + +{% endif %} + +{% if user.is_org_faculty %} + {% can_change_status object user as user_can_change_status %} + {% if user_can_change_status %} + + + {% else %} + + {% endif %} +{% endif %} \ No newline at end of file diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_status.html b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html index 69feec1ee2..2277b67ae7 100644 --- a/hypha/apply/projects/templates/application_projects/partials/invoice_status.html +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_status.html @@ -1,39 +1,43 @@ {% load i18n invoice_tools heroicons %} -{% for invoice in invoices %} - - {% display_invoice_status_for_user user invoice as invoice_status %} - {% trans "Date submitted" %}: {{ invoice.requested_at.date }} - {% trans "Invoice number" %}: {{ invoice.invoice_number }} - {% trans "Status" %}: {{ invoice_status }} - - - {% heroicon_micro "eye" aria_hidden=true class="me-1" %} - {% trans "View" %} - - {% if not rejected %} - {% can_edit invoice user as user_can_edit_request %} - {% if user_can_edit_request %} - - {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} - {% trans "Edit" %} - - {% endif %} - - {% can_delete invoice user as user_can_delete_request %} - {% if user.is_applicant and user_can_delete_request %} - - {% heroicon_micro "trash" aria_hidden=true class="me-1" %} - {% trans "Delete" %} - - {% endif %} - {% can_change_status invoice user as can_change_invoice_status %} - {% if can_change_invoice_status %} - - {% endif %} +
+

{% trans "Status" %}:

+
+
+ {% extract_status latest_activity user as latest_activity_status %} + {% get_comment_for_invoice_action object latest_activity as latest_activity_comment %} +

{{ latest_activity_status }} {% if user.is_applicant and latest_activity.user != user %} ({{ ORG_SHORT_NAME }}){% else %}({{ latest_activity.user }}){% endif %} + {{ latest_activity.timestamp }} + {% if latest_activity_comment %} + + {% trans "View comment" %} + {% endif %} +

+ {% for activity in activities %} + {% extract_status activity user as activity_status %} + {% get_comment_for_invoice_action object activity as activity_comment %} +

{{ activity_status }} {% if user.is_applicant and activity.user != user %} ({{ ORG_SHORT_NAME }}){% else %}({{ activity.user }}){% endif %} + {{ activity.timestamp }} + {% if activity_comment %} + + {% trans "View comment" %} {% endif %} - - -{% endfor %} \ No newline at end of file +

+ {% endfor %} +
+{% if activities %} + +{% endif %} \ No newline at end of file diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_status_table.html b/hypha/apply/projects/templates/application_projects/partials/invoice_status_table.html new file mode 100644 index 0000000000..69feec1ee2 --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_status_table.html @@ -0,0 +1,39 @@ +{% load i18n invoice_tools heroicons %} + +{% for invoice in invoices %} + + {% display_invoice_status_for_user user invoice as invoice_status %} + {% trans "Date submitted" %}: {{ invoice.requested_at.date }} + {% trans "Invoice number" %}: {{ invoice.invoice_number }} + {% trans "Status" %}: {{ invoice_status }} + + + {% heroicon_micro "eye" aria_hidden=true class="me-1" %} + {% trans "View" %} + + {% if not rejected %} + {% can_edit invoice user as user_can_edit_request %} + {% if user_can_edit_request %} + + {% heroicon_micro "pencil-square" aria_hidden=true class="me-1" %} + {% trans "Edit" %} + + {% endif %} + + {% can_delete invoice user as user_can_delete_request %} + {% if user.is_applicant and user_can_delete_request %} + + {% heroicon_micro "trash" aria_hidden=true class="me-1" %} + {% trans "Delete" %} + + {% endif %} + {% can_change_status invoice user as can_change_invoice_status %} + {% if can_change_invoice_status %} + + {% endif %} + {% endif %} + + +{% endfor %} \ No newline at end of file diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py index f22f3bc2a9..749ad44d96 100644 --- a/hypha/apply/projects/tests/test_views.py +++ b/hypha/apply/projects/tests/test_views.py @@ -1089,7 +1089,9 @@ def test_can_view_updated_invoice(self): self.assertEqual(response.status_code, 204) self.assertTrue("invoicesUpdated" in response.headers.get("HX-Trigger", "")) response = self.client.get( - reverse("apply:projects:partial-invoice-status", kwargs={"pk": project.pk}), + reverse( + "apply:projects:partial-invoices-status", kwargs={"pk": project.pk} + ), secure=True, follow=True, ) @@ -1114,7 +1116,9 @@ def test_can_view_updated_rejected_invoice(self): "rejectedInvoicesUpdated" in response.headers.get("HX-Trigger", "") ) response = self.client.get( - reverse("apply:projects:partial-invoice-status", kwargs={"pk": project.pk}), + reverse( + "apply:projects:partial-invoices-status", kwargs={"pk": project.pk} + ), secure=True, follow=True, ) @@ -1122,7 +1126,7 @@ def test_can_view_updated_rejected_invoice(self): rejected_response = self.client.get( reverse( - "apply:projects:partial-rejected-invoice-status", + "apply:projects:partial-rejected-invoices-status", kwargs={"pk": project.pk}, ), secure=True, diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index e5aec877da..98d86673cb 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -30,7 +30,9 @@ VendorPrivateMediaView, get_invoices_status_counts, get_project_status_counts, + partial_get_invoice_detail_actions, partial_get_invoice_status, + partial_get_invoice_status_table, partial_project_activities, ) @@ -106,14 +108,14 @@ ), path( "partial/invoice-status/", - partial_get_invoice_status, - name="partial-invoice-status", + partial_get_invoice_status_table, + name="partial-invoices-status", ), path( "partial/rejected-invoice-status/", - partial_get_invoice_status, + partial_get_invoice_status_table, {"rejected": True}, - name="partial-rejected-invoice-status", + name="partial-rejected-invoices-status", ), path( "invoices//", @@ -133,6 +135,16 @@ DeleteInvoiceView.as_view(), name="invoice-delete", ), + path( + "partial/status/", + partial_get_invoice_status, + name="partial-invoice-status", + ), + path( + "actions/", + partial_get_invoice_detail_actions, + name="partial-invoice-detail-actions", + ), path( "documents/invoice/", InvoicePrivateMedia.as_view(), diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py index 35e1cacbb6..c94770458b 100644 --- a/hypha/apply/projects/views/__init__.py +++ b/hypha/apply/projects/views/__init__.py @@ -33,7 +33,9 @@ from .project_partials import ( get_invoices_status_counts, get_project_status_counts, + partial_get_invoice_detail_actions, partial_get_invoice_status, + partial_get_invoice_status_table, partial_project_activities, ) from .report import ( @@ -48,7 +50,9 @@ __all__ = [ "partial_project_activities", + "partial_get_invoice_status_table", "partial_get_invoice_status", + "partial_get_invoice_detail_actions", "get_invoices_status_counts", "get_project_status_counts", "ChangeInvoiceStatusView", diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 9d235e6fd1..43cb025cc8 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -178,6 +178,12 @@ def get(self, *args, **kwargs): return render(self.request, self.template, self.get_context_data()) def post(self, *args, **kwargs): + # Don't process the post request if the user can't change the status + if not self.object.can_user_change_status(self.request.user): + return render( + self.request, self.template, self.get_context_data(), status=403 + ) + form = self.form_class( self.request.POST, instance=self.object, user=self.request.user ) @@ -191,10 +197,7 @@ def post(self, *args, **kwargs): ) return render( - self.request, - self.template, - self.get_context_data(form=form), - status=400, + self.request, self.template, self.get_context_data(form=form), status=400 ) @@ -241,31 +244,12 @@ def get_context_data(self, **kwargs): invoice = self.get_object() project = invoice.project deliverables = project.deliverables.all() - invoice_activities = Activity.actions.filter( - related_content_type__model="invoice", related_object_id=invoice.id - ).visible_to(self.request.user) - return super().get_context_data( - **kwargs, - deliverables=deliverables, - latest_activity=invoice_activities.first(), - activities=invoice_activities[1:], - ) + return super().get_context_data(**kwargs, deliverables=deliverables) class InvoiceApplicantView(InvoiceAccessMixin, DelegateableView, DetailView): form_views = [] - def get_context_data(self, **kwargs): - invoice = self.get_object() - invoice_activities = Activity.actions.filter( - related_content_type__model="invoice", related_object_id=invoice.id - ).visible_to(self.request.user) - return super().get_context_data( - **kwargs, - latest_activity=invoice_activities.first(), - activities=invoice_activities[1:], - ) - class InvoiceView(ViewDispatcher): admin_view = InvoiceAdminView diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index db7ad63087..ab002d72bf 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext as _ from django.views.decorators.http import require_GET +from hypha.apply.activity.models import Activity from hypha.apply.activity.services import ( get_related_actions_for_user, ) @@ -109,7 +110,7 @@ def get_invoices_status_counts(request): @login_required -def partial_get_invoice_status( +def partial_get_invoice_status_table( request: HttpRequest, pk: int, rejected: Optional[bool] = False ): """ @@ -127,10 +128,64 @@ def partial_get_invoice_status( return render( request, - "application_projects/partials/invoice_status.html", + "application_projects/partials/invoice_status_table.html", context={ "invoices": invoices.rejected if rejected else invoices.not_rejected, "user": request.user, "rejected": rejected, }, ) + + +@login_required +def partial_get_invoice_status(request: HttpRequest, pk: int, invoice_pk: int): + """ + Partial to get the invoice status for invoice detail view + + Args: + request: request used to retrieve partial + pk: ID of the associated project + invoice_pk: ID of the invoice to retrieve the status of + + Returns: + HttpResponse containing the status line of requested invoice + """ + invoice = get_object_or_404(Invoice, pk=invoice_pk) + user = request.user + invoice_activities = Activity.actions.filter( + related_content_type__model="invoice", related_object_id=invoice.id + ).visible_to(user) + + return render( + request, + "application_projects/partials/invoice_status.html", + context={ + "object": invoice, + "latest_activity": invoice_activities.first(), + "activities": invoice_activities[1:], + "user": user, + }, + ) + + +@login_required +def partial_get_invoice_detail_actions(request: HttpRequest, pk: int, invoice_pk: int): + """ + Partial to get the actions for the invoice detail view + + Args: + request: request used to retrieve partial + pk: ID of the associated project + invoice_pk: ID of the invoice to retrieve the status of + + Returns: + HttpResponse containing the status line of requested invoice + """ + invoice = get_object_or_404(Invoice, pk=invoice_pk) + user = request.user + + return render( + request, + "application_projects/partials/invoice_detail_actions.html", + context={"object": invoice, "user": user}, + ) diff --git a/hypha/static_src/sass/components/_button.scss b/hypha/static_src/sass/components/_button.scss index 1db57c1b94..665b258175 100644 --- a/hypha/static_src/sass/components/_button.scss +++ b/hypha/static_src/sass/components/_button.scss @@ -365,8 +365,11 @@ &--tooltip-disabled { background-color: $color--button-disabled; + border: 1px solid $color--button-disabled; - &:hover { + &:hover, + &:active, + &:focus { cursor: default; background-color: $color--button-disabled; } From 94b462f37aff596ccc71299e26b4aea536825d7b Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Wed, 14 Aug 2024 14:39:37 -0400 Subject: [PATCH 6/6] Added unit tests for status partial --- hypha/apply/projects/tests/test_views.py | 42 ++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py index 749ad44d96..4712c43fa1 100644 --- a/hypha/apply/projects/tests/test_views.py +++ b/hypha/apply/projects/tests/test_views.py @@ -1075,7 +1075,7 @@ def test_can(self): invoice.refresh_from_db() self.assertEqual(invoice.status, CHANGES_REQUESTED_BY_STAFF) - def test_can_view_updated_invoice(self): + def test_can_view_updated_invoice_table(self): project = ProjectFactory() invoice = InvoiceFactory(project=project) response = self.post_page( @@ -1099,7 +1099,7 @@ def test_can_view_updated_invoice(self): response, get_invoice_status_display_value(CHANGES_REQUESTED_BY_STAFF) ) - def test_can_view_updated_rejected_invoice(self): + def test_can_view_updated_rejected_invoice_table(self): project = ProjectFactory() invoice = InvoiceFactory(project=project) response = self.post_page( @@ -1136,6 +1136,44 @@ def test_can_view_updated_rejected_invoice(self): rejected_response, get_invoice_status_display_value(DECLINED) ) + def test_can_view_updated_invoice_status(self): + project = ProjectFactory() + invoice = InvoiceFactory(project=project) + + response = self.client.get( + reverse( + "apply:projects:partial-invoice-status", + kwargs={"pk": project.pk, "invoice_pk": invoice.pk}, + ), + secure=True, + follow=True, + ) + self.assertNotContains( + response, get_invoice_status_display_value(CHANGES_REQUESTED_BY_STAFF) + ) + + response = self.post_page( + invoice, + { + "form-submitted-change_invoice_status": "", + "status": CHANGES_REQUESTED_BY_STAFF, + "comment": "this is a comment", + }, + ) + self.assertEqual(response.status_code, 204) + self.assertTrue("invoicesUpdated" in response.headers.get("HX-Trigger", "")) + response = self.client.get( + reverse( + "apply:projects:partial-invoice-status", + kwargs={"pk": project.pk, "invoice_pk": invoice.pk}, + ), + secure=True, + follow=True, + ) + self.assertContains( + response, get_invoice_status_display_value(CHANGES_REQUESTED_BY_STAFF) + ) + class TestApplicantChangeInvoiceStatus(BaseViewTestCase): base_view_name = "invoice-detail"