Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions hypha/apply/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .models import (
ContractDocumentCategory,
DocumentCategory,
InvoiceTag,
ProjectForm,
ProjectReportForm,
ProjectSettings,
Expand Down Expand Up @@ -95,12 +96,20 @@ class ProjectSettingsAdmin(SettingModelAdmin):
model = ProjectSettings


class InvoiceTagAdmin(ModelAdmin):
model = InvoiceTag
menu_label = _("Invoice Tags")
menu_icon = "tag"
list_display = ("name",)


class ProjectAdminGroup(ModelAdminGroup):
menu_label = _("Projects")
menu_icon = str(AdminIcon.PROJECT)
items = (
ContractDocumentCategoryAdmin,
DocumentCategoryAdmin,
InvoiceTagAdmin,
ProjectFormAdmin,
ProjectReportFormAdmin,
ProjectSOWFormAdmin,
Expand Down
2 changes: 2 additions & 0 deletions hypha/apply/projects/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ChangeInvoiceStatusForm,
CreateInvoiceForm,
EditInvoiceForm,
InvoiceTagsForm,
SelectDocumentForm,
)
from .project import (
Expand Down Expand Up @@ -50,4 +51,5 @@
"CreateInvoiceForm",
"ChangeInvoiceStatusForm",
"EditInvoiceForm",
"InvoiceTagsForm",
]
15 changes: 15 additions & 0 deletions hypha/apply/projects/forms/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.translation import gettext_lazy as _
from django_file_form.forms import FileFormMixin

from hypha.apply.funds.widgets import MultiCheckboxesWidget
from hypha.apply.stream_forms.fields import MultiFileField, SingleFileField

from ..models.payment import (
Expand All @@ -22,6 +23,7 @@
RESUBMITTED,
SUBMITTED,
Invoice,
InvoiceTag,
SupportingDocument,
invoice_status_user_choices,
)
Expand Down Expand Up @@ -226,3 +228,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 InvoiceTagsForm(forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
queryset=InvoiceTag.objects.all(),
widget=MultiCheckboxesWidget,
required=False,
label=_("Tags"),
)

class Meta:
model = Invoice
fields = ["tags"]
42 changes: 42 additions & 0 deletions hypha/apply/projects/migrations/0104_invoice_tags.py
Original file line number Diff line number Diff line change
@@ -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="InvoiceTag",
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 tag",
"verbose_name_plural": "invoice tags",
"ordering": ["name"],
},
),
migrations.AddField(
model_name="invoice",
name="tags",
field=models.ManyToManyField(
blank=True,
related_name="invoices",
to="application_projects.invoicetag",
verbose_name="tags",
),
),
]
3 changes: 2 additions & 1 deletion hypha/apply/projects/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .payment import Invoice, SupportingDocument
from .payment import Invoice, InvoiceTag, SupportingDocument
from .project import (
Contract,
ContractDocumentCategory,
Expand Down Expand Up @@ -28,5 +28,6 @@
"DocumentCategory",
"ContractDocumentCategory",
"Invoice",
"InvoiceTag",
"SupportingDocument",
]
19 changes: 19 additions & 0 deletions hypha/apply/projects/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@

from hypha.apply.utils.storage import PrivateStorage


class InvoiceTag(models.Model):
name = models.CharField(max_length=100, unique=True)

class Meta:
verbose_name = _("invoice tag")
verbose_name_plural = _("invoice tags")
ordering = ["name"]

def __str__(self):
return self.name


SUBMITTED = "submitted"
RESUBMITTED = "resubmitted"
CHANGES_REQUESTED_BY_STAFF = "changes_requested_staff"
Expand Down Expand Up @@ -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)
tags = models.ManyToManyField(
InvoiceTag,
blank=True,
related_name="invoices",
verbose_name=_("tags"),
)
objects = InvoiceQueryset.as_manager()

wagtail_reference_index_ignore = True
Expand Down
17 changes: 17 additions & 0 deletions hypha/apply/projects/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ class Meta:
"tabindex": "0", # Accessibility
}

def render_tags(self, record):
tags = record.tags.all()
if not tags:
return mark_safe("<span class='text-base-content/40'>—</span>")
badges = "".join(
format_html(
"<span class='badge badge-soft badge-neutral whitespace-nowrap'>{}</span>",
tag.name,
)
for tag in tags
)
return mark_safe(f"<div class='flex flex-wrap gap-1'>{badges}</div>")

def render_requested_at(self, record):
return format_html(
"<relative-time datetime='{}' prefix=''>{}</relative-time>",
Expand Down Expand Up @@ -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={
Expand All @@ -118,6 +132,7 @@ class Meta(BaseInvoiceTable.Meta):
"status",
"requested_at",
"invoice_amount",
"tags",
]
model = Invoice
orderable = True
Expand All @@ -135,6 +150,7 @@ def render_vendor_name(self, record):

class AdminInvoiceListTable(BaseInvoiceTable):
project = tables.Column(verbose_name=_("Project Name"))
tags = tables.Column(verbose_name=_("Tags"), orderable=False, empty_values=())
selected = LabeledCheckboxColumn(
accessor=A("pk"),
attrs={
Expand All @@ -154,6 +170,7 @@ class Meta(BaseInvoiceTable.Meta):
"status",
"requested_at",
"project",
"tags",
]
model = Invoice
orderable = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
<p>
<b>{% trans "Fund" %}:</b> {{ object.project.submission.page }}
</p>
<p
class="lg:col-span-2"
hx-get="{% url 'apply:projects:partial-invoice-tags' pk=object.project.pk invoice_pk=object.pk %}"
hx-trigger="load, invoicesUpdated from:body"
></p>
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load i18n %}
<c-modal.header>{% trans "Update tags" %}</c-modal.header>

<div class="p-4">
<form
hx-post="{% url 'apply:projects:invoice-tags' pk=object.project.pk invoice_pk=object.pk %}"
hx-swap="innerHTML"
>
{% csrf_token %}
{% include "forms/includes/field.html" with field=form.tags %}

<div class="mt-4 sm:flex-row-reverse card-actions">
<button type="submit" class="w-full sm:w-auto btn btn-primary">
{% trans "Save" %}
</button>
<button type="button" class="w-full sm:w-auto btn btn-secondary btn-outline btn-soft" @click="show = false">
{% trans "Cancel" %}
</button>
</div>
</form>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4>
</a>
{% endif %}

<!-- Tags -->
{% if user.is_apply_staff or user.is_finance %}
<button
class="btn btn-secondary btn-outline btn-block"
hx-get="{% url 'apply:projects:invoice-tags' pk=object.project.pk invoice_pk=object.pk %}"
hx-target="#htmx-modal"
>
{% heroicon_micro "tag" aria_hidden=true class="opacity-80 size-4" %}
{% trans "Tags" %}
</button>
{% endif %}

<!-- Delete -->
{% can_delete object user as user_can_delete_request %}
{% if user_can_delete_request %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load i18n %}
{% with tags=object.tags.all %}
{% if tags %}
<b>{% trans "Tags" %}:</b> {{ tags|join:", " }}
{% endif %}
{% endwith %}
12 changes: 12 additions & 0 deletions hypha/apply/projects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
SendForApprovalView,
SkipPAFApprovalProcessView,
SubmitContractDocumentsView,
TagInvoiceView,
UpdateAssignApproversView,
UpdateLeadView,
UpdatePAFApproversView,
Expand All @@ -41,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,
Expand Down Expand Up @@ -252,6 +254,16 @@
partial_get_invoice_detail_actions,
name="partial-invoice-detail-actions",
),
path(
"tags/",
TagInvoiceView.as_view(),
name="invoice-tags",
),
path(
"partial/tags/",
partial_get_invoice_tags,
name="partial-invoice-tags",
),
path(
"documents/invoice/",
InvoicePrivateMedia.as_view(),
Expand Down
4 changes: 4 additions & 0 deletions hypha/apply/projects/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
InvoiceListView,
InvoicePrivateMedia,
InvoiceView,
TagInvoiceView,
)
from .project import (
AdminProjectDetailView,
Expand Down Expand Up @@ -45,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,
Expand All @@ -59,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",
Expand Down Expand Up @@ -98,4 +101,5 @@
"EditInvoiceView",
"DeleteInvoiceView",
"InvoicePrivateMedia",
"TagInvoiceView",
]
Loading