From 010eb2b81f1579c1263020913f1ce51f28e97dfb Mon Sep 17 00:00:00 2001 From: Sandeep Chauhan Date: Sun, 31 Mar 2024 01:26:33 +0530 Subject: [PATCH] Add status update batch action to invoices table (#3756) Fixes part of #3537 --- .../apply/activity/adapters/activity_feed.py | 17 +++ hypha/apply/activity/adapters/base.py | 1 + .../migrations/0080_alter_event_type.py | 84 +++++++++++++ hypha/apply/activity/options.py | 4 + .../includes/table_filter_and_search.html | 62 +++++---- hypha/apply/projects/forms/__init__.py | 2 + hypha/apply/projects/forms/payment.py | 118 +++++++++++------- hypha/apply/projects/service_utils.py | 6 + hypha/apply/projects/tables.py | 56 ++++++++- .../includes/batch_invoice_status_update.html | 13 ++ .../application_projects/invoice_list.html | 5 +- hypha/apply/projects/utils.py | 5 + hypha/apply/projects/views/payment.py | 74 +++++++++-- hypha/static_src/javascript/batch-actions.js | 43 +++++++ .../sass/components/_projects-table.scss | 22 ++++ 15 files changed, 435 insertions(+), 77 deletions(-) create mode 100644 hypha/apply/activity/migrations/0080_alter_event_type.py create mode 100644 hypha/apply/projects/templates/application_projects/includes/batch_invoice_status_update.html diff --git a/hypha/apply/activity/adapters/activity_feed.py b/hypha/apply/activity/adapters/activity_feed.py index a8335198bb..6131f0ffe3 100644 --- a/hypha/apply/activity/adapters/activity_feed.py +++ b/hypha/apply/activity/adapters/activity_feed.py @@ -8,6 +8,7 @@ from hypha.apply.activity.options import MESSAGES from hypha.apply.projects.utils import ( get_invoice_public_status, + get_invoice_status_display_value, get_project_public_status, get_project_status_display_value, ) @@ -66,6 +67,7 @@ class ActivityAdapter(AdapterBase): MESSAGES.DISABLED_REPORTING: _("Reporting disabled"), MESSAGES.BATCH_DELETE_SUBMISSION: "handle_batch_delete_submission", MESSAGES.BATCH_ARCHIVE_SUBMISSION: "handle_batch_archive_submission", + MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "handle_batch_update_invoice_status", MESSAGES.ARCHIVE_SUBMISSION: _( "{user} has archived the submission: {source.title}" ), @@ -164,6 +166,21 @@ def handle_batch_archive_submission(self, sources, **kwargs): title=submissions_text ) + def handle_batch_update_invoice_status(self, sources, invoices, **kwargs): + invoice_numbers = ", ".join( + [ + invoice.invoice_number if invoice.invoice_number else "" + for invoice in invoices + ] + ) + invoice_status = invoices[0].status if invoices else "" + return _( + "Successfully updated status to {invoice_status} for invoices: {invoice_numbers}" + ).format( + invoice_status=get_invoice_status_display_value(invoice_status), + invoice_numbers=invoice_numbers, + ) + def handle_paf_assignment(self, source, paf_approvals, **kwargs): if hasattr(paf_approvals, "__iter__"): # paf_approvals has to be iterable users = ", ".join( diff --git a/hypha/apply/activity/adapters/base.py b/hypha/apply/activity/adapters/base.py index 535a3a2eab..6889e34272 100644 --- a/hypha/apply/activity/adapters/base.py +++ b/hypha/apply/activity/adapters/base.py @@ -36,6 +36,7 @@ MESSAGES.CREATE_REMINDER: "reminder", MESSAGES.DELETE_REMINDER: "reminder", MESSAGES.REVIEW_REMINDER: "reminder", + MESSAGES.BATCH_UPDATE_INVOICE_STATUS: "invoices", } diff --git a/hypha/apply/activity/migrations/0080_alter_event_type.py b/hypha/apply/activity/migrations/0080_alter_event_type.py new file mode 100644 index 0000000000..452120d67e --- /dev/null +++ b/hypha/apply/activity/migrations/0080_alter_event_type.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.9 on 2024-02-08 04:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("activity", "0079_alter_activity_visibility"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="type", + field=models.CharField( + choices=[ + ("UPDATE_LEAD", "updated lead"), + ("BATCH_UPDATE_LEAD", "batch updated lead"), + ("EDIT_SUBMISSION", "edited submission"), + ("APPLICANT_EDIT", "edited applicant"), + ("NEW_SUBMISSION", "submitted new submission"), + ("DRAFT_SUBMISSION", "submitted new draft submission"), + ("SCREENING", "screened"), + ("TRANSITION", "transitioned"), + ("BATCH_TRANSITION", "batch transitioned"), + ("DETERMINATION_OUTCOME", "sent determination outcome"), + ("BATCH_DETERMINATION_OUTCOME", "sent batch determination outcome"), + ("INVITED_TO_PROPOSAL", "invited to proposal"), + ("REVIEWERS_UPDATED", "updated reviewers"), + ("BATCH_REVIEWERS_UPDATED", "batch updated reviewers"), + ("PARTNERS_UPDATED", "updated partners"), + ("PARTNERS_UPDATED_PARTNER", "partners updated partner"), + ("READY_FOR_REVIEW", "marked ready for review"), + ("BATCH_READY_FOR_REVIEW", "marked batch ready for review"), + ("NEW_REVIEW", "added new review"), + ("COMMENT", "added comment"), + ("PROPOSAL_SUBMITTED", "submitted proposal"), + ("OPENED_SEALED", "opened sealed submission"), + ("REVIEW_OPINION", "reviewed opinion"), + ("DELETE_SUBMISSION", "deleted submission"), + ("DELETE_REVIEW", "deleted review"), + ("DELETE_REVIEW_OPINION", "deleted review opinion"), + ("CREATED_PROJECT", "created project"), + ("UPDATED_VENDOR", "updated contracting information"), + ("UPDATE_PROJECT_LEAD", "updated project lead"), + ("EDIT_REVIEW", "edited review"), + ("SEND_FOR_APPROVAL", "sent for approval"), + ("APPROVE_PROJECT", "approved project"), + ("ASSIGN_PAF_APPROVER", "assign paf approver"), + ("APPROVE_PAF", "approved paf"), + ("PROJECT_TRANSITION", "transitioned project"), + ("REQUEST_PROJECT_CHANGE", "requested project change"), + ("SUBMIT_CONTRACT_DOCUMENTS", "submitted contract documents"), + ("UPLOAD_DOCUMENT", "uploaded document to project"), + ("REMOVE_DOCUMENT", "removed document from project"), + ("UPLOAD_CONTRACT", "uploaded contract to project"), + ("APPROVE_CONTRACT", "approved contract"), + ("CREATE_INVOICE", "created invoice for project"), + ("UPDATE_INVOICE_STATUS", "updated invoice status"), + ("APPROVE_INVOICE", "approve invoice"), + ("DELETE_INVOICE", "deleted invoice"), + ("SENT_TO_COMPLIANCE", "sent project to compliance"), + ("UPDATE_INVOICE", "updated invoice"), + ("SUBMIT_REPORT", "submitted report"), + ("SKIPPED_REPORT", "skipped report"), + ("REPORT_FREQUENCY_CHANGED", "changed report frequency"), + ("DISABLED_REPORTING", "disabled reporting"), + ("REPORT_NOTIFY", "notified report"), + ("CREATE_REMINDER", "created reminder"), + ("DELETE_REMINDER", "deleted reminder"), + ("REVIEW_REMINDER", "reminder to review"), + ("BATCH_DELETE_SUBMISSION", "batch deleted submissions"), + ("BATCH_ARCHIVE_SUBMISSION", "batch archive submissions"), + ("BATCH_INVOICE_STATUS_UPDATE", "batch update invoice status"), + ("STAFF_ACCOUNT_CREATED", "created new account"), + ("STAFF_ACCOUNT_EDITED", "edited account"), + ("ARCHIVE_SUBMISSION", "archived submission"), + ("UNARCHIVE_SUBMISSION", "unarchived submission"), + ], + max_length=50, + verbose_name="verb", + ), + ), + ] diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index 5aa17b9d57..2675ec57c2 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -73,6 +73,10 @@ class MESSAGES(TextChoices): "BATCH_ARCHIVE_SUBMISSION", _("batch archive submissions"), ) + BATCH_UPDATE_INVOICE_STATUS = ( + "BATCH_INVOICE_STATUS_UPDATE", + _("batch update invoice status"), + ) STAFF_ACCOUNT_CREATED = "STAFF_ACCOUNT_CREATED", _("created new account") STAFF_ACCOUNT_EDITED = "STAFF_ACCOUNT_EDITED", _("edited account") ARCHIVE_SUBMISSION = "ARCHIVE_SUBMISSION", _("archived submission") diff --git a/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html b/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html index f837c4bfe0..6633c87285 100644 --- a/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html +++ b/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html @@ -27,35 +27,42 @@

0 {% trans "Selected" %}

- + {% if not project_actions %} + - + - + - + - {% if can_bulk_archive %} - + {% endif %} + {% elif invoice_actions %} + {% endif %}
@@ -124,4 +131,9 @@

{% include "funds/includes/batch_progress_form.html" %} {% include "funds/includes/batch_delete_submission_form.html" %} {% include "funds/includes/batch_archive_submission_form.html" %} + {% if project_actions %} + {% if invoice_actions %} + {% include "application_projects/includes/batch_invoice_status_update.html" %} + {% endif %} + {% endif %} {% endif %} diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index 2514a9b340..3405b79303 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -1,4 +1,5 @@ from .payment import ( + BatchUpdateInvoiceStatusForm, ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm, @@ -39,6 +40,7 @@ "ApproveContractForm", "ApproversForm", "AssignApproversForm", + "BatchUpdateInvoiceStatusForm", "ChangePAFStatusForm", "ChangeProjectStatusForm", "CreateProjectForm", diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index a68c9978fe..4896527c6c 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -28,6 +28,7 @@ invoice_status_user_choices, ) from ..models.project import PacketFile +from ..utils import get_invoice_status_display_value def filter_request_choices(choices, user_choices): @@ -36,6 +37,50 @@ def filter_request_choices(choices, user_choices): ] +def get_invoice_possible_transition_for_user(user, invoice): + 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, + APPROVED_BY_FINANCE, + ], + user_choices, + ), + CHANGES_REQUESTED_BY_FINANCE: filter_request_choices( + [CHANGES_REQUESTED_BY_STAFF, DECLINED], user_choices + ), + APPROVED_BY_FINANCE: filter_request_choices([PAID], user_choices), + PAID: filter_request_choices([PAYMENT_FAILED], user_choices), + PAYMENT_FAILED: filter_request_choices([PAID], user_choices), + } + if settings.INVOICE_EXTENDED_WORKFLOW: + possible_status_transitions_lut.update( + { + CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( + [ + CHANGES_REQUESTED_BY_FINANCE, + APPROVED_BY_FINANCE, + ], + user_choices, + ), + APPROVED_BY_FINANCE: filter_request_choices( + [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], + user_choices, + ), + APPROVED_BY_FINANCE_2: filter_request_choices([PAID], user_choices), + } + ) + return possible_status_transitions_lut.get(invoice.status, []) + + class ChangeInvoiceStatusForm(forms.ModelForm): name_prefix = "change_invoice_status_form" @@ -47,49 +92,10 @@ def __init__(self, instance, user, *args, **kwargs): super().__init__(*args, **kwargs, instance=instance) 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, - APPROVED_BY_FINANCE, - ], - user_choices, - ), - CHANGES_REQUESTED_BY_FINANCE: filter_request_choices( - [CHANGES_REQUESTED_BY_STAFF, DECLINED], user_choices - ), - APPROVED_BY_FINANCE: filter_request_choices([PAID], user_choices), - PAID: filter_request_choices([PAYMENT_FAILED], user_choices), - PAYMENT_FAILED: filter_request_choices([PAID], user_choices), - } - if settings.INVOICE_EXTENDED_WORKFLOW: - possible_status_transitions_lut.update( - { - CHANGES_REQUESTED_BY_FINANCE_2: filter_request_choices( - [ - CHANGES_REQUESTED_BY_FINANCE, - APPROVED_BY_FINANCE, - ], - user_choices, - ), - APPROVED_BY_FINANCE: filter_request_choices( - [CHANGES_REQUESTED_BY_FINANCE_2, APPROVED_BY_FINANCE_2], - user_choices, - ), - APPROVED_BY_FINANCE_2: filter_request_choices([PAID], user_choices), - } - ) - status_field.choices = possible_status_transitions_lut.get(instance.status, []) + + status_field.choices = get_invoice_possible_transition_for_user( + user, invoice=instance + ) class InvoiceBaseForm(forms.ModelForm): @@ -199,3 +205,29 @@ def clean_document(self): @transaction.atomic() def save(self, *args, **kwargs): return super().save(*args, **kwargs) + + +class BatchUpdateInvoiceStatusForm(forms.Form): + invoice_action = forms.ChoiceField(label=_("Status")) + invoices = forms.CharField( + widget=forms.HiddenInput(attrs={"class": "js-invoices-id"}) + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + super().__init__(*args, **kwargs) + if self.user.is_apply_staff: + self.fields["invoice_action"].choices = [ + (DECLINED, get_invoice_status_display_value(DECLINED)) + ] + elif self.user.is_finance: + self.fields["invoice_action"].choices = [ + (DECLINED, get_invoice_status_display_value(DECLINED)), + (PAID, get_invoice_status_display_value(PAID)), + (PAYMENT_FAILED, get_invoice_status_display_value(PAYMENT_FAILED)), + ] + + def clean_invoices(self): + value = self.cleaned_data["invoices"] + invoice_ids = [int(invoice) for invoice in value.split(",")] + return Invoice.objects.filter(id__in=invoice_ids) diff --git a/hypha/apply/projects/service_utils.py b/hypha/apply/projects/service_utils.py index b1ae26e201..db23ab5436 100644 --- a/hypha/apply/projects/service_utils.py +++ b/hypha/apply/projects/service_utils.py @@ -152,3 +152,9 @@ def handle_tasks_on_invoice_update(old_status, invoice): ), related_obj=invoice, ) + + +def batch_update_invoices_status(invoices, user, status): + for invoice in invoices: + invoice.status = status + invoice.save(update_fields=["status"]) diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py index 2838b6792a..4d351299ae 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -1,20 +1,46 @@ +import json import textwrap import django_tables2 as tables from django.utils.safestring import mark_safe +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ +from django_tables2.utils import A +from hypha.apply.funds.tables import LabeledCheckboxColumn + +from .forms.payment import get_invoice_possible_transition_for_user from .models import Invoice, PAFApprovals, Project, Report +def render_invoice_actions(table, record): + user = table.context["user"] + actions = get_invoice_possible_transition_for_user(user, invoice=record) + return json.dumps([str(slugify(action)) for action, _ in actions]) + + class BaseInvoiceTable(tables.Table): invoice_number = tables.LinkColumn( "funds:projects:invoice-detail", verbose_name=_("Invoice Number"), args=[tables.utils.A("project__pk"), tables.utils.A("pk")], + attrs={ + "td": { + "class": "js-title", # using title as class because of batch-actions.js + }, + "a": { + "data-tippy-content": lambda record: record.invoice_number, + "data-tippy-placement": "top", + # Use after:content-[''] after:block to hide the default browser tooltip on Safari + # https://stackoverflow.com/a/43915246 + "class": "truncate inline-block w-[calc(100%-2rem)] after:content-[''] after:block", + }, + }, ) project = tables.Column(verbose_name=_("Project Name")) - status = tables.Column() + status = tables.Column( + attrs={"td": {"data-actions": render_invoice_actions, "class": "js-actions"}}, + ) requested_at = tables.DateColumn(verbose_name=_("Submitted")) def render_project(self, value): @@ -56,6 +82,34 @@ class Meta: attrs = {"class": "invoices-table"} +class AdminInvoiceListTable(BaseInvoiceTable): + selected = LabeledCheckboxColumn( + accessor=A("pk"), + attrs={ + "input": {"class": "js-batch-select"}, + "th__input": {"class": "js-batch-select-all"}, + }, + ) + + class Meta: + fields = [ + "selected", + "requested_at", + "invoice_number", + "status", + "project", + ] + model = Invoice + orderable = True + sequence = fields + order_by = ["-requested_at"] + template_name = "application_projects/tables/table.html" + attrs = {"class": "invoices-table"} + row_attrs = { + "data-record-id": lambda record: record.id, + } + + class BaseProjectsTable(tables.Table): title = tables.LinkColumn( "funds:projects:detail", diff --git a/hypha/apply/projects/templates/application_projects/includes/batch_invoice_status_update.html b/hypha/apply/projects/templates/application_projects/includes/batch_invoice_status_update.html new file mode 100644 index 0000000000..75120ef70d --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/includes/batch_invoice_status_update.html @@ -0,0 +1,13 @@ +{% load i18n %} + diff --git a/hypha/apply/projects/templates/application_projects/invoice_list.html b/hypha/apply/projects/templates/application_projects/invoice_list.html index bb724fe23a..0197df611a 100644 --- a/hypha/apply/projects/templates/application_projects/invoice_list.html +++ b/hypha/apply/projects/templates/application_projects/invoice_list.html @@ -22,7 +22,7 @@ {% if table %} {% trans "invoices" as search_placeholder %} - {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action use_batch_actions=True search_placeholder=search_placeholder %} + {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action use_batch_actions=True project_actions=True invoice_actions=True search_placeholder=search_placeholder %} {% render_table table %} {% else %}

{% trans "No Invoices available" %}

@@ -38,5 +38,8 @@ {% block extra_js %} {{ filter.form.media.js }} + + + {% endblock %} diff --git a/hypha/apply/projects/utils.py b/hypha/apply/projects/utils.py index 8a88c021c9..5a07ecb82e 100644 --- a/hypha/apply/projects/utils.py +++ b/hypha/apply/projects/utils.py @@ -20,6 +20,7 @@ CHANGES_REQUESTED_BY_FINANCE_2, CHANGES_REQUESTED_BY_STAFF, DECLINED, + INVOICE_STATUS_CHOICES, PAID, PAYMENT_FAILED, RESUBMITTED, @@ -159,6 +160,10 @@ def get_project_public_status(project_status): return dict(PROJECT_PUBLIC_STATUSES)[project_status] +def get_invoice_status_display_value(invoice_status): + return dict(INVOICE_STATUS_CHOICES)[invoice_status] + + def get_invoice_table_status(invoice_status, is_applicant=False): if invoice_status in [SUBMITTED, RESUBMITTED]: if is_applicant: diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 350545c23b..ca348b84a5 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -8,8 +8,15 @@ from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ -from django.views.generic import CreateView, DeleteView, DetailView, UpdateView +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + FormView, + UpdateView, +) from django_filters.views import FilterView from django_tables2 import SingleTableMixin @@ -29,10 +36,20 @@ from hypha.apply.users.decorators import staff_or_finance_required from hypha.apply.users.groups import STAFF_GROUP_NAME from hypha.apply.utils.storage import PrivateMediaView -from hypha.apply.utils.views import DelegateableView, DelegatedViewMixin, ViewDispatcher +from hypha.apply.utils.views import ( + DelegateableListView, + DelegateableView, + DelegatedViewMixin, + ViewDispatcher, +) from ..filters import InvoiceListFilter -from ..forms import ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm +from ..forms import ( + BatchUpdateInvoiceStatusForm, + ChangeInvoiceStatusForm, + CreateInvoiceForm, + EditInvoiceForm, +) from ..models.payment import ( APPROVED_BY_FINANCE, APPROVED_BY_STAFF, @@ -42,8 +59,8 @@ Invoice, ) from ..models.project import PROJECT_ACTION_MESSAGE_TAG, Project -from ..service_utils import handle_tasks_on_invoice_update -from ..tables import InvoiceListTable +from ..service_utils import batch_update_invoices_status, handle_tasks_on_invoice_update +from ..tables import AdminInvoiceListTable @method_decorator(login_required, name="dispatch") @@ -420,8 +437,51 @@ def test_func(self): @method_decorator(staff_or_finance_required, name="dispatch") -class InvoiceListView(SingleTableMixin, FilterView): +class BatchUpdateInvoiceStatusView(DelegatedViewMixin, FormView): + form_class = BatchUpdateInvoiceStatusForm + context_name = "batch_invoice_status_form" + + def form_valid(self, form): + new_status = form.cleaned_data["invoice_action"] + invoices = form.cleaned_data["invoices"] + invoices_old_statuses = {invoice: invoice.status for invoice in invoices} + batch_update_invoices_status( + invoices=invoices, + user=self.request.user, + status=new_status, + ) + + # add activity feed for batch update invoice status + projects = Project.objects.filter( + id__in=[invoice.project.id for invoice in invoices] + ) + messenger( + MESSAGES.BATCH_UPDATE_INVOICE_STATUS, + request=self.request, + user=self.request.user, + sources=projects, + related=invoices, + ) + + # update tasks for selected invoices + for invoice, old_status in invoices_old_statuses.items(): + handle_tasks_on_invoice_update(old_status, invoice) + return super().form_valid(form) + + def form_invalid(self, form): + messages.error( + self.request, + mark_safe(_("Sorry something went wrong") + form.errors.as_ul()), + ) + return super().form_invalid(form) + + +@method_decorator(staff_or_finance_required, name="dispatch") +class InvoiceListView(SingleTableMixin, FilterView, DelegateableListView): + form_views = [ + BatchUpdateInvoiceStatusView, + ] filterset_class = InvoiceListFilter model = Invoice - table_class = InvoiceListTable + table_class = AdminInvoiceListTable template_name = "application_projects/invoice_list.html" diff --git a/hypha/static_src/javascript/batch-actions.js b/hypha/static_src/javascript/batch-actions.js index 532c21ef0b..e94bd4d672 100644 --- a/hypha/static_src/javascript/batch-actions.js +++ b/hypha/static_src/javascript/batch-actions.js @@ -6,10 +6,13 @@ const $allCheckboxInput = $(".js-batch-select-all"); const $batchButtons = $(".js-batch-button"); const $batchProgress = $(".js-batch-progress"); + const $batchInvoiceProgress = $(".js-batch-invoice-progress"); const $actionOptions = $("#id_action option"); + const $actionInvoiceOptions = $("#id_invoice_action option"); const $batchTitlesList = $(".js-batch-titles"); const $batchTitleCount = $(".js-batch-title-count"); const $hiddenIDlist = $(".js-submissions-id"); + const $hiddenInvoiceIDlist = $(".js-invoices-id"); const $batchDetermineSend = $(".js-batch-determine-send"); const $batchDetermineConfirm = $(".js-batch-determine-confirm"); const $batchDetermineForm = $batchDetermineSend.parent("form"); @@ -48,6 +51,7 @@ toggleBatchActions(); updateCount(); updateProgressButton(); + updateInvoiceProgressButton(); }); $checkbox.change(function () { @@ -63,6 +67,7 @@ } updateProgressButton(); + updateInvoiceProgressButton(); }); // append selected project titles to batch update reviewer modal @@ -75,6 +80,9 @@ $batchProgress.click(function () { updateProgressButton(); }); + $batchInvoiceProgress.click(function () { + updateInvoiceProgressButton(); + }); // show/hide the list of actions $toggleBatchList.click((e) => { @@ -116,6 +124,41 @@ $batchTitleCount.append(`${selectedIDs.length} submissions selected`); $hiddenIDlist.val(selectedIDs.join(",")); + $hiddenInvoiceIDlist.val(selectedIDs.join(",")); + } + + function updateInvoiceProgressButton() { + var actions = $actionInvoiceOptions + .map(function () { + return this.value; + }) + .get(); + $checkbox.filter(":checked").each(function () { + let newActions = $(this) + .parents("tr") + .find(".js-actions") + .data("actions"); + actions = actions.filter((action) => newActions.includes(action)); + }); + + $actionInvoiceOptions.each(function () { + if (!actions.includes(this.value)) { + $(this).attr("disabled", "disabled"); + } else { + $(this).removeAttr("disabled"); + } + }); + $actionInvoiceOptions.filter(":enabled:first").prop("selected", true); + if (actions.length === 0) { + $batchInvoiceProgress.attr("disabled", "disabled"); + $batchInvoiceProgress.attr( + "data-tooltip", + "Status changes can't be applied to Invoices with this combination of statuses" + ); + } else { + $batchInvoiceProgress.removeAttr("disabled"); + $batchInvoiceProgress.removeAttr("data-tooltip"); + } } function updateProgressButton() { diff --git a/hypha/static_src/sass/components/_projects-table.scss b/hypha/static_src/sass/components/_projects-table.scss index 525ba44efe..70ebf6a303 100644 --- a/hypha/static_src/sass/components/_projects-table.scss +++ b/hypha/static_src/sass/components/_projects-table.scss @@ -44,6 +44,17 @@ @include media-query($table-breakpoint) { display: table-header-group; } + + th { + &.selected { + @include table-checkbox; + + @include media-query($table-breakpoint) { + width: 50px; + padding-right: 0; + } + } + } } tbody { @@ -57,6 +68,17 @@ display: none; } } + + // batch action checkboxes + &.selected { + @include table-checkbox; + display: none; + padding-right: 0; + + @include media-query($table-breakpoint) { + display: table-cell; + } + } } } }