From 3c7250c1d614396ff4e8f01fb94b91bf15154274 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Tue, 23 Jun 2026 08:14:44 +0200 Subject: [PATCH 1/8] Implement feature to tag invoices with terms. --- hypha/apply/projects/admin.py | 9 ++++ hypha/apply/projects/forms/__init__.py | 2 + hypha/apply/projects/forms/payment.py | 14 ++++++ .../projects/migrations/0104_invoice_terms.py | 42 ++++++++++++++++ hypha/apply/projects/models/__init__.py | 3 +- hypha/apply/projects/models/payment.py | 19 ++++++++ hypha/apply/projects/tables.py | 15 ++++++ .../modals/invoice_tag.html | 33 +++++++++++++ .../partials/invoice_detail_actions.html | 12 +++++ hypha/apply/projects/urls.py | 6 +++ hypha/apply/projects/views/__init__.py | 2 + hypha/apply/projects/views/payment.py | 48 ++++++++++++++++++- 12 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 hypha/apply/projects/migrations/0104_invoice_terms.py create mode 100644 hypha/apply/projects/templates/application_projects/modals/invoice_tag.html diff --git a/hypha/apply/projects/admin.py b/hypha/apply/projects/admin.py index 68b7440e44..70171476be 100644 --- a/hypha/apply/projects/admin.py +++ b/hypha/apply/projects/admin.py @@ -15,6 +15,7 @@ from .models import ( ContractDocumentCategory, DocumentCategory, + InvoiceTerm, ProjectForm, ProjectReportForm, ProjectSettings, @@ -95,12 +96,20 @@ class ProjectSettingsAdmin(SettingModelAdmin): model = ProjectSettings +class InvoiceTermAdmin(ModelAdmin): + model = InvoiceTerm + menu_label = _("Invoice terms") + menu_icon = "tag" + list_display = ("name",) + + class ProjectAdminGroup(ModelAdminGroup): menu_label = _("Projects") menu_icon = str(AdminIcon.PROJECT) items = ( ContractDocumentCategoryAdmin, DocumentCategoryAdmin, + InvoiceTermAdmin, ProjectFormAdmin, ProjectReportFormAdmin, ProjectSOWFormAdmin, diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index 85642d0089..4537815dd3 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -3,6 +3,7 @@ ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm, + InvoiceTermsForm, SelectDocumentForm, ) from .project import ( @@ -50,4 +51,5 @@ "CreateInvoiceForm", "ChangeInvoiceStatusForm", "EditInvoiceForm", + "InvoiceTermsForm", ] diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index 1c4d67c613..d95461fd42 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -22,6 +22,7 @@ RESUBMITTED, SUBMITTED, Invoice, + InvoiceTerm, SupportingDocument, invoice_status_user_choices, ) @@ -226,3 +227,16 @@ 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) + + +class InvoiceTermsForm(forms.ModelForm): + terms = forms.ModelMultipleChoiceField( + queryset=InvoiceTerm.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + label=_("Terms"), + ) + + class Meta: + model = Invoice + fields = ["terms"] diff --git a/hypha/apply/projects/migrations/0104_invoice_terms.py b/hypha/apply/projects/migrations/0104_invoice_terms.py new file mode 100644 index 0000000000..afd709f6ca --- /dev/null +++ b/hypha/apply/projects/migrations/0104_invoice_terms.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.15 on 2026-06-23 05:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("application_projects", "0103_alter_contract_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="InvoiceTerm", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ], + options={ + "verbose_name": "invoice term", + "verbose_name_plural": "invoice terms", + "ordering": ["name"], + }, + ), + migrations.AddField( + model_name="invoice", + name="terms", + field=models.ManyToManyField( + blank=True, + related_name="invoices", + to="application_projects.invoiceterm", + verbose_name="terms", + ), + ), + ] diff --git a/hypha/apply/projects/models/__init__.py b/hypha/apply/projects/models/__init__.py index 181b7c93c9..126e013273 100644 --- a/hypha/apply/projects/models/__init__.py +++ b/hypha/apply/projects/models/__init__.py @@ -1,4 +1,4 @@ -from .payment import Invoice, SupportingDocument +from .payment import Invoice, InvoiceTerm, SupportingDocument from .project import ( Contract, ContractDocumentCategory, @@ -28,5 +28,6 @@ "DocumentCategory", "ContractDocumentCategory", "Invoice", + "InvoiceTerm", "SupportingDocument", ] diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index 87f8106905..ee4c61a830 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -12,6 +12,19 @@ from hypha.apply.utils.storage import PrivateStorage + +class InvoiceTerm(models.Model): + name = models.CharField(max_length=100, unique=True) + + class Meta: + verbose_name = _("invoice term") + verbose_name_plural = _("invoice terms") + ordering = ["name"] + + def __str__(self): + return self.name + + SUBMITTED = "submitted" RESUBMITTED = "resubmitted" CHANGES_REQUESTED_BY_STAFF = "changes_requested_staff" @@ -138,6 +151,12 @@ class Invoice(models.Model): ) status_field = State(default=SUBMITTED, states=INVOICE_STATUS_CHOICES) requested_at = models.DateTimeField(auto_now_add=True) + terms = models.ManyToManyField( + InvoiceTerm, + blank=True, + related_name="invoices", + verbose_name=_("terms"), + ) objects = InvoiceQueryset.as_manager() wagtail_reference_index_ignore = True diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py index 17bfe21ce5..bb6573b152 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -135,6 +135,7 @@ def render_vendor_name(self, record): class AdminInvoiceListTable(BaseInvoiceTable): project = tables.Column(verbose_name=_("Project Name")) + terms = tables.Column(verbose_name=_("Terms"), orderable=False, empty_values=()) selected = LabeledCheckboxColumn( accessor=A("pk"), attrs={ @@ -154,6 +155,7 @@ class Meta(BaseInvoiceTable.Meta): "status", "requested_at", "project", + "terms", ] model = Invoice orderable = True @@ -168,6 +170,19 @@ class Meta(BaseInvoiceTable.Meta): def render_project(self, record): return get_project_title(record.project) + def render_terms(self, record): + terms = record.terms.all() + if not terms: + return mark_safe("") + badges = "".join( + format_html( + "{}", + term.name, + ) + for term in terms + ) + return mark_safe(f"
{badges}
") + class BaseProjectsTable(tables.Table): title = tables.LinkColumn( diff --git a/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html b/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html new file mode 100644 index 0000000000..20acd8953e --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html @@ -0,0 +1,33 @@ +{% load i18n %} +{% trans "Manage Invoice Terms" %} + +
+
+ {% csrf_token %} + {% if form.terms.field.queryset.exists %} +
+ {% trans "Select terms to apply to this invoice" %} + {% for checkbox in form.terms %} + + {% endfor %} +
+ {% else %} +

{% trans "No invoice terms have been configured yet. Add terms in the Wagtail admin under Projects > Invoice terms." %}

+ {% endif %} + +
+ + +
+
+
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 index 37eb2a1027..14c6db48b7 100644 --- a/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html @@ -47,6 +47,18 @@ {% endif %} + + {% if user.is_apply_staff or user.is_finance %} + + {% endif %} + {% can_delete object user as user_can_delete_request %} {% if user_can_delete_request %} diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index c5cdc419cd..f7d6eca2b4 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -31,6 +31,7 @@ SendForApprovalView, SkipPAFApprovalProcessView, SubmitContractDocumentsView, + TagInvoiceView, UpdateAssignApproversView, UpdateLeadView, UpdatePAFApproversView, @@ -252,6 +253,11 @@ partial_get_invoice_detail_actions, name="partial-invoice-detail-actions", ), + path( + "terms/", + TagInvoiceView.as_view(), + name="invoice-terms", + ), path( "documents/invoice/", InvoicePrivateMedia.as_view(), diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py index 84396463fe..d22ac1758b 100644 --- a/hypha/apply/projects/views/__init__.py +++ b/hypha/apply/projects/views/__init__.py @@ -7,6 +7,7 @@ InvoiceListView, InvoicePrivateMedia, InvoiceView, + TagInvoiceView, ) from .project import ( AdminProjectDetailView, @@ -98,4 +99,5 @@ "EditInvoiceView", "DeleteInvoiceView", "InvoicePrivateMedia", + "TagInvoiceView", ] diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 8e4c521a3d..fbb5957c68 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -61,6 +61,7 @@ ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm, + InvoiceTermsForm, ) from ..models.payment import ( APPROVED_BY_FINANCE, @@ -588,9 +589,54 @@ class InvoiceListView(SingleTableMixin, FilterView, DelegateableListView): template_name = "application_projects/invoice_list.html" def get_queryset(self): - return super().get_queryset().select_related("project", "project__user") + return ( + super() + .get_queryset() + .select_related("project", "project__user") + .prefetch_related("terms") + ) def get_table_class(self): if self.request.user.is_finance: return FinanceInvoiceTable return super().get_table_class() + + +@method_decorator(staff_or_finance_required, name="dispatch") +class TagInvoiceView(InvoiceAccessMixin, View): + form_class = InvoiceTermsForm + template_name = "application_projects/modals/invoice_tag.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 = self.form_class(instance=self.object) + return render( + self.request, + self.template_name, + {"form": form, "object": self.object}, + ) + + def post(self, *args, **kwargs): + form = self.form_class(self.request.POST, instance=self.object) + if form.is_valid(): + form.save() + return HttpResponse( + status=204, + headers={ + "HX-Trigger": json.dumps( + { + "invoicesUpdated": None, + "showMessage": _("Invoice terms updated."), + } + ) + }, + ) + return render( + self.request, + self.template_name, + {"form": form, "object": self.object}, + status=400, + ) From 8a339b4ff9bafe457950676c3977a6e0805f5ac3 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Tue, 23 Jun 2026 11:06:19 +0200 Subject: [PATCH 2/8] Add tags to add invoice form. --- hypha/apply/projects/forms/payment.py | 7 +++++++ .../partials/invoice_detail_actions.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index d95461fd42..cad9046582 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -96,12 +96,17 @@ class Meta: "invoice_date", "document", "message_for_pm", + "terms", ] model = Invoice + widgets = { + "terms": forms.CheckboxSelectMultiple, + } def __init__(self, user=None, *args, **kwargs): super().__init__(*args, **kwargs) self.initial["message_for_pm"] = "" + self.fields["terms"].required = False class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): @@ -124,6 +129,7 @@ class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", + "terms", ] def save(self, commit=True): @@ -150,6 +156,7 @@ class EditInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", + "terms", ] @transaction.atomic 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 index 14c6db48b7..8744276a29 100644 --- a/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html @@ -55,7 +55,7 @@ hx-target="#htmx-modal" > {% heroicon_micro "tag" aria_hidden=true class="opacity-80 size-4" %} - {% trans "Manage terms" %} + {% trans "Tags" %} {% endif %} From 552dd845f06a7f8debb8444d7f22045556cb6999 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Tue, 23 Jun 2026 11:10:42 +0200 Subject: [PATCH 3/8] Rename terms to tags. --- hypha/apply/projects/admin.py | 10 ++++----- hypha/apply/projects/forms/__init__.py | 4 ++-- hypha/apply/projects/forms/payment.py | 22 +++++++++---------- .../projects/migrations/0104_invoice_terms.py | 12 +++++----- hypha/apply/projects/models/__init__.py | 4 ++-- hypha/apply/projects/models/payment.py | 12 +++++----- hypha/apply/projects/tables.py | 14 ++++++------ .../modals/invoice_tag.html | 12 +++++----- .../partials/invoice_detail_actions.html | 4 ++-- hypha/apply/projects/urls.py | 2 +- hypha/apply/projects/views/payment.py | 6 ++--- 11 files changed, 51 insertions(+), 51 deletions(-) diff --git a/hypha/apply/projects/admin.py b/hypha/apply/projects/admin.py index 70171476be..ed07ab63f6 100644 --- a/hypha/apply/projects/admin.py +++ b/hypha/apply/projects/admin.py @@ -15,7 +15,7 @@ from .models import ( ContractDocumentCategory, DocumentCategory, - InvoiceTerm, + InvoiceTag, ProjectForm, ProjectReportForm, ProjectSettings, @@ -96,9 +96,9 @@ class ProjectSettingsAdmin(SettingModelAdmin): model = ProjectSettings -class InvoiceTermAdmin(ModelAdmin): - model = InvoiceTerm - menu_label = _("Invoice terms") +class InvoiceTagAdmin(ModelAdmin): + model = InvoiceTag + menu_label = _("Invoice Tags") menu_icon = "tag" list_display = ("name",) @@ -109,7 +109,7 @@ class ProjectAdminGroup(ModelAdminGroup): items = ( ContractDocumentCategoryAdmin, DocumentCategoryAdmin, - InvoiceTermAdmin, + InvoiceTagAdmin, ProjectFormAdmin, ProjectReportFormAdmin, ProjectSOWFormAdmin, diff --git a/hypha/apply/projects/forms/__init__.py b/hypha/apply/projects/forms/__init__.py index 4537815dd3..c83844cdcc 100644 --- a/hypha/apply/projects/forms/__init__.py +++ b/hypha/apply/projects/forms/__init__.py @@ -3,7 +3,7 @@ ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm, - InvoiceTermsForm, + InvoiceTagsForm, SelectDocumentForm, ) from .project import ( @@ -51,5 +51,5 @@ "CreateInvoiceForm", "ChangeInvoiceStatusForm", "EditInvoiceForm", - "InvoiceTermsForm", + "InvoiceTagsForm", ] diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index cad9046582..3b1aae9c41 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -22,7 +22,7 @@ RESUBMITTED, SUBMITTED, Invoice, - InvoiceTerm, + InvoiceTag, SupportingDocument, invoice_status_user_choices, ) @@ -96,17 +96,17 @@ class Meta: "invoice_date", "document", "message_for_pm", - "terms", + "tags", ] model = Invoice widgets = { - "terms": forms.CheckboxSelectMultiple, + "tags": forms.CheckboxSelectMultiple, } def __init__(self, user=None, *args, **kwargs): super().__init__(*args, **kwargs) self.initial["message_for_pm"] = "" - self.fields["terms"].required = False + self.fields["tags"].required = False class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): @@ -129,7 +129,7 @@ class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", - "terms", + "tags", ] def save(self, commit=True): @@ -156,7 +156,7 @@ class EditInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", - "terms", + "tags", ] @transaction.atomic @@ -236,14 +236,14 @@ def clean_invoices(self): return Invoice.objects.filter(id__in=invoice_ids) -class InvoiceTermsForm(forms.ModelForm): - terms = forms.ModelMultipleChoiceField( - queryset=InvoiceTerm.objects.all(), +class InvoiceTagsForm(forms.ModelForm): + tags = forms.ModelMultipleChoiceField( + queryset=InvoiceTag.objects.all(), widget=forms.CheckboxSelectMultiple, required=False, - label=_("Terms"), + label=_("Tags"), ) class Meta: model = Invoice - fields = ["terms"] + fields = ["tags"] diff --git a/hypha/apply/projects/migrations/0104_invoice_terms.py b/hypha/apply/projects/migrations/0104_invoice_terms.py index afd709f6ca..0cf54673fb 100644 --- a/hypha/apply/projects/migrations/0104_invoice_terms.py +++ b/hypha/apply/projects/migrations/0104_invoice_terms.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="InvoiceTerm", + name="InvoiceTag", fields=[ ( "id", @@ -24,19 +24,19 @@ class Migration(migrations.Migration): ("name", models.CharField(max_length=100, unique=True)), ], options={ - "verbose_name": "invoice term", - "verbose_name_plural": "invoice terms", + "verbose_name": "invoice tag", + "verbose_name_plural": "invoice tags", "ordering": ["name"], }, ), migrations.AddField( model_name="invoice", - name="terms", + name="tags", field=models.ManyToManyField( blank=True, related_name="invoices", - to="application_projects.invoiceterm", - verbose_name="terms", + to="application_projects.invoicetag", + verbose_name="tags", ), ), ] diff --git a/hypha/apply/projects/models/__init__.py b/hypha/apply/projects/models/__init__.py index 126e013273..50170e64ac 100644 --- a/hypha/apply/projects/models/__init__.py +++ b/hypha/apply/projects/models/__init__.py @@ -1,4 +1,4 @@ -from .payment import Invoice, InvoiceTerm, SupportingDocument +from .payment import Invoice, InvoiceTag, SupportingDocument from .project import ( Contract, ContractDocumentCategory, @@ -28,6 +28,6 @@ "DocumentCategory", "ContractDocumentCategory", "Invoice", - "InvoiceTerm", + "InvoiceTag", "SupportingDocument", ] diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index ee4c61a830..02eefad257 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -13,12 +13,12 @@ from hypha.apply.utils.storage import PrivateStorage -class InvoiceTerm(models.Model): +class InvoiceTag(models.Model): name = models.CharField(max_length=100, unique=True) class Meta: - verbose_name = _("invoice term") - verbose_name_plural = _("invoice terms") + verbose_name = _("invoice tag") + verbose_name_plural = _("invoice tags") ordering = ["name"] def __str__(self): @@ -151,11 +151,11 @@ class Invoice(models.Model): ) status_field = State(default=SUBMITTED, states=INVOICE_STATUS_CHOICES) requested_at = models.DateTimeField(auto_now_add=True) - terms = models.ManyToManyField( - InvoiceTerm, + tags = models.ManyToManyField( + InvoiceTag, blank=True, related_name="invoices", - verbose_name=_("terms"), + verbose_name=_("tags"), ) objects = InvoiceQueryset.as_manager() diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py index bb6573b152..9ace85a013 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -135,7 +135,7 @@ def render_vendor_name(self, record): class AdminInvoiceListTable(BaseInvoiceTable): project = tables.Column(verbose_name=_("Project Name")) - terms = tables.Column(verbose_name=_("Terms"), orderable=False, empty_values=()) + tags = tables.Column(verbose_name=_("Tags"), orderable=False, empty_values=()) selected = LabeledCheckboxColumn( accessor=A("pk"), attrs={ @@ -155,7 +155,7 @@ class Meta(BaseInvoiceTable.Meta): "status", "requested_at", "project", - "terms", + "tags", ] model = Invoice orderable = True @@ -170,16 +170,16 @@ class Meta(BaseInvoiceTable.Meta): def render_project(self, record): return get_project_title(record.project) - def render_terms(self, record): - terms = record.terms.all() - if not terms: + def render_tags(self, record): + tags = record.tags.all() + if not tags: return mark_safe("") badges = "".join( format_html( "{}", - term.name, + tag.name, ) - for term in terms + for tag in tags ) return mark_safe(f"
{badges}
") diff --git a/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html b/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html index 20acd8953e..323e9fd63e 100644 --- a/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html +++ b/hypha/apply/projects/templates/application_projects/modals/invoice_tag.html @@ -1,16 +1,16 @@ {% load i18n %} -{% trans "Manage Invoice Terms" %} +{% trans "Manage Invoice Tags" %}
{% csrf_token %} - {% if form.terms.field.queryset.exists %} + {% if form.tags.field.queryset.exists %}
- {% trans "Select terms to apply to this invoice" %} - {% for checkbox in form.terms %} + {% trans "Select tags to apply to this invoice" %} + {% for checkbox in form.tags %}
{% else %} -

{% trans "No invoice terms have been configured yet. Add terms in the Wagtail admin under Projects > Invoice terms." %}

+

{% trans "No invoice tags have been configured yet. Add tags in the Wagtail admin under Projects > Invoice Tags." %}

{% endif %}
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 index 8744276a29..579702619f 100644 --- a/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_detail_actions.html @@ -47,11 +47,11 @@ {% endif %} - + {% if user.is_apply_staff or user.is_finance %}
diff --git a/hypha/apply/projects/templates/application_projects/partials/invoice_tags.html b/hypha/apply/projects/templates/application_projects/partials/invoice_tags.html new file mode 100644 index 0000000000..1eb1064452 --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/partials/invoice_tags.html @@ -0,0 +1,6 @@ +{% load i18n %} +{% with tags=object.tags.all %} + {% if tags %} + {% trans "Tags" %}: {{ tags|join:", " }} + {% endif %} +{% endwith %} diff --git a/hypha/apply/projects/urls.py b/hypha/apply/projects/urls.py index 1d7bb185fd..2c545d8adf 100644 --- a/hypha/apply/projects/urls.py +++ b/hypha/apply/projects/urls.py @@ -42,6 +42,7 @@ partial_get_invoice_detail_actions, partial_get_invoice_status, partial_get_invoice_status_table, + partial_get_invoice_tags, partial_project_information, partial_project_lead, partial_project_title, @@ -254,10 +255,15 @@ name="partial-invoice-detail-actions", ), path( - "terms/", + "tags/", TagInvoiceView.as_view(), name="invoice-tags", ), + path( + "partial/tags/", + partial_get_invoice_tags, + name="partial-invoice-tags", + ), path( "documents/invoice/", InvoicePrivateMedia.as_view(), diff --git a/hypha/apply/projects/views/__init__.py b/hypha/apply/projects/views/__init__.py index d22ac1758b..b18bb2df5e 100644 --- a/hypha/apply/projects/views/__init__.py +++ b/hypha/apply/projects/views/__init__.py @@ -46,6 +46,7 @@ partial_get_invoice_detail_actions, partial_get_invoice_status, partial_get_invoice_status_table, + partial_get_invoice_tags, partial_project_information, partial_project_lead, partial_project_title, @@ -60,6 +61,7 @@ "partial_get_invoice_status_table", "partial_get_invoice_status", "partial_get_invoice_detail_actions", + "partial_get_invoice_tags", "partial_contracting_documents", "BatchUpdateInvoiceStatusView", "ChangeInvoiceStatusView", diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index 0885b3547c..393ee66b54 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -629,7 +629,7 @@ def post(self, *args, **kwargs): "HX-Trigger": json.dumps( { "invoicesUpdated": None, - "showMessage": _("Invoice terms updated."), + "showMessage": _("Invoice tags updated."), } ) }, diff --git a/hypha/apply/projects/views/project_partials.py b/hypha/apply/projects/views/project_partials.py index 3f9c00433f..6c63fcf366 100644 --- a/hypha/apply/projects/views/project_partials.py +++ b/hypha/apply/projects/views/project_partials.py @@ -157,3 +157,13 @@ def partial_get_invoice_detail_actions(request: HttpRequest, pk: int, invoice_pk "application_projects/partials/invoice_detail_actions.html", context={"object": invoice, "user": user}, ) + + +@login_required +def partial_get_invoice_tags(request: HttpRequest, pk: int, invoice_pk: int): + invoice = get_object_or_404(Invoice, pk=invoice_pk) + return render( + request, + "application_projects/partials/invoice_tags.html", + context={"object": invoice}, + ) From 5aa376f971b6c72c1cdea1607b9d830073c2a366 Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Tue, 23 Jun 2026 12:09:03 +0200 Subject: [PATCH 7/8] Add tags to FinanceInvoiceTable as well. --- ..._invoice_terms.py => 0104_invoice_tags.py} | 0 hypha/apply/projects/tables.py | 28 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) rename hypha/apply/projects/migrations/{0104_invoice_terms.py => 0104_invoice_tags.py} (100%) diff --git a/hypha/apply/projects/migrations/0104_invoice_terms.py b/hypha/apply/projects/migrations/0104_invoice_tags.py similarity index 100% rename from hypha/apply/projects/migrations/0104_invoice_terms.py rename to hypha/apply/projects/migrations/0104_invoice_tags.py diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py index 9ace85a013..5ce1ff6f86 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -55,6 +55,19 @@ class Meta: "tabindex": "0", # Accessibility } + def render_tags(self, record): + tags = record.tags.all() + if not tags: + return mark_safe("") + badges = "".join( + format_html( + "{}", + tag.name, + ) + for tag in tags + ) + return mark_safe(f"
{badges}
") + def render_requested_at(self, record): return format_html( "{}", @@ -101,6 +114,7 @@ def render_project(self, record): class FinanceInvoiceTable(BaseInvoiceTable): vendor_name = tables.Column(verbose_name=_("Vendor Name"), empty_values=()) + tags = tables.Column(verbose_name=_("Tags"), orderable=False, empty_values=()) selected = LabeledCheckboxColumn( accessor=A("pk"), attrs={ @@ -118,6 +132,7 @@ class Meta(BaseInvoiceTable.Meta): "status", "requested_at", "invoice_amount", + "tags", ] model = Invoice orderable = True @@ -170,19 +185,6 @@ class Meta(BaseInvoiceTable.Meta): def render_project(self, record): return get_project_title(record.project) - def render_tags(self, record): - tags = record.tags.all() - if not tags: - return mark_safe("") - badges = "".join( - format_html( - "{}", - tag.name, - ) - for tag in tags - ) - return mark_safe(f"
{badges}
") - class BaseProjectsTable(tables.Table): title = tables.LinkColumn( From 02aab8c734e816ecfc6453f315b59f41da7aae1c Mon Sep 17 00:00:00 2001 From: Fredrik Jonsson Date: Tue, 23 Jun 2026 12:12:31 +0200 Subject: [PATCH 8/8] Removed tags field from add/edit invoice forms since only staff should add them. --- hypha/apply/projects/forms/payment.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hypha/apply/projects/forms/payment.py b/hypha/apply/projects/forms/payment.py index 28a66776ba..7fffef4637 100644 --- a/hypha/apply/projects/forms/payment.py +++ b/hypha/apply/projects/forms/payment.py @@ -97,17 +97,12 @@ class Meta: "invoice_date", "document", "message_for_pm", - "tags", ] model = Invoice - widgets = { - "tags": MultiCheckboxesWidget, - } def __init__(self, user=None, *args, **kwargs): super().__init__(*args, **kwargs) self.initial["message_for_pm"] = "" - self.fields["tags"].required = False class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): @@ -130,7 +125,6 @@ class CreateInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", - "tags", ] def save(self, commit=True): @@ -157,7 +151,6 @@ class EditInvoiceForm(FileFormMixin, InvoiceBaseForm): "document", "supporting_documents", "message_for_pm", - "tags", ] @transaction.atomic