diff --git a/scipost_django/finances/constants.py b/scipost_django/finances/constants.py index 4c27616167891b2f738a8a18746a092e767378e8..7b0ba685de557228ccfa909b2afcee4bc2706aa8 100644 --- a/scipost_django/finances/constants.py +++ b/scipost_django/finances/constants.py @@ -27,10 +27,12 @@ SUBSIDY_PROMISED = "promised" SUBSIDY_INVOICED = "invoiced" SUBSIDY_RECEIVED = "received" SUBSIDY_WITHDRAWN = "withdrawn" +SUBSIDY_UPTODATE = "uptodate" SUBSIDY_STATUS = ( - (SUBSIDY_PROMISED, "promised"), - (SUBSIDY_INVOICED, "invoiced"), - (SUBSIDY_RECEIVED, "received"), - (SUBSIDY_WITHDRAWN, "withdrawn"), + (SUBSIDY_PROMISED, "Promised"), + (SUBSIDY_INVOICED, "Invoiced"), + (SUBSIDY_RECEIVED, "Received"), + (SUBSIDY_WITHDRAWN, "Withdrawn"), + (SUBSIDY_UPTODATE, "Up to date"), ) diff --git a/scipost_django/finances/forms.py b/scipost_django/finances/forms.py index c4ce038193eec632a2a6b87c490c2cf32fdd998a..f08517580e7d1e88e536ea7700ba3889a3027f35 100644 --- a/scipost_django/finances/forms.py +++ b/scipost_django/finances/forms.py @@ -7,16 +7,20 @@ import re from django import forms from django.contrib.auth import get_user_model from django.utils.dates import MONTHS -from django.db.models import Q, Sum +from django.db.models import Q, Case, DateField, Sum, Value, When, F from django.utils import timezone from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit -from crispy_forms.bootstrap import InlineRadios from crispy_bootstrap5.bootstrap5 import FloatingField from dal import autocomplete from dateutil.rrule import rrule, MONTHLY +from finances.constants import ( + SUBSIDY_STATUS, + SUBSIDY_TYPE_SPONSORSHIPAGREEMENT, + SUBSIDY_TYPES, +) from organizations.models import Organization from scipost.fields import UserModelChoiceField @@ -65,14 +69,34 @@ class SubsidySearchForm(forms.Form): required=False, label="Country name or code", ) - ordering = forms.ChoiceField( + status = forms.MultipleChoiceField( + label="Status", + choices=SUBSIDY_STATUS, + required=False, + ) + type = forms.MultipleChoiceField( + choices=SUBSIDY_TYPES, + required=False, + ) + + orderby = forms.ChoiceField( + label="Order by", choices=( ("amount", "Amount"), ("date_from", "Date from"), ("date_until", "Date until"), + ("annot_renewal_action_date", "Renewal date"), ), - initial="date_from", - widget=forms.RadioSelect, + required=False, + ) + ordering = forms.ChoiceField( + label="Ordering", + choices=( + # FIXME: Emperically, the ordering appers to be reversed for dates? + ("-", "Ascending"), + ("+", "Descending"), + ), + required=False, ) def __init__(self, *args, **kwargs): @@ -80,10 +104,19 @@ class SubsidySearchForm(forms.Form): self.helper = FormHelper() self.helper.layout = Layout( Div( - Div(FloatingField("organization_query"), css_class="col-lg-5"), - Div(FloatingField("country"), css_class="col-lg-3"), - Div(InlineRadios("ordering"), css_class="col-lg-4"), - css_class="row", + Div( + Div(FloatingField("organization_query"), css_class="col-12"), + Div( + Div(FloatingField("country"), css_class="col-12 col-lg-4"), + Div(FloatingField("orderby"), css_class="col-6 col-lg-4"), + Div(FloatingField("ordering"), css_class="col-6 col-lg-4"), + css_class="row mb-0", + ), + css_class="col-12 col-lg", + ), + Div(Field("status", size=6), css_class="col-12 col-lg-auto"), + Div(Field("type", size=6), css_class="col-12 col-lg-auto"), + css_class="row mb-0", ), ) @@ -92,6 +125,19 @@ class SubsidySearchForm(forms.Form): subsidies = Subsidy.objects.all() else: subsidies = Subsidy.objects.obtained() + + # Include `renewal_action_date` property in queryset + subsidies = subsidies.annotate( + annot_renewal_action_date=Case( + When( + Q(subsidy_type=SUBSIDY_TYPE_SPONSORSHIPAGREEMENT), + then=F("date_until") - datetime.timedelta(days=122), + ), + default=Value(None), + output_field=DateField(), + ) + ) + if self.cleaned_data["organization_query"]: subsidies = subsidies.filter( Q(organization__name__icontains=self.cleaned_data["organization_query"]) @@ -105,13 +151,30 @@ class SubsidySearchForm(forms.Form): subsidies = subsidies.filter( organization__country__icontains=self.cleaned_data["country"], ) - if self.cleaned_data["ordering"]: - if self.cleaned_data["ordering"] == "amount": - subsidies = subsidies.order_by("-amount") - if self.cleaned_data["ordering"] == "date_from": - subsidies = subsidies.order_by("-date_from") - if self.cleaned_data["ordering"] == "date_until": - subsidies = subsidies.order_by("-date_until") + + if status := self.cleaned_data["status"]: + subsidies = subsidies.filter(status__in=status) + + if subsidy_type := self.cleaned_data["type"]: + subsidies = subsidies.filter(subsidy_type__in=subsidy_type) + + # Ordering of subsidies + # Only order if both fields are set + if (orderby_value := self.cleaned_data.get("orderby")) and ( + ordering_value := self.cleaned_data.get("ordering") + ): + # Remove the + from the ordering value, causes a Django error + ordering_value = ordering_value.replace("+", "") + + # Ordering string is built by the ordering (+/-), and the field name + # from the orderby field split by "," and joined together + subsidies = subsidies.order_by( + *[ + ordering_value + order_part + for order_part in orderby_value.split(",") + ] + ) + return subsidies @@ -123,22 +186,27 @@ class SubsidyPaymentForm(forms.ModelForm): "reference", "amount", "date_scheduled", - "invoice", - "proof_of_payment", ) widgets = { "date_scheduled": forms.DateInput(attrs={"type": "date"}), } + invoice = forms.ChoiceField(required=False) + proof_of_payment = forms.ChoiceField(required=False) + def __init__(self, *args, **kwargs): subsidy = kwargs.pop("subsidy") super().__init__(*args, **kwargs) self.fields["subsidy"].initial = subsidy self.fields["subsidy"].widget = forms.HiddenInput() - self.fields["invoice"].queryset = subsidy.attachments.invoices() - self.fields[ - "proof_of_payment" - ].queryset = subsidy.attachments.proofs_of_payment() + self.fields["invoice"].choices = [ + (att.id, f"{att.attachment.name.split('/')[-1]}") + for att in subsidy.attachments.invoices() + ] + self.fields["proof_of_payment"].choices = [ + (att.id, f"{att.attachment.name.split('/')[-1]}") + for att in subsidy.attachments.proofs_of_payment() + ] self.helper = FormHelper() self.helper.layout = Layout( Field("subsidy"), diff --git a/scipost_django/finances/migrations/0028_alter_subsidy_status.py b/scipost_django/finances/migrations/0028_alter_subsidy_status.py new file mode 100644 index 0000000000000000000000000000000000000000..dabd6cd6657390bddc26397cfecbfa7eb649a3eb --- /dev/null +++ b/scipost_django/finances/migrations/0028_alter_subsidy_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-11-13 17:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0027_remove_subsidyattachment_name'), + ] + + operations = [ + migrations.AlterField( + model_name='subsidy', + name='status', + field=models.CharField(choices=[('promised', 'Promised'), ('invoiced', 'Invoiced'), ('received', 'Received'), ('withdrawn', 'Withdrawn'), ('uptodate', 'Up to date')], max_length=32), + ), + ] diff --git a/scipost_django/finances/templates/finances/subsidy_list.html b/scipost_django/finances/templates/finances/subsidy_list.html index ae0cad94eed0d4e7d656b410fd364a261e034843..fbcd0c91dc463fe311a0ca1afeddb5c72969e141 100644 --- a/scipost_django/finances/templates/finances/subsidy_list.html +++ b/scipost_django/finances/templates/finances/subsidy_list.html @@ -1,78 +1,111 @@ {% extends 'finances/base.html' %} - {% load crispy_forms_tags %} -{% block meta_description %}{{ block.super }} Subsidies List{% endblock meta_description %} -{% block pagetitle %}: Subsidies{% endblock pagetitle %} +{% block meta_description %} + {{ block.super }} Subsidies List +{% endblock meta_description %} + +{% block pagetitle %} + : Subsidies +{% endblock pagetitle %} {% load static %} {% load bootstrap %} - -{% block breadcrumb_items %} - {{ block.super }} - <span class="breadcrumb-item">Subsidies</span> -{% endblock %} +{% block breadcrumb_items %}{{ block.super }} <span class="breadcrumb-item">Subsidies</span>{% endblock %} {% block content %} - + <div class="row"> <div class="col-12"> <h1 class="highlight">Subsidies</h1> + {% if perms.scipost.can_manage_subsidies %} - <ul> - <li><a href="{% url 'finances:subsidy_create' %}">Add a Subsidy</a></li> - <li><a href="{% url 'finances:subsidies_old' %}" target="_blank">Go to the old list page</a></li> - </ul> + + <ul> + <li> + <a href="{% url 'finances:subsidy_create' %}">Add a Subsidy</a> + </li> + <li> + <a href="{% url 'finances:subsidies_old' %}" target="_blank">Go to the old list page</a> + </li> + </ul> {% endif %} + + </div> </div> - <div class="row"> <div class="col"> + <div class="card mb-2"> - <div class="card-header"> - Search / filter - <span id="indicator-subsidy-list" - class="htmx-indicator p-2" - > - <button class="btn btn-warning" type="button" disabled> - <strong>Loading...</strong> - <div class="spinner-grow spinner-grow-sm ms-2" role="status" aria-hidden="true"></div> - </button> - </span> - </div> - <div class="card-body"> - <form - id="subsidy-search-form" - hx-post="{% url 'finances:_hx_subsidy_list' %}" - hx-trigger="load, keyup delay:500ms, change" - hx-target="#subsidy-table-tbody" - hx-indicator="#indicator-subsidy-list" - > - {% crispy form %} - </form> - </div> + + <div class="card-header d-flex flex-row align-items-center justify-content-between"> + <span class="fs-5">Search / filter</span> + + <span> + <span id="indicator-subsidy-list" class="htmx-indicator p-2"> + <button class="btn btn-warning" type="button" disabled> + <strong>Loading...</strong> + <div class="spinner-grow spinner-grow-sm ms-2" + role="status" + aria-hidden="true"></div> + </button> + </span> + <button id="refresh-button" class="m-2 btn btn-primary"> + {% include "bi/arrow-clockwise.html" %} + Refresh + </button> + </span> + </div> + + <div class="card-body"> + <form id="subsidy-search-form" + hx-post="{% url 'finances:_hx_subsidy_list' %}" + hx-trigger="load, keyup delay:500ms, change, click from:#refresh-button" + hx-target="#subsidy-table-tbody" + hx-indicator="#indicator-subsidy-list"> + {% crispy form %} + </form> + </div> + </div> + + <table class="table"> - <thead class="table-light"> - <tr> - <th>From Organization</th> - <th>Type</th> - <th>Amount</th> - <th>From</th> - <th>Until</th> - {% if perms.scipost.can_manage_subsidies %} - <th>Status</th> - <th><span class="small" style="writing-mode: vertical-lr;">Renewable?</span></th> - <th><span class="small" style="writing-mode: vertical-lr;">Renewed?</span></th> - <th>Renewal<br/>action date</th> - {% endif %} - </tr> - <tbody id="subsidy-table-tbody"> - </tbody> - </table> - </div> - </div> + <thead class="table-light"> + <tr> + <th>From Organization</th> + <th>Type</th> + <th>Amount</th> + <th>From</th> + <th>Until</th> -{% endblock content %} + {% if perms.scipost.can_manage_subsidies %} + + <th>Status</th> + <th> + <span class="small" style="writing-mode: vertical-lr;">Renewable?</span> + </th> + <th> + <span class="small" style="writing-mode: vertical-lr;">Renewed?</span> + </th> + <th> + Renewal + <br /> + action date + </th> + {% endif %} + + + </tr> + + <tbody id="subsidy-table-tbody"> + </tbody> + + </table> + + + </div> + </div> + {% endblock content %}